Merge from vscode bead496a613e475819f89f08e9e882b841bc1fe8 (#14883)

* Merge from vscode bead496a613e475819f89f08e9e882b841bc1fe8

* Bump distro

* Upgrade GCC to 4.9 due to yarn install errors

* Update build image

* Fix bootstrap base url

* Bump distro

* Fix build errors

* Update source map file

* Disable checkbox for blocking migration issues (#15131)

* disable checkbox for blocking issues

* wip

* disable checkbox fixes

* fix strings

* Remove duplicate tsec command

* Default to off for tab color if settings not present

* re-skip failing tests

* Fix mocha error

* Bump sqlite version & fix notebooks search view

* Turn off esbuild warnings

* Update esbuild log level

* Fix overflowactionbar tests

* Fix ts-ignore in dropdown tests

* cleanup/fixes

* Fix hygiene

* Bundle in entire zone.js module

* Remove extra constructor param

* bump distro for web compile break

* bump distro for web compile break v2

* Undo log level change

* New distro

* Fix integration test scripts

* remove the "no yarn.lock changes" workflow

* fix scripts v2

* Update unit test scripts

* Ensure ads-kerberos2 updates in .vscodeignore

* Try fix unit tests

* Upload crash reports

* remove nogpu

* always upload crashes

* Use bash script

* Consolidate data/ext dir names

* Create in tmp directory

Co-authored-by: chlafreniere <hichise@gmail.com>
Co-authored-by: Christopher Suh <chsuh@microsoft.com>
Co-authored-by: chgagnon <chgagnon@microsoft.com>
This commit is contained in:
Karl Burtram
2021-04-27 14:01:59 -07:00
committed by GitHub
parent 7e1c0076ba
commit 867a963882
1817 changed files with 81812 additions and 50843 deletions

View File

@@ -14,8 +14,8 @@ export interface ITelemetryData {
}
export type WorkbenchActionExecutedClassification = {
id: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
from: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
id: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; };
from: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; };
};
export type WorkbenchActionExecutedEvent = {
@@ -65,7 +65,7 @@ export interface IActionChangeEvent {
export class Action extends Disposable implements IAction {
protected _onDidChange = this._register(new Emitter<IActionChangeEvent>());
readonly onDidChange: Event<IActionChangeEvent> = this._onDidChange.event;
readonly onDidChange = this._onDidChange.event;
protected readonly _id: string;
protected _label: string;
@@ -198,10 +198,10 @@ export interface IRunEvent {
export class ActionRunner extends Disposable implements IActionRunner {
private _onBeforeRun = this._register(new Emitter<IRunEvent>());
readonly onBeforeRun: Event<IRunEvent> = this._onBeforeRun.event;
readonly onBeforeRun = this._onBeforeRun.event;
private _onDidRun = this._register(new Emitter<IRunEvent>());
readonly onDidRun: Event<IRunEvent> = this._onDidRun.event;
readonly onDidRun = this._onDidRun.event;
async run(action: IAction, context?: any): Promise<any> {
if (!action.enabled) {
@@ -265,14 +265,43 @@ export class ActionWithMenuAction extends Action {
}
}
export class SubmenuAction extends Action {
export class SubmenuAction implements IAction {
get actions(): IAction[] {
readonly id: string;
readonly label: string;
readonly class: string | undefined;
readonly tooltip: string = '';
readonly enabled: boolean = true;
readonly checked: boolean = false;
private readonly _actions: readonly IAction[];
constructor(id: string, label: string, actions: readonly IAction[], cssClass?: string) {
this.id = id;
this.label = label;
this.class = cssClass;
this._actions = actions;
}
dispose(): void {
// there is NOTHING to dispose and the SubmenuAction should
// never have anything to dispose as it is a convenience type
// to bridge into the rendering world.
}
get actions(): readonly IAction[] {
return this._actions;
}
constructor(id: string, label: string, private _actions: IAction[], cssClass?: string) {
super(id, label, cssClass, !!_actions?.length);
async run(): Promise<any> { }
// {{SQL CARBON EDIT}}
get expanded(): boolean {
return false;
}
set expanded(value: boolean) {
}
protected _setExpanded(value: boolean): void {
}
}
@@ -282,3 +311,18 @@ export class EmptySubmenuAction extends Action {
super(EmptySubmenuAction.ID, nls.localize('submenu.empty', '(empty)'), undefined, false);
}
}
export function toAction(props: { id: string, label: string, enabled?: boolean, checked?: boolean, run: Function; }): IAction {
return {
id: props.id,
label: props.label,
class: undefined,
// {{SQL CARBON EDIT}} - add expanded type
expanded: false,
enabled: props.enabled ?? true,
checked: props.checked ?? false,
run: async () => props.run(),
tooltip: props.label,
dispose: () => { }
};
}

View File

@@ -4,9 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import * as errors from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { canceled, onUnexpectedError } from 'vs/base/common/errors';
import { Emitter, Event, Listener } from 'vs/base/common/event';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { LinkedList } from 'vs/base/common/linkedList';
import { URI } from 'vs/base/common/uri';
export function isThenable<T>(obj: any): obj is Promise<T> {
@@ -23,7 +24,7 @@ export function createCancelablePromise<T>(callback: (token: CancellationToken)
const thenable = callback(source.token);
const promise = new Promise<T>((resolve, reject) => {
source.token.onCancellationRequested(() => {
reject(errors.canceled());
reject(canceled());
});
Promise.resolve(thenable).then(value => {
source.dispose();
@@ -152,13 +153,13 @@ export class Throttler {
return result;
};
this.queuedPromise = new Promise(c => {
this.activePromise!.then(onComplete, onComplete).then(c);
this.queuedPromise = new Promise(resolve => {
this.activePromise!.then(onComplete, onComplete).then(resolve);
});
}
return new Promise((c, e) => {
this.queuedPromise!.then(c, e);
return new Promise((resolve, reject) => {
this.queuedPromise!.then(resolve, reject);
});
}
@@ -181,7 +182,7 @@ export class Sequencer {
private current: Promise<any> = Promise.resolve(null);
queue<T>(promiseTask: ITask<Promise<T>>): Promise<T> {
return this.current = this.current.then(() => promiseTask());
return this.current = this.current.then(() => promiseTask(), () => promiseTask());
}
}
@@ -205,7 +206,7 @@ export class SequencerByKey<TKey> {
}
/**
* A helper to delay execution of a task that is being requested often.
* A helper to delay (debounce) execution of a task that is being requested often.
*
* Following the throttler, now imagine the mail man wants to optimize the number of
* trips proactively. The trip itself can be long, so he decides not to make the trip
@@ -248,9 +249,9 @@ export class Delayer<T> implements IDisposable {
this.cancelTimeout();
if (!this.completionPromise) {
this.completionPromise = new Promise((c, e) => {
this.doResolve = c;
this.doReject = e;
this.completionPromise = new Promise((resolve, reject) => {
this.doResolve = resolve;
this.doReject = reject;
}).then(() => {
this.completionPromise = null;
this.doResolve = null;
@@ -282,7 +283,7 @@ export class Delayer<T> implements IDisposable {
if (this.completionPromise) {
if (this.doReject) {
this.doReject(errors.canceled());
this.doReject(canceled());
}
this.completionPromise = null;
}
@@ -377,7 +378,7 @@ export function timeout(millis: number, token?: CancellationToken): CancelablePr
const handle = setTimeout(resolve, millis);
token.onCancellationRequested(() => {
clearTimeout(handle);
reject(errors.canceled());
reject(canceled());
});
});
}
@@ -1013,3 +1014,206 @@ export class IntervalCounter {
}
//#endregion
//#region
export type ValueCallback<T = any> = (value: T | Promise<T>) => void;
/**
* Creates a promise whose resolution or rejection can be controlled imperatively.
*/
export class DeferredPromise<T> {
private completeCallback!: ValueCallback<T>;
private errorCallback!: (err: any) => void;
private rejected = false;
private resolved = false;
public get isRejected() {
return this.rejected;
}
public get isResolved() {
return this.resolved;
}
public get isSettled() {
return this.rejected || this.resolved;
}
public p: Promise<T>;
constructor() {
this.p = new Promise<T>((c, e) => {
this.completeCallback = c;
this.errorCallback = e;
});
}
public complete(value: T) {
return new Promise<void>(resolve => {
this.completeCallback(value);
this.resolved = true;
resolve();
});
}
public error(err: any) {
return new Promise<void>(resolve => {
this.errorCallback(err);
this.rejected = true;
resolve();
});
}
public cancel() {
new Promise<void>(resolve => {
this.errorCallback(canceled());
this.rejected = true;
resolve();
});
}
}
//#endregion
//#region
export interface IWaitUntil {
waitUntil(thenable: Promise<any>): void;
}
export class AsyncEmitter<T extends IWaitUntil> extends Emitter<T> {
private _asyncDeliveryQueue?: LinkedList<[Listener<T>, Omit<T, 'waitUntil'>]>;
async fireAsync(data: Omit<T, 'waitUntil'>, token: CancellationToken, promiseJoin?: (p: Promise<any>, listener: Function) => Promise<any>): Promise<void> {
if (!this._listeners) {
return;
}
if (!this._asyncDeliveryQueue) {
this._asyncDeliveryQueue = new LinkedList();
}
for (const listener of this._listeners) {
this._asyncDeliveryQueue.push([listener, data]);
}
while (this._asyncDeliveryQueue.size > 0 && !token.isCancellationRequested) {
const [listener, data] = this._asyncDeliveryQueue.shift()!;
const thenables: Promise<any>[] = [];
const event = <T>{
...data,
waitUntil: (p: Promise<any>): void => {
if (Object.isFrozen(thenables)) {
throw new Error('waitUntil can NOT be called asynchronous');
}
if (promiseJoin) {
p = promiseJoin(p, typeof listener === 'function' ? listener : listener[0]);
}
thenables.push(p);
}
};
try {
if (typeof listener === 'function') {
listener.call(undefined, event);
} else {
listener[0].call(listener[1], event);
}
} catch (e) {
onUnexpectedError(e);
continue;
}
// freeze thenables-collection to enforce sync-calls to
// wait until and then wait for all thenables to resolve
Object.freeze(thenables);
await Promises.settled(thenables).catch(e => onUnexpectedError(e));
}
}
}
//#endregion
//#region Promises
export namespace Promises {
export interface IResolvedPromise<T> {
status: 'fulfilled';
value: T;
}
export interface IRejectedPromise {
status: 'rejected';
reason: Error;
}
/**
* Interface of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled
*/
interface PromiseWithAllSettled<T> {
allSettled<T>(promises: Promise<T>[]): Promise<ReadonlyArray<IResolvedPromise<T> | IRejectedPromise>>;
}
/**
* A polyfill of `Promise.allSettled`: returns after all promises have
* resolved or rejected and provides access to each result or error
* in the order of the original passed in promises array.
* See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled
*/
export async function allSettled<T>(promises: Promise<T>[]): Promise<ReadonlyArray<IResolvedPromise<T> | IRejectedPromise>> {
if (typeof (Promise as unknown as PromiseWithAllSettled<T>).allSettled === 'function') {
return allSettledNative(promises); // in some environments we can benefit from native implementation
}
return allSettledShim(promises);
}
async function allSettledNative<T>(promises: Promise<T>[]): Promise<ReadonlyArray<IResolvedPromise<T> | IRejectedPromise>> {
return (Promise as unknown as PromiseWithAllSettled<T>).allSettled(promises);
}
async function allSettledShim<T>(promises: Promise<T>[]): Promise<ReadonlyArray<IResolvedPromise<T> | IRejectedPromise>> {
return Promise.all(promises.map(promise => (promise.then(value => {
const fulfilled: IResolvedPromise<T> = { status: 'fulfilled', value };
return fulfilled;
}, error => {
const rejected: IRejectedPromise = { status: 'rejected', reason: error };
return rejected;
}))));
}
/**
* A drop-in replacement for `Promise.all` with the only difference
* that the method awaits every promise to either fulfill or reject.
*
* Similar to `Promise.all`, only the first error will be returned
* if any.
*/
export async function settled<T>(promises: Promise<T>[]): Promise<T[]> {
let firstError: Error | undefined = undefined;
const result = await Promise.all(promises.map(promise => promise.then(value => value, error => {
if (!firstError) {
firstError = error;
}
return undefined; // do not rethrow so that other promises can settle
})));
if (firstError) {
throw firstError;
}
return result as unknown as T[]; // cast is needed and protected by the `throw` above
}
}
//#endregion

View File

@@ -34,8 +34,9 @@ export class VSBuffer {
return new VSBuffer(actual);
}
static fromString(source: string): VSBuffer {
if (hasBuffer) {
static fromString(source: string, options?: { dontUseNodeBuffer?: boolean; }): VSBuffer {
const dontUseNodeBuffer = options?.dontUseNodeBuffer || false;
if (!dontUseNodeBuffer && hasBuffer) {
return new VSBuffer(Buffer.from(source));
} else if (hasTextEncoder) {
if (!textEncoder) {

View File

@@ -1,16 +0,0 @@
{
"name": "vs/base",
"dependencies": [
{
"name": "vs",
"internal": false
}
],
"libs": [
"lib.core.d.ts"
],
"sources": [
"**/*.ts"
],
"declares": []
}

View File

@@ -61,8 +61,57 @@ export class Codicon implements CSSIcon {
public get cssSelector() { return '.codicon.codicon-' + this.id; }
}
export function getClassNamesArray(id: string, modifier?: string) {
const classNames = ['codicon', 'codicon-' + id];
if (modifier) {
classNames.push('codicon-modifier-' + modifier);
}
return classNames;
}
export interface CSSIcon {
readonly classNames: string;
readonly id: string;
}
export namespace CSSIcon {
export const iconNameExpression = '[A-Za-z0-9\\-]+';
export const iconModifierExpression = '~[A-Za-z]+';
const cssIconIdRegex = new RegExp(`^(${iconNameExpression})(${iconModifierExpression})?$`);
export function asClassNameArray(icon: CSSIcon): string[] {
if (icon instanceof Codicon) {
return ['codicon', 'codicon-' + icon.id];
}
const match = cssIconIdRegex.exec(icon.id);
if (!match) {
return asClassNameArray(Codicon.error);
}
let [, id, modifier] = match;
// {{SQL CARBON EDIT}} Modifying method to not add 'codicon' in front of sql carbon icons.
let sqlCarbonIcons = ['book', 'dataExplorer'];
if (sqlCarbonIcons.includes(id)) {
return [id];
// {{SQL CARBON EDIT}} End of edit
} else {
const classNames = ['codicon', 'codicon-' + id];
if (modifier) {
classNames.push('codicon-modifier-' + modifier.substr(1));
}
return classNames;
}
}
export function asClassName(icon: CSSIcon): string {
return asClassNameArray(icon).join(' ');
}
export function asCSSSelector(icon: CSSIcon): string {
return '.' + asClassNameArray(icon).join('.');
}
}
@@ -498,6 +547,14 @@ export namespace Codicon {
export const passFilled = new Codicon('pass-filled', { character: '\\ebb3' });
export const circleLargeFilled = new Codicon('circle-large-filled', { character: '\\ebb4' });
export const circleLargeOutline = new Codicon('circle-large-outline', { character: '\\ebb5' });
export const combine = new Codicon('combine', { character: '\\ebb6' });
export const gather = new Codicon('gather', { character: '\\ebb6' });
export const table = new Codicon('table', { character: '\\ebb7' });
export const variableGroup = new Codicon('variable-group', { character: '\\ebb8' });
export const typeHierarchy = new Codicon('type-hierarchy', { character: '\\ebb9' });
export const typeHierarchySub = new Codicon('type-hierarchy-sub', { character: '\\ebba' });
export const typeHierarchySuper = new Codicon('type-hierarchy-super', { character: '\\ebbb' });
export const gitPullRequestCreate = new Codicon('git-pull-request-create', { character: '\\ebbc' });
export const dropDownButton = new Codicon('drop-down-button', Codicon.chevronDown.definition, localize('dropDownButton', 'Icon for drop down buttons.'));
}

View File

@@ -142,6 +142,15 @@ export function isPromiseCanceledError(error: any): boolean {
return error instanceof Error && error.name === canceledName && error.message === canceledName;
}
// !!!IMPORTANT!!!
// Do NOT change this class because it is also used as an API-type.
export class CancellationError extends Error {
constructor() {
super(canceledName);
this.name = this.message;
}
}
/**
* Returns an error that signals cancellation.
*/

View File

@@ -7,7 +7,7 @@ import { onUnexpectedError } from 'vs/base/common/errors';
import { once as onceFn } from 'vs/base/common/functional';
import { Disposable, IDisposable, toDisposable, combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { LinkedList } from 'vs/base/common/linkedList';
import { CancellationToken } from 'vs/base/common/cancellation';
import { StopWatch } from 'vs/base/common/stopwatch';
/**
* To an event a function with one or zero parameters
@@ -374,11 +374,11 @@ export namespace Event {
}
export function toPromise<T>(event: Event<T>): Promise<T> {
return new Promise(c => once(event)(c));
return new Promise(resolve => once(event)(resolve));
}
}
type Listener<T> = [(e: T) => void, any] | ((e: T) => void);
export type Listener<T> = [(e: T) => void, any] | ((e: T) => void);
export interface EmitterOptions {
onFirstListenerAdd?: Function;
@@ -386,6 +386,41 @@ export interface EmitterOptions {
onListenerDidAdd?: Function;
onLastListenerRemove?: Function;
leakWarningThreshold?: number;
/** ONLY enable this during development */
_profName?: string
}
class EventProfiling {
private static _idPool = 0;
private _name: string;
private _stopWatch?: StopWatch;
private _listenerCount: number = 0;
private _invocationCount = 0;
private _elapsedOverall = 0;
constructor(name: string) {
this._name = `${name}_${EventProfiling._idPool++}`;
}
start(listenerCount: number): void {
this._stopWatch = new StopWatch(true);
this._listenerCount = listenerCount;
}
stop(): void {
if (this._stopWatch) {
const elapsed = this._stopWatch.elapsed();
this._elapsedOverall += elapsed;
this._invocationCount += 1;
console.info(`did FIRE ${this._name}: elapsed_ms: ${elapsed.toFixed(5)}, listener: ${this._listenerCount} (elapsed_overall: ${this._elapsedOverall.toFixed(2)}, invocations: ${this._invocationCount})`);
this._stopWatch = undefined;
}
}
}
let _globalLeakWarningThreshold = -1;
@@ -487,6 +522,7 @@ export class Emitter<T> {
private readonly _options?: EmitterOptions;
private readonly _leakageMon?: LeakageMonitor;
private readonly _perfMon?: EventProfiling;
private _disposed: boolean = false;
private _event?: Event<T>;
private _deliveryQueue?: LinkedList<[Listener<T>, T]>;
@@ -494,9 +530,8 @@ export class Emitter<T> {
constructor(options?: EmitterOptions) {
this._options = options;
this._leakageMon = _globalLeakWarningThreshold > 0
? new LeakageMonitor(this._options && this._options.leakWarningThreshold)
: undefined;
this._leakageMon = _globalLeakWarningThreshold > 0 ? new LeakageMonitor(this._options && this._options.leakWarningThreshold) : undefined;
this._perfMon = this._options?._profName ? new EventProfiling(this._options._profName) : undefined;
}
/**
@@ -527,10 +562,7 @@ export class Emitter<T> {
}
// check and record this emitter for potential leakage
let removeMonitor: (() => void) | undefined;
if (this._leakageMon) {
removeMonitor = this._leakageMon.check(this._listeners.size);
}
const removeMonitor = this._leakageMon?.check(this._listeners.size);
let result: IDisposable;
result = {
@@ -580,6 +612,9 @@ export class Emitter<T> {
this._deliveryQueue.push([listener, event]);
}
// start/stop performance insight collection
this._perfMon?.start(this._deliveryQueue.size);
while (this._deliveryQueue.size > 0) {
const [listener, event] = this._deliveryQueue.shift()!;
try {
@@ -592,19 +627,15 @@ export class Emitter<T> {
onUnexpectedError(e);
}
}
this._perfMon?.stop();
}
}
dispose() {
if (this._listeners) {
this._listeners.clear();
}
if (this._deliveryQueue) {
this._deliveryQueue.clear();
}
if (this._leakageMon) {
this._leakageMon.dispose();
}
this._listeners?.clear();
this._deliveryQueue?.clear();
this._leakageMon?.dispose();
this._disposed = true;
}
}
@@ -617,7 +648,7 @@ export class PauseableEmitter<T> extends Emitter<T> {
constructor(options?: EmitterOptions & { merge?: (input: T[]) => T }) {
super(options);
this._mergeFn = options && options.merge;
this._mergeFn = options?.merge;
}
pause(): void {
@@ -654,64 +685,6 @@ export class PauseableEmitter<T> extends Emitter<T> {
}
}
export interface IWaitUntil {
waitUntil(thenable: Promise<any>): void;
}
export class AsyncEmitter<T extends IWaitUntil> extends Emitter<T> {
private _asyncDeliveryQueue?: LinkedList<[Listener<T>, Omit<T, 'waitUntil'>]>;
async fireAsync(data: Omit<T, 'waitUntil'>, token: CancellationToken, promiseJoin?: (p: Promise<any>, listener: Function) => Promise<any>): Promise<void> {
if (!this._listeners) {
return;
}
if (!this._asyncDeliveryQueue) {
this._asyncDeliveryQueue = new LinkedList();
}
for (const listener of this._listeners) {
this._asyncDeliveryQueue.push([listener, data]);
}
while (this._asyncDeliveryQueue.size > 0 && !token.isCancellationRequested) {
const [listener, data] = this._asyncDeliveryQueue.shift()!;
const thenables: Promise<any>[] = [];
const event = <T>{
...data,
waitUntil: (p: Promise<any>): void => {
if (Object.isFrozen(thenables)) {
throw new Error('waitUntil can NOT be called asynchronous');
}
if (promiseJoin) {
p = promiseJoin(p, typeof listener === 'function' ? listener : listener[0]);
}
thenables.push(p);
}
};
try {
if (typeof listener === 'function') {
listener.call(undefined, event);
} else {
listener[0].call(listener[1], event);
}
} catch (e) {
onUnexpectedError(e);
continue;
}
// freeze thenables-collection to enforce sync-calls to
// wait until and then wait for all thenables to resolve
Object.freeze(thenables);
await Promise.all(thenables).catch(e => onUnexpectedError(e));
}
}
}
export class EventMultiplexer<T> implements IDisposable {
private readonly emitter: Emitter<T>;

View File

@@ -277,14 +277,25 @@ export function isRootOrDriveLetter(path: string): boolean {
return false;
}
return isWindowsDriveLetter(pathNormalized.charCodeAt(0))
&& pathNormalized.charCodeAt(1) === CharCode.Colon
&& (path.length === 2 || pathNormalized.charCodeAt(2) === CharCode.Backslash);
return hasDriveLetter(pathNormalized) &&
(path.length === 2 || pathNormalized.charCodeAt(2) === CharCode.Backslash);
}
return pathNormalized === posix.sep;
}
export function hasDriveLetter(path: string): boolean {
if (isWindows) {
return isWindowsDriveLetter(path.charCodeAt(0)) && path.charCodeAt(1) === CharCode.Colon;
}
return false;
}
export function getDriveLetter(path: string): string | undefined {
return hasDriveLetter(path) ? path[0] : undefined;
}
export function indexOfPath(path: string, candidate: string, ignoreCase?: boolean): number {
if (candidate.length > path.length) {
return -1;

View File

@@ -370,24 +370,23 @@ export function anyScore(pattern: string, lowPattern: string, _patternPos: numbe
if (result) {
return result;
}
let matches = 0;
let matches: number[] = [];
let score = 0;
let idx = _wordPos;
for (let patternPos = 0; patternPos < lowPattern.length && patternPos < _maxLen; ++patternPos) {
const wordPos = lowWord.indexOf(lowPattern.charAt(patternPos), idx);
if (wordPos >= 0) {
score += 1;
matches += 2 ** wordPos;
matches.unshift(wordPos);
idx = wordPos + 1;
} else if (matches !== 0) {
} else if (matches.length > 0) {
// once we have started matching things
// we need to match the remaining pattern
// characters
break;
}
}
return [score, matches, _wordPos];
return [score, _wordPos, ...matches];
}
//#region --- fuzzyScore ---
@@ -396,19 +395,15 @@ export function createMatches(score: undefined | FuzzyScore): IMatch[] {
if (typeof score === 'undefined') {
return [];
}
const matches = score[1].toString(2);
const wordStart = score[2];
const res: IMatch[] = [];
for (let pos = wordStart; pos < _maxLen; pos++) {
if (matches[matches.length - (pos + 1)] === '1') {
const last = res[res.length - 1];
if (last && last.end === pos) {
last.end = pos + 1;
} else {
res.push({ start: pos, end: pos + 1 });
}
const wordPos = score[1];
for (let i = score.length - 1; i > 1; i--) {
const pos = score[i] + wordPos;
const last = res[res.length - 1];
if (last && last.end === pos) {
last.end = pos + 1;
} else {
res.push({ start: pos, end: pos + 1 });
}
}
return res;
@@ -418,20 +413,28 @@ const _maxLen = 128;
function initTable() {
const table: number[][] = [];
const row: number[] = [0];
for (let i = 1; i <= _maxLen; i++) {
row.push(-i);
const row: number[] = [];
for (let i = 0; i <= _maxLen; i++) {
row[i] = 0;
}
for (let i = 0; i <= _maxLen; i++) {
const thisRow = row.slice(0);
thisRow[0] = -i;
table.push(thisRow);
table.push(row.slice(0));
}
return table;
}
function initArr(maxLen: number) {
const row: number[] = [];
for (let i = 0; i <= maxLen; i++) {
row[i] = 0;
}
return row;
}
const _minWordMatchPos = initArr(2 * _maxLen); // min word position for a certain pattern position
const _maxWordMatchPos = initArr(2 * _maxLen); // max word position for a certain pattern position
const _diag = initTable(); // the length of a contiguous diagonal match
const _table = initTable();
const _scores = initTable();
const _arrows = <Arrow[][]>initTable();
const _debug = false;
@@ -460,14 +463,14 @@ function printTables(pattern: string, patternStart: number, word: string, wordSt
word = word.substr(wordStart);
console.log(printTable(_table, pattern, pattern.length, word, word.length));
console.log(printTable(_arrows, pattern, pattern.length, word, word.length));
console.log(printTable(_scores, pattern, pattern.length, word, word.length));
console.log(printTable(_diag, pattern, pattern.length, word, word.length));
}
function isSeparatorAtPos(value: string, index: number): boolean {
if (index < 0 || index >= value.length) {
return false;
}
const code = value.charCodeAt(index);
const code = value.codePointAt(index);
switch (code) {
case CharCode.Underline:
case CharCode.Dash:
@@ -479,8 +482,16 @@ function isSeparatorAtPos(value: string, index: number): boolean {
case CharCode.DoubleQuote:
case CharCode.Colon:
case CharCode.DollarSign:
case CharCode.LessThan:
case CharCode.OpenParen:
case CharCode.OpenSquareBracket:
return true;
case undefined:
return false;
default:
if (strings.isEmojiImprecise(code)) {
return true;
}
return false;
}
}
@@ -503,9 +514,13 @@ function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean {
return word[pos] !== wordLow[pos];
}
export function isPatternInWord(patternLow: string, patternPos: number, patternLen: number, wordLow: string, wordPos: number, wordLen: number): boolean {
export function isPatternInWord(patternLow: string, patternPos: number, patternLen: number, wordLow: string, wordPos: number, wordLen: number, fillMinWordPosArr = false): boolean {
while (patternPos < patternLen && wordPos < wordLen) {
if (patternLow[patternPos] === wordLow[wordPos]) {
if (fillMinWordPosArr) {
// Remember the min word position for each pattern position
_minWordMatchPos[patternPos] = wordPos;
}
patternPos += 1;
}
wordPos += 1;
@@ -513,21 +528,24 @@ export function isPatternInWord(patternLow: string, patternPos: number, patternL
return patternPos === patternLen; // pattern must be exhausted
}
const enum Arrow { Top = 0b1, Diag = 0b10, Left = 0b100 }
const enum Arrow { Diag = 1, Left = 2, LeftLeft = 3 }
/**
* A tuple of three values.
* An array representating a fuzzy match.
*
* 0. the score
* 1. the matches encoded as bitmask (2^53)
* 2. the offset at which matching started
* 1. the offset at which matching started
* 2. `<match_pos_N>`
* 3. `<match_pos_1>`
* 4. `<match_pos_0>` etc
*/
export type FuzzyScore = [number, number, number];
export type FuzzyScore = [score: number, wordStart: number, ...matches: number[]];// [number, number, number];
export namespace FuzzyScore {
/**
* No matches and value `-100`
*/
export const Default: [-100, 0, 0] = <[-100, 0, 0]>Object.freeze([-100, 0, 0]);
export const Default: FuzzyScore = ([-100, 0]);
export function isDefault(score?: FuzzyScore): score is [-100, 0, 0] {
return !score || (score[0] === -100 && score[1] === 0 && score[2] === 0);
@@ -550,58 +568,71 @@ export function fuzzyScore(pattern: string, patternLow: string, patternStart: nu
// Run a simple check if the characters of pattern occur
// (in order) at all in word. If that isn't the case we
// stop because no match will be possible
if (!isPatternInWord(patternLow, patternStart, patternLen, wordLow, wordStart, wordLen)) {
if (!isPatternInWord(patternLow, patternStart, patternLen, wordLow, wordStart, wordLen, true)) {
return undefined;
}
// Find the max matching word position for each pattern position
// NOTE: the min matching word position was filled in above, in the `isPatternInWord` call
_fillInMaxWordMatchPos(patternLen, wordLen, patternStart, wordStart, patternLow, wordLow);
let row: number = 1;
let column: number = 1;
let patternPos = patternStart;
let wordPos = wordStart;
let hasStrongFirstMatch = false;
const hasStrongFirstMatch = [false];
// There will be a match, fill in tables
for (row = 1, patternPos = patternStart; patternPos < patternLen; row++, patternPos++) {
for (column = 1, wordPos = wordStart; wordPos < wordLen; column++, wordPos++) {
// Reduce search space to possible matching word positions and to possible access from next row
const minWordMatchPos = _minWordMatchPos[patternPos];
const maxWordMatchPos = _maxWordMatchPos[patternPos];
const nextMaxWordMatchPos = (patternPos + 1 < patternLen ? _maxWordMatchPos[patternPos + 1] : wordLen);
const score = _doScore(pattern, patternLow, patternPos, patternStart, word, wordLow, wordPos);
for (column = minWordMatchPos - wordStart + 1, wordPos = minWordMatchPos; wordPos < nextMaxWordMatchPos; column++, wordPos++) {
if (patternPos === patternStart && score > 1) {
hasStrongFirstMatch = true;
let score = Number.MIN_SAFE_INTEGER;
let canComeDiag = false;
if (wordPos <= maxWordMatchPos) {
score = _doScore(
pattern, patternLow, patternPos, patternStart,
word, wordLow, wordPos, wordLen, wordStart,
_diag[row - 1][column - 1] === 0,
hasStrongFirstMatch
);
}
_scores[row][column] = score;
let diagScore = 0;
if (score !== Number.MAX_SAFE_INTEGER) {
canComeDiag = true;
diagScore = score + _table[row - 1][column - 1];
}
const diag = _table[row - 1][column - 1] + (score > 1 ? 1 : score);
const top = _table[row - 1][column] + -1;
const left = _table[row][column - 1] + -1;
const canComeLeft = wordPos > minWordMatchPos;
const leftScore = canComeLeft ? _table[row][column - 1] + (_diag[row][column - 1] > 0 ? -5 : 0) : 0; // penalty for a gap start
if (left >= top) {
// left or diag
if (left > diag) {
_table[row][column] = left;
_arrows[row][column] = Arrow.Left;
} else if (left === diag) {
_table[row][column] = left;
_arrows[row][column] = Arrow.Left | Arrow.Diag;
} else {
_table[row][column] = diag;
_arrows[row][column] = Arrow.Diag;
}
const canComeLeftLeft = wordPos > minWordMatchPos + 1 && _diag[row][column - 1] > 0;
const leftLeftScore = canComeLeftLeft ? _table[row][column - 2] + (_diag[row][column - 2] > 0 ? -5 : 0) : 0; // penalty for a gap start
if (canComeLeftLeft && (!canComeLeft || leftLeftScore >= leftScore) && (!canComeDiag || leftLeftScore >= diagScore)) {
// always prefer choosing left left to jump over a diagonal because that means a match is earlier in the word
_table[row][column] = leftLeftScore;
_arrows[row][column] = Arrow.LeftLeft;
_diag[row][column] = 0;
} else if (canComeLeft && (!canComeDiag || leftScore >= diagScore)) {
// always prefer choosing left since that means a match is earlier in the word
_table[row][column] = leftScore;
_arrows[row][column] = Arrow.Left;
_diag[row][column] = 0;
} else if (canComeDiag) {
_table[row][column] = diagScore;
_arrows[row][column] = Arrow.Diag;
_diag[row][column] = _diag[row - 1][column - 1] + 1;
} else {
// top or diag
if (top > diag) {
_table[row][column] = top;
_arrows[row][column] = Arrow.Top;
} else if (top === diag) {
_table[row][column] = top;
_arrows[row][column] = Arrow.Top | Arrow.Diag;
} else {
_table[row][column] = diag;
_arrows[row][column] = Arrow.Diag;
}
throw new Error(`not possible`);
}
}
}
@@ -610,144 +641,152 @@ export function fuzzyScore(pattern: string, patternLow: string, patternStart: nu
printTables(pattern, patternStart, word, wordStart);
}
if (!hasStrongFirstMatch && !firstMatchCanBeWeak) {
if (!hasStrongFirstMatch[0] && !firstMatchCanBeWeak) {
return undefined;
}
_matchesCount = 0;
_topScore = -100;
_wordStart = wordStart;
_firstMatchCanBeWeak = firstMatchCanBeWeak;
row--;
column--;
_findAllMatches2(row - 1, column - 1, patternLen === wordLen ? 1 : 0, 0, false);
if (_matchesCount === 0) {
return undefined;
const result: FuzzyScore = [_table[row][column], wordStart];
let backwardsDiagLength = 0;
let maxMatchColumn = 0;
while (row >= 1) {
// Find the column where we go diagonally up
let diagColumn = column;
do {
const arrow = _arrows[row][diagColumn];
if (arrow === Arrow.LeftLeft) {
diagColumn = diagColumn - 2;
} else if (arrow === Arrow.Left) {
diagColumn = diagColumn - 1;
} else {
// found the diagonal
break;
}
} while (diagColumn >= 1);
// Overturn the "forwards" decision if keeping the "backwards" diagonal would give a better match
if (
backwardsDiagLength > 1 // only if we would have a contiguous match of 3 characters
&& patternLow[patternStart + row - 1] === wordLow[wordStart + column - 1] // only if we can do a contiguous match diagonally
&& !isUpperCaseAtPos(diagColumn + wordStart - 1, word, wordLow) // only if the forwards chose diagonal is not an uppercase
&& backwardsDiagLength + 1 > _diag[row][diagColumn] // only if our contiguous match would be longer than the "forwards" contiguous match
) {
diagColumn = column;
}
if (diagColumn === column) {
// this is a contiguous match
backwardsDiagLength++;
} else {
backwardsDiagLength = 1;
}
if (!maxMatchColumn) {
// remember the last matched column
maxMatchColumn = diagColumn;
}
row--;
column = diagColumn - 1;
result.push(column);
}
return [_topScore, _topMatch2, wordStart];
if (wordLen === patternLen) {
// the word matches the pattern with all characters!
// giving the score a total match boost (to come up ahead other words)
result[0] += 2;
}
// Add 1 penalty for each skipped character in the word
const skippedCharsCount = maxMatchColumn - patternLen;
result[0] -= skippedCharsCount;
return result;
}
function _doScore(pattern: string, patternLow: string, patternPos: number, patternStart: number, word: string, wordLow: string, wordPos: number) {
if (patternLow[patternPos] !== wordLow[wordPos]) {
return -1;
function _fillInMaxWordMatchPos(patternLen: number, wordLen: number, patternStart: number, wordStart: number, patternLow: string, wordLow: string) {
let patternPos = patternLen - 1;
let wordPos = wordLen - 1;
while (patternPos >= patternStart && wordPos >= wordStart) {
if (patternLow[patternPos] === wordLow[wordPos]) {
_maxWordMatchPos[patternPos] = wordPos;
patternPos--;
}
wordPos--;
}
}
function _doScore(
pattern: string, patternLow: string, patternPos: number, patternStart: number,
word: string, wordLow: string, wordPos: number, wordLen: number, wordStart: number,
newMatchStart: boolean,
outFirstMatchStrong: boolean[],
): number {
if (patternLow[patternPos] !== wordLow[wordPos]) {
return Number.MIN_SAFE_INTEGER;
}
let score = 1;
let isGapLocation = false;
if (wordPos === (patternPos - patternStart)) {
// common prefix: `foobar <-> foobaz`
// ^^^^^
if (pattern[patternPos] === word[wordPos]) {
return 7;
} else {
return 5;
}
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
} else if (isUpperCaseAtPos(wordPos, word, wordLow) && (wordPos === 0 || !isUpperCaseAtPos(wordPos - 1, word, wordLow))) {
// hitting upper-case: `foo <-> forOthers`
// ^^ ^
if (pattern[patternPos] === word[wordPos]) {
return 7;
} else {
return 5;
}
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
isGapLocation = true;
} else if (isSeparatorAtPos(wordLow, wordPos) && (wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1))) {
// hitting a separator: `. <-> foo.bar`
// ^
return 5;
score = 5;
} else if (isSeparatorAtPos(wordLow, wordPos - 1) || isWhitespaceAtPos(wordLow, wordPos - 1)) {
// post separator: `foo <-> bar_foo`
// ^^^
return 5;
score = 5;
isGapLocation = true;
}
if (score > 1 && patternPos === patternStart) {
outFirstMatchStrong[0] = true;
}
if (!isGapLocation) {
isGapLocation = isUpperCaseAtPos(wordPos, word, wordLow) || isSeparatorAtPos(wordLow, wordPos - 1) || isWhitespaceAtPos(wordLow, wordPos - 1);
}
//
if (patternPos === patternStart) { // first character in pattern
if (wordPos > wordStart) {
// the first pattern character would match a word character that is not at the word start
// so introduce a penalty to account for the gap preceding this match
score -= isGapLocation ? 3 : 5;
}
} else {
return 1;
}
}
let _matchesCount: number = 0;
let _topMatch2: number = 0;
let _topScore: number = 0;
let _wordStart: number = 0;
let _firstMatchCanBeWeak: boolean = false;
function _findAllMatches2(row: number, column: number, total: number, matches: number, lastMatched: boolean): void {
if (_matchesCount >= 10 || total < -25) {
// stop when having already 10 results, or
// when a potential alignment as already 5 gaps
return;
}
let simpleMatchCount = 0;
while (row > 0 && column > 0) {
const score = _scores[row][column];
const arrow = _arrows[row][column];
if (arrow === Arrow.Left) {
// left -> no match, skip a word character
column -= 1;
if (lastMatched) {
total -= 5; // new gap penalty
} else if (matches !== 0) {
total -= 1; // gap penalty after first match
}
lastMatched = false;
simpleMatchCount = 0;
} else if (arrow & Arrow.Diag) {
if (arrow & Arrow.Left) {
// left
_findAllMatches2(
row,
column - 1,
matches !== 0 ? total - 1 : total, // gap penalty after first match
matches,
lastMatched
);
}
// diag
total += score;
row -= 1;
column -= 1;
lastMatched = true;
// match -> set a 1 at the word pos
matches += 2 ** (column + _wordStart);
// count simple matches and boost a row of
// simple matches when they yield in a
// strong match.
if (score === 1) {
simpleMatchCount += 1;
if (row === 0 && !_firstMatchCanBeWeak) {
// when the first match is a weak
// match we discard it
return undefined;
}
} else {
// boost
total += 1 + (simpleMatchCount * (score - 1));
simpleMatchCount = 0;
}
if (newMatchStart) {
// this would be the beginning of a new match (i.e. there would be a gap before this location)
score += isGapLocation ? 2 : 0;
} else {
return undefined;
// this is part of a contiguous match, so give it a slight bonus, but do so only if it would not be a prefered gap location
score += isGapLocation ? 0 : 1;
}
}
total -= column >= 3 ? 9 : column * 3; // late start penalty
// dynamically keep track of the current top score
// and insert the current best score at head, the rest at tail
_matchesCount += 1;
if (total > _topScore) {
_topScore = total;
_topMatch2 = matches;
if (wordPos + 1 === wordLen) {
// we always penalize gaps, but this gives unfair advantages to a match that would match the last character in the word
// so pretend there is a gap after the last character in the word to normalize things
score -= isGapLocation ? 3 : 5;
}
return score;
}
//#endregion

View File

@@ -389,7 +389,7 @@ export function scoreItemFuzzy<T>(item: T, query: IPreparedQuery, fuzzy: boolean
// - description (if provided)
// - query (normalized)
// - number of query pieces (i.e. 'hello world' and 'helloworld' are different)
// - wether fuzzy matching is enabled or not
// - whether fuzzy matching is enabled or not
let cacheHash: string;
if (description) {
cacheHash = `${label}${description}${query.normalized}${Array.isArray(query.values) ? query.values.length : ''}${fuzzy}`;

View File

@@ -393,15 +393,24 @@ function trivia3(pattern: string, options: IGlobOptions): ParsedStringPattern {
}
// common patterns: **/something/else just need endsWith check, something/else just needs and equals check
function trivia4and5(path: string, pattern: string, matchPathEnds: boolean): ParsedStringPattern {
const nativePath = paths.sep !== paths.posix.sep ? path.replace(ALL_FORWARD_SLASHES, paths.sep) : path;
function trivia4and5(targetPath: string, pattern: string, matchPathEnds: boolean): ParsedStringPattern {
const usingPosixSep = paths.sep === paths.posix.sep;
const nativePath = usingPosixSep ? targetPath : targetPath.replace(ALL_FORWARD_SLASHES, paths.sep);
const nativePathEnd = paths.sep + nativePath;
const parsedPattern: ParsedStringPattern = matchPathEnds ? function (path, basename) {
return typeof path === 'string' && (path === nativePath || path.endsWith(nativePathEnd)) ? pattern : null;
} : function (path, basename) {
return typeof path === 'string' && path === nativePath ? pattern : null;
const targetPathEnd = paths.posix.sep + targetPath;
const parsedPattern: ParsedStringPattern = matchPathEnds ? function (testPath, basename) {
return typeof testPath === 'string' &&
((testPath === nativePath || testPath.endsWith(nativePathEnd))
|| !usingPosixSep && (testPath === targetPath || testPath.endsWith(targetPathEnd)))
? pattern : null;
} : function (testPath, basename) {
return typeof testPath === 'string' &&
(testPath === nativePath
|| (!usingPosixSep && testPath === targetPath))
? pattern : null;
};
parsedPattern.allPaths = [(matchPathEnds ? '*/' : './') + path];
parsedPattern.allPaths = [(matchPathEnds ? '*/' : './') + targetPath];
return parsedPattern;
}

View File

@@ -5,7 +5,7 @@
import { equals } from 'vs/base/common/arrays';
import { UriComponents } from 'vs/base/common/uri';
import { escapeCodicons } from 'vs/base/common/codicons';
import { escapeIcons } from 'vs/base/common/iconLabels';
import { illegalArgument } from 'vs/base/common/errors';
export interface IMarkdownString {
@@ -46,9 +46,7 @@ export class MarkdownString implements IMarkdownString {
}
appendText(value: string, newlineStyle: MarkdownStringTextNewlineStyle = MarkdownStringTextNewlineStyle.Paragraph): MarkdownString {
// escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash
this.value += (this.supportThemeIcons ? escapeCodicons(value) : value)
.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&')
this.value += escapeMarkdownSyntaxTokens(this.supportThemeIcons ? escapeIcons(value) : value)
.replace(/([ \t]+)/g, (_match, g1) => '&nbsp;'.repeat(g1.length))
.replace(/^>/gm, '\\>')
.replace(/\n/g, newlineStyle === MarkdownStringTextNewlineStyle.Break ? '\\\n' : '\n\n');
@@ -116,6 +114,11 @@ function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean {
}
}
export function escapeMarkdownSyntaxTokens(text: string): string {
// escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash
return text.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&');
}
export function removeMarkdownEscapes(text: string): string {
if (!text) {
return text;

View File

@@ -0,0 +1,159 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CSSIcon } from 'vs/base/common/codicons';
import { matchesFuzzy, IMatch } from 'vs/base/common/filters';
import { ltrim } from 'vs/base/common/strings';
export const iconStartMarker = '$(';
const iconsRegex = new RegExp(`\\$\\(${CSSIcon.iconNameExpression}(?:${CSSIcon.iconModifierExpression})?\\)`, 'g'); // no capturing groups
const escapeIconsRegex = new RegExp(`(\\\\)?${iconsRegex.source}`, 'g');
export function escapeIcons(text: string): string {
return text.replace(escapeIconsRegex, (match, escaped) => escaped ? match : `\\${match}`);
}
const markdownEscapedIconsRegex = new RegExp(`\\\\${iconsRegex.source}`, 'g');
export function markdownEscapeEscapedIcons(text: string): string {
// Need to add an extra \ for escaping in markdown
return text.replace(markdownEscapedIconsRegex, match => `\\${match}`);
}
const stripIconsRegex = new RegExp(`(\\s)?(\\\\)?${iconsRegex.source}(\\s)?`, 'g');
export function stripIcons(text: string): string {
if (text.indexOf(iconStartMarker) === -1) {
return text;
}
return text.replace(stripIconsRegex, (match, preWhitespace, escaped, postWhitespace) => escaped ? match : preWhitespace || postWhitespace || '');
}
export interface IParsedLabelWithIcons {
readonly text: string;
readonly iconOffsets?: readonly number[];
}
export function parseLabelWithIcons(text: string): IParsedLabelWithIcons {
const firstIconIndex = text.indexOf(iconStartMarker);
if (firstIconIndex === -1) {
return { text }; // return early if the word does not include an icon
}
return doParseLabelWithIcons(text, firstIconIndex);
}
function doParseLabelWithIcons(text: string, firstIconIndex: number): IParsedLabelWithIcons {
const iconOffsets: number[] = [];
let textWithoutIcons: string = '';
function appendChars(chars: string) {
if (chars) {
textWithoutIcons += chars;
for (const _ of chars) {
iconOffsets.push(iconsOffset); // make sure to fill in icon offsets
}
}
}
let currentIconStart = -1;
let currentIconValue: string = '';
let iconsOffset = 0;
let char: string;
let nextChar: string;
let offset = firstIconIndex;
const length = text.length;
// Append all characters until the first icon
appendChars(text.substr(0, firstIconIndex));
// example: $(file-symlink-file) my cool $(other-icon) entry
while (offset < length) {
char = text[offset];
nextChar = text[offset + 1];
// beginning of icon: some value $( <--
if (char === iconStartMarker[0] && nextChar === iconStartMarker[1]) {
currentIconStart = offset;
// if we had a previous potential icon value without
// the closing ')', it was actually not an icon and
// so we have to add it to the actual value
appendChars(currentIconValue);
currentIconValue = iconStartMarker;
offset++; // jump over '('
}
// end of icon: some value $(some-icon) <--
else if (char === ')' && currentIconStart !== -1) {
const currentIconLength = offset - currentIconStart + 1; // +1 to include the closing ')'
iconsOffset += currentIconLength;
currentIconStart = -1;
currentIconValue = '';
}
// within icon
else if (currentIconStart !== -1) {
// Make sure this is a real icon name
if (/^[a-z0-9\-]$/i.test(char)) {
currentIconValue += char;
} else {
// This is not a real icon, treat it as text
appendChars(currentIconValue);
currentIconStart = -1;
currentIconValue = '';
}
}
// any value outside of icon
else {
appendChars(char);
}
offset++;
}
// if we had a previous potential icon value without
// the closing ')', it was actually not an icon and
// so we have to add it to the actual value
appendChars(currentIconValue);
return { text: textWithoutIcons, iconOffsets };
}
export function matchesFuzzyIconAware(query: string, target: IParsedLabelWithIcons, enableSeparateSubstringMatching = false): IMatch[] | null {
const { text, iconOffsets } = target;
// Return early if there are no icon markers in the word to match against
if (!iconOffsets || iconOffsets.length === 0) {
return matchesFuzzy(query, text, enableSeparateSubstringMatching);
}
// Trim the word to match against because it could have leading
// whitespace now if the word started with an icon
const wordToMatchAgainstWithoutIconsTrimmed = ltrim(text, ' ');
const leadingWhitespaceOffset = text.length - wordToMatchAgainstWithoutIconsTrimmed.length;
// match on value without icon
const matches = matchesFuzzy(query, wordToMatchAgainstWithoutIconsTrimmed, enableSeparateSubstringMatching);
// Map matches back to offsets with icon and trimming
if (matches) {
for (const match of matches) {
const iconOffset = iconOffsets[match.start + leadingWhitespaceOffset] /* icon offsets at index */ + leadingWhitespaceOffset /* overall leading whitespace offset */;
match.start += iconOffset;
match.end += iconOffset;
}
}
return matches;
}

View File

@@ -22,6 +22,10 @@ export namespace Iterable {
return iterable || _empty;
}
export function isEmpty<T>(iterable: Iterable<T> | undefined | null): boolean {
return !iterable || iterable[Symbol.iterator]().next().done === true;
}
export function first<T>(iterable: Iterable<T>): T | undefined {
return iterable[Symbol.iterator]().next().value;
}
@@ -35,6 +39,8 @@ export namespace Iterable {
return false;
}
export function filter<T, R extends T>(iterable: Iterable<T>, predicate: (t: T) => t is R): Iterable<R>;
export function filter<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): Iterable<T>;
export function* filter<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): Iterable<T> {
for (const element of iterable) {
if (predicate(element)) {
@@ -57,6 +63,41 @@ export namespace Iterable {
}
}
export function* concatNested<T>(iterables: Iterable<Iterable<T>>): Iterable<T> {
for (const iterable of iterables) {
for (const element of iterable) {
yield element;
}
}
}
export function reduce<T, R>(iterable: Iterable<T>, reducer: (previousValue: R, currentValue: T) => R, initialValue: R): R {
let value = initialValue;
for (const element of iterable) {
value = reducer(value, element);
}
return value;
}
/**
* Returns an iterable slice of the array, with the same semantics as `array.slice()`.
*/
export function* slice<T>(iterable: ReadonlyArray<T>, from: number, to = iterable.length): Iterable<T> {
if (from < 0) {
from += iterable.length;
}
if (to < 0) {
to += iterable.length;
} else if (to > iterable.length) {
to = iterable.length;
}
for (; from < to; from++) {
yield iterable[from];
}
}
/**
* Consumes `atMost` elements from iterable and returns the consumed elements,
* and an iterable for the rest of the elements.

View File

@@ -597,6 +597,17 @@ export abstract class ResolvedKeybinding {
/**
* Returns the parts that should be used for dispatching.
* Returns null for parts consisting of only modifier keys
* @example keybinding "Shift" -> null
* @example keybinding ("D" with shift == true) -> "shift+D"
*/
public abstract getDispatchParts(): (string | null)[];
/**
* Returns the parts that should be used for dispatching single modifier keys
* Returns null for parts that contain more than one modifier or a regular key.
* @example keybinding "Shift" -> "shift"
* @example keybinding ("D" with shift == true") -> null
*/
public abstract getSingleModifierDispatchParts(): (string | null)[];
}

View File

@@ -9,11 +9,12 @@ import { startsWithIgnoreCase, rtrim } from 'vs/base/common/strings';
import { Schemas } from 'vs/base/common/network';
import { isLinux, isWindows, isMacintosh } from 'vs/base/common/platform';
import { isEqual, basename, relativePath } from 'vs/base/common/resources';
import { hasDriveLetter, isRootOrDriveLetter } from 'vs/base/common/extpath';
export interface IWorkspaceFolderProvider {
getWorkspaceFolder(resource: URI): { uri: URI, name?: string } | null;
getWorkspaceFolder(resource: URI): { uri: URI, name?: string; } | null;
getWorkspace(): {
folders: { uri: URI, name?: string }[];
folders: { uri: URI, name?: string; }[];
};
}
@@ -84,21 +85,13 @@ export function getBaseLabel(resource: URI | string | undefined): string | undef
const base = basename(resource) || (resource.scheme === Schemas.file ? resource.fsPath : resource.path) /* can be empty string if '/' is passed in */;
// convert c: => C:
if (hasDriveLetter(base)) {
if (isWindows && isRootOrDriveLetter(base)) {
return normalizeDriveLetter(base);
}
return base;
}
function hasDriveLetter(path: string): boolean {
return !!(isWindows && path && path[1] === ':');
}
export function extractDriveLetter(path: string): string | undefined {
return hasDriveLetter(path) ? path[0] : undefined;
}
export function normalizeDriveLetter(path: string): string {
if (hasDriveLetter(path)) {
return path.charAt(0).toUpperCase() + path.slice(1);
@@ -107,7 +100,7 @@ export function normalizeDriveLetter(path: string): string {
return path;
}
let normalizedUserHomeCached: { original: string; normalized: string } = Object.create(null);
let normalizedUserHomeCached: { original: string; normalized: string; } = Object.create(null);
export function tildify(path: string, userHome: string): string {
if (isWindows || !path || !userHome) {
return path; // unsupported
@@ -286,7 +279,7 @@ interface ISegment {
* @param value string to which templating is applied
* @param values the values of the templates to use
*/
export function template(template: string, values: { [key: string]: string | ISeparator | undefined | null } = Object.create(null)): string {
export function template(template: string, values: { [key: string]: string | ISeparator | undefined | null; } = Object.create(null)): string {
const segments: ISegment[] = [];
let inVariable = false;
@@ -390,7 +383,7 @@ export function unmnemonicLabel(label: string): string {
/**
* Splits a path in name and parent path, supporting both '/' and '\'
*/
export function splitName(fullPath: string): { name: string, parentPath: string } {
export function splitName(fullPath: string): { name: string, parentPath: string; } {
const p = fullPath.indexOf('/') !== -1 ? posix : win32;
const name = p.basename(fullPath);
const parentPath = p.dirname(fullPath);

View File

@@ -14,34 +14,53 @@ import { Iterable } from 'vs/base/common/iterator';
* extend Disposable or use a DisposableStore. This means there are a lot of false positives.
*/
const TRACK_DISPOSABLES = false;
let disposableTracker: IDisposableTracker | null = null;
const __is_disposable_tracked__ = '__is_disposable_tracked__';
function markTracked<T extends IDisposable>(x: T): void {
if (!TRACK_DISPOSABLES) {
return;
}
if (x && x !== Disposable.None) {
try {
(x as any)[__is_disposable_tracked__] = true;
} catch {
// noop
}
}
export interface IDisposableTracker {
trackDisposable(x: IDisposable): void;
markTracked(x: IDisposable): void;
}
function trackDisposable<T extends IDisposable>(x: T): T {
if (!TRACK_DISPOSABLES) {
export function setDisposableTracker(tracker: IDisposableTracker | null): void {
disposableTracker = tracker;
}
if (TRACK_DISPOSABLES) {
const __is_disposable_tracked__ = '__is_disposable_tracked__';
disposableTracker = new class implements IDisposableTracker {
trackDisposable(x: IDisposable): void {
const stack = new Error('Potentially leaked disposable').stack!;
setTimeout(() => {
if (!(x as any)[__is_disposable_tracked__]) {
console.log(stack);
}
}, 3000);
}
markTracked(x: IDisposable): void {
if (x && x !== Disposable.None) {
try {
(x as any)[__is_disposable_tracked__] = true;
} catch {
// noop
}
}
}
};
}
function markTracked<T extends IDisposable>(x: T): void {
if (!disposableTracker) {
return;
}
disposableTracker.markTracked(x);
}
export function trackDisposable<T extends IDisposable>(x: T): T {
if (!disposableTracker) {
return x;
}
const stack = new Error('Potentially leaked disposable').stack!;
setTimeout(() => {
if (!(x as any)[__is_disposable_tracked__]) {
console.log(stack);
}
}, 3000);
disposableTracker.trackDisposable(x);
return x;
}
@@ -49,7 +68,7 @@ export class MultiDisposeError extends Error {
constructor(
public readonly errors: any[]
) {
super(`Encounter errors while disposing of store. Errors: [${errors.join(', ')}]`);
super(`Encountered errors while disposing of store. Errors: [${errors.join(', ')}]`);
}
}
@@ -98,7 +117,7 @@ export function dispose<T extends IDisposable>(arg: T | IterableIterator<T> | un
export function combinedDisposable(...disposables: IDisposable[]): IDisposable {
disposables.forEach(markTracked);
return trackDisposable({ dispose: () => dispose(disposables) });
return toDisposable(() => dispose(disposables));
}
export function toDisposable(fn: () => void): IDisposable {
@@ -212,9 +231,7 @@ export class MutableDisposable<T extends IDisposable> implements IDisposable {
return;
}
if (this._value) {
this._value.dispose();
}
this._value?.dispose();
if (value) {
markTracked(value);
}
@@ -228,9 +245,7 @@ export class MutableDisposable<T extends IDisposable> implements IDisposable {
dispose(): void {
this._isDisposed = true;
markTracked(this);
if (this._value) {
this._value.dispose();
}
this._value?.dispose();
this._value = undefined;
}
}

View File

@@ -377,7 +377,8 @@ export class TernarySearchTree<K, V> {
}
has(key: K): boolean {
return !!this._getNode(key);
const node = this._getNode(key);
return !(node?.value === undefined && node?.mid === undefined);
}
delete(key: K): void {

View File

@@ -60,6 +60,8 @@ export namespace Schemas {
export const vscodeNotebookCell = 'vscode-notebook-cell';
export const vscodeNotebookCellMetadata = 'vscode-notebook-cell-metadata';
export const vscodeSettings = 'vscode-settings';
export const webviewPanel = 'webview-panel';
@@ -147,8 +149,8 @@ class FileAccessImpl {
* **Note:** use `dom.ts#asCSSUrl` whenever the URL is to be used in CSS context.
*/
asBrowserUri(uri: URI): URI;
asBrowserUri(moduleId: string, moduleIdToUrl: { toUrl(moduleId: string): string }): URI;
asBrowserUri(uriOrModule: URI | string, moduleIdToUrl?: { toUrl(moduleId: string): string }): URI {
asBrowserUri(moduleId: string, moduleIdToUrl: { toUrl(moduleId: string): string }, __forceCodeFileUri?: boolean): URI;
asBrowserUri(uriOrModule: URI | string, moduleIdToUrl?: { toUrl(moduleId: string): string }, __forceCodeFileUri?: boolean): URI {
const uri = this.toUri(uriOrModule, moduleIdToUrl);
// Handle remote URIs via `RemoteAuthorities`
@@ -157,37 +159,23 @@ class FileAccessImpl {
}
// Only convert the URI if we are in a native context and it has `file:` scheme
if (platform.isElectronSandboxed && platform.isNative && uri.scheme === Schemas.file) {
return this.toCodeFileUri(uri);
// and we have explicitly enabled the conversion (sandbox, or ENABLE_VSCODE_BROWSER_CODE_LOADING)
if (platform.isNative && (__forceCodeFileUri || platform.isPreferringBrowserCodeLoad) && uri.scheme === Schemas.file) {
return uri.with({
scheme: Schemas.vscodeFileResource,
// We need to provide an authority here so that it can serve
// as origin for network and loading matters in chromium.
// If the URI is not coming with an authority already, we
// add our own
authority: uri.authority || this.FALLBACK_AUTHORITY,
query: null,
fragment: null
});
}
return uri;
}
/**
* TODO@bpasero remove me eventually when vscode-file is adopted everywhere
*/
_asCodeFileUri(uri: URI): URI;
_asCodeFileUri(moduleId: string, moduleIdToUrl: { toUrl(moduleId: string): string }): URI;
_asCodeFileUri(uriOrModule: URI | string, moduleIdToUrl?: { toUrl(moduleId: string): string }): URI {
const uri = this.toUri(uriOrModule, moduleIdToUrl);
return this.toCodeFileUri(uri);
}
private toCodeFileUri(uri: URI): URI {
return uri.with({
scheme: Schemas.vscodeFileResource,
// We need to provide an authority here so that it can serve
// as origin for network and loading matters in chromium.
// If the URI is not coming with an authority already, we
// add our own
authority: uri.authority || this.FALLBACK_AUTHORITY,
query: null,
fragment: null
});
}
/**
* Returns the `file` URI to use in contexts where node.js
* is responsible for loading.

View File

@@ -187,18 +187,3 @@ export function mapPager<T, R>(pager: IPager<T>, fn: (t: T) => R): IPager<R> {
getPage: (pageIndex, token) => pager.getPage(pageIndex, token).then(r => r.map(fn))
};
}
/**
* Merges two pagers.
*/
export function mergePagers<T>(one: IPager<T>, other: IPager<T>): IPager<T> {
return {
firstPage: [...one.firstPage, ...other.firstPage],
total: one.total + other.total,
pageSize: one.pageSize + other.pageSize,
getPage(pageIndex: number, token): Promise<T[]> {
return Promise.all([one.getPage(pageIndex, token), other.getPage(pageIndex, token)])
.then(([onePage, otherPage]) => [...onePage, ...otherPage]);
}
};
}

View File

@@ -43,7 +43,7 @@ const CHAR_QUESTION_MARK = 63; /* ? */
class ErrorInvalidArgType extends Error {
code: 'ERR_INVALID_ARG_TYPE';
constructor(name: string, expected: string, actual: any) {
constructor(name: string, expected: string, actual: unknown) {
// determiner: 'must be' or 'must not be'
let determiner;
if (typeof expected === 'string' && expected.indexOf('not ') === 0) {
@@ -215,7 +215,7 @@ export const win32: IPath = {
// absolute path, get cwd for that drive, or the process cwd if
// the drive cwd is not available. We're sure the device is not
// a UNC path at this points, because UNC paths are always absolute.
path = (process.env as any)[`=${resolvedDevice}`] || process.cwd();
path = process.env[`=${resolvedDevice}`] || process.cwd();
// Verify that a cwd was found and that it actually points
// to our drive. If not, default to the drive's root.

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface PerformanceEntry {
export interface PerformanceMark {
readonly name: string;
readonly startTime: number;
}
@@ -11,12 +11,6 @@ export interface PerformanceEntry {
export function mark(name: string): void;
/**
* All entries filtered by type and sorted by `startTime`.
* Returns all marks, sorted by `startTime`.
*/
export function getEntries(): PerformanceEntry[];
export function getDuration(from: string, to: string): number;
type ExportData = any[];
export function importEntries(data: ExportData): void;
export function exportEntries(): ExportData;
export function getMarks(): PerformanceMark[];

View File

@@ -7,66 +7,88 @@
//@ts-check
function _factory(sharedObj) {
/**
* @returns {{mark(name:string):void, getMarks():{name:string, startTime:number}[]}}
*/
function _definePolyfillMarks(timeOrigin) {
sharedObj.MonacoPerformanceMarks = sharedObj.MonacoPerformanceMarks || [];
const _dataLen = 2;
const _nativeMark = typeof performance === 'object' && typeof performance.mark === 'function' ? performance.mark.bind(performance) : () => { };
function importEntries(entries) {
sharedObj.MonacoPerformanceMarks.splice(0, 0, ...entries);
const _data = [];
if (typeof timeOrigin === 'number') {
_data.push('code/timeOrigin', timeOrigin);
}
function exportEntries() {
return sharedObj.MonacoPerformanceMarks.slice(0);
function mark(name) {
_data.push(name, Date.now());
}
function getEntries() {
function getMarks() {
const result = [];
const entries = sharedObj.MonacoPerformanceMarks;
for (let i = 0; i < entries.length; i += _dataLen) {
for (let i = 0; i < _data.length; i += 2) {
result.push({
name: entries[i],
startTime: entries[i + 1],
name: _data[i],
startTime: _data[i + 1],
});
}
return result;
}
return { mark, getMarks };
}
function getDuration(from, to) {
const entries = sharedObj.MonacoPerformanceMarks;
let target = to;
let endIndex = 0;
for (let i = entries.length - _dataLen; i >= 0; i -= _dataLen) {
if (entries[i] === target) {
if (target === to) {
// found `to` (end of interval)
endIndex = i;
target = from;
} else {
// found `from` (start of interval)
return entries[endIndex + 1] - entries[i + 1];
/**
* @returns {{mark(name:string):void, getMarks():{name:string, startTime:number}[]}}
*/
function _define() {
if (typeof performance === 'object' && typeof performance.mark === 'function') {
// in a browser context, reuse performance-util
if (typeof performance.timeOrigin !== 'number' && !performance.timing) {
// safari & webworker: because there is no timeOrigin and no workaround
// we use the `Date.now`-based polyfill.
return _definePolyfillMarks();
} else {
// use "native" performance for mark and getMarks
return {
mark(name) {
performance.mark(name);
},
getMarks() {
let timeOrigin = performance.timeOrigin;
if (typeof timeOrigin !== 'number') {
// safari: there is no timerOrigin but in renderers there is the timing-property
// see https://bugs.webkit.org/show_bug.cgi?id=174862
timeOrigin = performance.timing.navigationStart || performance.timing.redirectStart || performance.timing.fetchStart;
}
const result = [{ name: 'code/timeOrigin', startTime: Math.round(timeOrigin) }];
for (const entry of performance.getEntriesByType('mark')) {
result.push({
name: entry.name,
startTime: Math.round(timeOrigin + entry.startTime)
});
}
return result;
}
}
};
}
return 0;
} else if (typeof process === 'object') {
// node.js: use the normal polyfill but add the timeOrigin
// from the node perf_hooks API as very first mark
const timeOrigin = Math.round((require.nodeRequire || require)('perf_hooks').performance.timeOrigin);
return _definePolyfillMarks(timeOrigin);
} else {
// unknown environment
console.trace('perf-util loaded in UNKNOWN environment');
return _definePolyfillMarks();
}
}
function mark(name) {
sharedObj.MonacoPerformanceMarks.push(name, Date.now());
_nativeMark(name);
function _factory(sharedObj) {
if (!sharedObj.MonacoPerformanceMarks) {
sharedObj.MonacoPerformanceMarks = _define();
}
const exports = {
mark: mark,
getEntries: getEntries,
getDuration: getDuration,
importEntries: importEntries,
exportEntries: exportEntries
};
return exports;
return sharedObj.MonacoPerformanceMarks;
}
// This module can be loaded in an amd and commonjs-context.
@@ -92,5 +114,6 @@ if (typeof define === 'function') {
// commonjs
module.exports = _factory(sharedObj);
} else {
console.trace('perf-util defined in UNKNOWN context (neither requirejs or commonjs)');
sharedObj.perf = _factory(sharedObj);
}

View File

@@ -8,6 +8,7 @@ const LANGUAGE_DEFAULT = 'en';
let _isWindows = false;
let _isMacintosh = false;
let _isLinux = false;
let _isLinuxSnap = false;
let _isNative = false;
let _isWeb = false;
let _isIOS = false;
@@ -61,6 +62,26 @@ if (typeof process !== 'undefined') {
const isElectronRenderer = typeof nodeProcess?.versions?.electron === 'string' && nodeProcess.type === 'renderer';
export const isElectronSandboxed = isElectronRenderer && nodeProcess?.sandboxed;
export const browserCodeLoadingCacheStrategy: 'none' | 'code' | 'bypassHeatCheck' | 'bypassHeatCheckAndEagerCompile' | undefined = (() => {
// Always enabled when sandbox is enabled
if (isElectronSandboxed) {
return 'bypassHeatCheck';
}
// Otherwise, only enabled conditionally
const env = nodeProcess?.env['ENABLE_VSCODE_BROWSER_CODE_LOADING'];
if (typeof env === 'string') {
if (env === 'none' || env === 'code' || env === 'bypassHeatCheck' || env === 'bypassHeatCheckAndEagerCompile') {
return env;
}
return 'bypassHeatCheck';
}
return undefined;
})();
export const isPreferringBrowserCodeLoad = typeof browserCodeLoadingCacheStrategy === 'string';
// Web environment
if (typeof navigator === 'object' && !isElectronRenderer) {
@@ -79,6 +100,7 @@ else if (typeof nodeProcess === 'object') {
_isWindows = (nodeProcess.platform === 'win32');
_isMacintosh = (nodeProcess.platform === 'darwin');
_isLinux = (nodeProcess.platform === 'linux');
_isLinuxSnap = _isLinux && !!nodeProcess.env['SNAP'] && !!nodeProcess.env['SNAP_REVISION'];
_locale = LANGUAGE_DEFAULT;
_language = LANGUAGE_DEFAULT;
const rawNlsConfig = nodeProcess.env['VSCODE_NLS_CONFIG'];
@@ -128,6 +150,7 @@ if (_isMacintosh) {
export const isWindows = _isWindows;
export const isMacintosh = _isMacintosh;
export const isLinux = _isLinux;
export const isLinuxSnap = _isLinuxSnap;
export const isNative = _isNative;
export const isWeb = _isWeb;
export const isIOS = _isIOS;
@@ -212,7 +235,7 @@ export const setImmediate: ISetImmediate = (function defineSetImmediate() {
globals.postMessage({ vscodeSetImmediateId: myId }, '*');
};
}
if (nodeProcess) {
if (nodeProcess && typeof nodeProcess.nextTick === 'function') {
return nodeProcess.nextTick.bind(nodeProcess);
}
const _promise = Promise.resolve();

View File

@@ -108,7 +108,6 @@ export function sanitizeProcessEnvironment(env: IProcessEnvironment, ...preserve
}, {} as Record<string, boolean>);
const keysToRemove = [
/^ELECTRON_.+$/,
/^GOOGLE_API_KEY$/,
/^VSCODE_.+$/,
/^SNAP(|_.*)$/,
/^GDK_PIXBUF_.+$/,

View File

@@ -42,8 +42,8 @@ export interface IExtUri {
/**
* Tests whether a `candidate` URI is a parent or equal of a given `base` URI.
*
* @param base A uri which is "longer"
* @param parentCandidate A uri which is "shorter" then `base`
* @param base A uri which is "longer" or at least same length as `parentCandidate`
* @param parentCandidate A uri which is "shorter" or up to same length as `base`
* @param ignoreFragment Ignore the fragment (defaults to `false`)
*/
isEqualOrParent(base: URI, parentCandidate: URI, ignoreFragment?: boolean): boolean;

View File

@@ -13,6 +13,8 @@ export const enum ScrollbarVisibility {
}
export interface ScrollEvent {
inSmoothScrolling: boolean;
oldWidth: number;
oldScrollWidth: number;
oldScrollLeft: number;
@@ -132,7 +134,7 @@ export class ScrollState implements IScrollDimensions, IScrollPosition {
);
}
public createScrollEvent(previous: ScrollState): ScrollEvent {
public createScrollEvent(previous: ScrollState, inSmoothScrolling: boolean): ScrollEvent {
const widthChanged = (this.width !== previous.width);
const scrollWidthChanged = (this.scrollWidth !== previous.scrollWidth);
const scrollLeftChanged = (this.scrollLeft !== previous.scrollLeft);
@@ -142,6 +144,7 @@ export class ScrollState implements IScrollDimensions, IScrollPosition {
const scrollTopChanged = (this.scrollTop !== previous.scrollTop);
return {
inSmoothScrolling: inSmoothScrolling,
oldWidth: previous.width,
oldScrollWidth: previous.scrollWidth,
oldScrollLeft: previous.scrollLeft,
@@ -242,7 +245,7 @@ export class Scrollable extends Disposable {
public setScrollDimensions(dimensions: INewScrollDimensions, useRawScrollPositions: boolean): void {
const newState = this._state.withScrollDimensions(dimensions, useRawScrollPositions);
this._setState(newState);
this._setState(newState, Boolean(this._smoothScrolling));
// Validate outstanding animated scroll position target
if (this._smoothScrolling) {
@@ -279,10 +282,10 @@ export class Scrollable extends Disposable {
this._smoothScrolling = null;
}
this._setState(newState);
this._setState(newState, false);
}
public setScrollPositionSmooth(update: INewScrollPosition): void {
public setScrollPositionSmooth(update: INewScrollPosition, reuseAnimation?: boolean): void {
if (this._smoothScrollDuration === 0) {
// Smooth scrolling not supported.
return this.setScrollPositionNow(update);
@@ -302,8 +305,12 @@ export class Scrollable extends Disposable {
// No need to interrupt or extend the current animation since we're going to the same place
return;
}
const newSmoothScrolling = this._smoothScrolling.combine(this._state, validTarget, this._smoothScrollDuration);
let newSmoothScrolling: SmoothScrollingOperation;
if (reuseAnimation) {
newSmoothScrolling = new SmoothScrollingOperation(this._smoothScrolling.from, validTarget, this._smoothScrolling.startTime, this._smoothScrolling.duration);
} else {
newSmoothScrolling = this._smoothScrolling.combine(this._state, validTarget, this._smoothScrollDuration);
}
this._smoothScrolling.dispose();
this._smoothScrolling = newSmoothScrolling;
} else {
@@ -330,7 +337,7 @@ export class Scrollable extends Disposable {
const update = this._smoothScrolling.tick();
const newState = this._state.withScrollPosition(update);
this._setState(newState);
this._setState(newState, true);
if (!this._smoothScrolling) {
// Looks like someone canceled the smooth scrolling
@@ -354,14 +361,14 @@ export class Scrollable extends Disposable {
});
}
private _setState(newState: ScrollState): void {
private _setState(newState: ScrollState, inSmoothScrolling: boolean): void {
const oldState = this._state;
if (oldState.equals(newState)) {
// no change
return;
}
this._state = newState;
this._onScroll.fire(this._state.createScrollEvent(oldState));
this._onScroll.fire(this._state.createScrollEvent(oldState, inSmoothScrolling));
}
}
@@ -404,17 +411,17 @@ export class SmoothScrollingOperation {
public readonly from: ISmoothScrollPosition;
public to: ISmoothScrollPosition;
public readonly duration: number;
private readonly _startTime: number;
public readonly startTime: number;
public animationFrameDisposable: IDisposable | null;
private scrollLeft!: IAnimation;
private scrollTop!: IAnimation;
protected constructor(from: ISmoothScrollPosition, to: ISmoothScrollPosition, startTime: number, duration: number) {
constructor(from: ISmoothScrollPosition, to: ISmoothScrollPosition, startTime: number, duration: number) {
this.from = from;
this.to = to;
this.duration = duration;
this._startTime = startTime;
this.startTime = startTime;
this.animationFrameDisposable = null;
@@ -460,7 +467,7 @@ export class SmoothScrollingOperation {
}
protected _tick(now: number): SmoothScrollingUpdate {
const completion = (now - this._startTime) / this.duration;
const completion = (now - this.startTime) / this.duration;
if (completion < 1) {
const newScrollLeft = this.scrollLeft(completion);

View File

@@ -35,6 +35,6 @@ export class StopWatch {
}
private _now(): number {
return this._highResolution ? globals.performance.now() : new Date().getTime();
return this._highResolution ? globals.performance.now() : Date.now();
}
}

View File

@@ -3,6 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { onUnexpectedError } from 'vs/base/common/errors';
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
/**
@@ -229,7 +230,7 @@ class WriteableStreamImpl<T> implements WriteableStream<T> {
// flowing: directly send the data to listeners
if (this.state.flowing) {
this.listeners.data.forEach(listener => listener(data));
this.emitData(data);
}
// not yet flowing: buffer data until flowing
@@ -250,7 +251,7 @@ class WriteableStreamImpl<T> implements WriteableStream<T> {
// flowing: directly send the error to listeners
if (this.state.flowing) {
this.listeners.error.forEach(listener => listener(error));
this.emitError(error);
}
// not yet flowing: buffer errors until flowing
@@ -273,7 +274,7 @@ class WriteableStreamImpl<T> implements WriteableStream<T> {
// flowing: send end event to listeners
if (this.state.flowing) {
this.listeners.end.forEach(listener => listener());
this.emitEnd();
this.destroy();
}
@@ -284,6 +285,22 @@ class WriteableStreamImpl<T> implements WriteableStream<T> {
}
}
private emitData(data: T): void {
this.listeners.data.slice(0).forEach(listener => listener(data)); // slice to avoid listener mutation from delivering event
}
private emitError(error: Error): void {
if (this.listeners.error.length === 0) {
onUnexpectedError(error); // nobody listened to this error so we log it as unexpected
} else {
this.listeners.error.slice(0).forEach(listener => listener(error)); // slice to avoid listener mutation from delivering event
}
}
private emitEnd(): void {
this.listeners.end.slice(0).forEach(listener => listener()); // slice to avoid listener mutation from delivering event
}
on(event: 'data', callback: (data: T) => void): void;
on(event: 'error', callback: (err: Error) => void): void;
on(event: 'end', callback: () => void): void;
@@ -361,7 +378,7 @@ class WriteableStreamImpl<T> implements WriteableStream<T> {
if (this.buffer.data.length > 0) {
const fullDataBuffer = this.reducer(this.buffer.data);
this.listeners.data.forEach(listener => listener(fullDataBuffer));
this.emitData(fullDataBuffer);
this.buffer.data.length = 0;
@@ -375,7 +392,7 @@ class WriteableStreamImpl<T> implements WriteableStream<T> {
private flowErrors(): void {
if (this.listeners.error.length > 0) {
for (const error of this.buffer.error) {
this.listeners.error.forEach(listener => listener(error));
this.emitError(error);
}
this.buffer.error.length = 0;
@@ -384,7 +401,7 @@ class WriteableStreamImpl<T> implements WriteableStream<T> {
private flowEnd(): boolean {
if (this.state.ended) {
this.listeners.end.forEach(listener => listener());
this.emitEnd();
return this.listeners.end.length > 0;
}
@@ -574,3 +591,40 @@ export function transform<Original, Transformed>(stream: ReadableStreamEvents<Or
return target;
}
export interface IReadableStreamObservable {
/**
* A promise to await the `end` or `error` event
* of a stream.
*/
errorOrEnd: () => Promise<void>;
}
/**
* Helper to observe a stream for certain events through
* a promise based API.
*/
export function observe(stream: ReadableStream<unknown>): IReadableStreamObservable {
// A stream is closed when it ended or errord
// We install this listener right from the
// beginning to catch the events early.
const errorOrEnd = Promise.race([
new Promise<void>(resolve => stream.on('end', () => resolve())),
new Promise<void>(resolve => stream.on('error', () => resolve()))
]);
return {
errorOrEnd(): Promise<void> {
// We need to ensure the stream is flowing so that our
// listeners are getting triggered. It is possible that
// the stream is not flowing because no `data` listener
// was attached yet.
stream.resume();
return errorOrEnd;
}
};
}

File diff suppressed because one or more lines are too long

View File

@@ -108,7 +108,7 @@ export class URI implements UriComponents {
&& typeof (<URI>thing).path === 'string'
&& typeof (<URI>thing).query === 'string'
&& typeof (<URI>thing).scheme === 'string'
&& typeof (<URI>thing).fsPath === 'function'
&& typeof (<URI>thing).fsPath === 'string'
&& typeof (<URI>thing).with === 'function'
&& typeof (<URI>thing).toString === 'function';
}