mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
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:
@@ -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: () => { }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"name": "vs/base",
|
||||
"dependencies": [
|
||||
{
|
||||
"name": "vs",
|
||||
"internal": false
|
||||
}
|
||||
],
|
||||
"libs": [
|
||||
"lib.core.d.ts"
|
||||
],
|
||||
"sources": [
|
||||
"**/*.ts"
|
||||
],
|
||||
"declares": []
|
||||
}
|
||||
@@ -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.'));
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => ' '.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;
|
||||
|
||||
159
src/vs/base/common/iconLabels.ts
Normal file
159
src/vs/base/common/iconLabels.ts
Normal 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;
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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)[];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
12
src/vs/base/common/performance.d.ts
vendored
12
src/vs/base/common/performance.d.ts
vendored
@@ -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[];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -108,7 +108,6 @@ export function sanitizeProcessEnvironment(env: IProcessEnvironment, ...preserve
|
||||
}, {} as Record<string, boolean>);
|
||||
const keysToRemove = [
|
||||
/^ELECTRON_.+$/,
|
||||
/^GOOGLE_API_KEY$/,
|
||||
/^VSCODE_.+$/,
|
||||
/^SNAP(|_.*)$/,
|
||||
/^GDK_PIXBUF_.+$/,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user