Delete unused files
This commit is contained in:
@@ -1,70 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mainWindow } from 'vs/base/browser/window';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export class BroadcastDataChannel<T> extends Disposable {
|
||||
|
||||
private broadcastChannel: BroadcastChannel | undefined;
|
||||
|
||||
private readonly _onDidReceiveData = this._register(new Emitter<T>());
|
||||
readonly onDidReceiveData = this._onDidReceiveData.event;
|
||||
|
||||
constructor(private readonly channelName: string) {
|
||||
super();
|
||||
|
||||
// Use BroadcastChannel
|
||||
if ('BroadcastChannel' in mainWindow) {
|
||||
try {
|
||||
this.broadcastChannel = new BroadcastChannel(channelName);
|
||||
const listener = (event: MessageEvent) => {
|
||||
this._onDidReceiveData.fire(event.data);
|
||||
};
|
||||
this.broadcastChannel.addEventListener('message', listener);
|
||||
this._register(toDisposable(() => {
|
||||
if (this.broadcastChannel) {
|
||||
this.broadcastChannel.removeEventListener('message', listener);
|
||||
this.broadcastChannel.close();
|
||||
}
|
||||
}));
|
||||
} catch (error) {
|
||||
console.warn('Error while creating broadcast channel. Falling back to localStorage.', getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastChannel is not supported. Use storage.
|
||||
if (!this.broadcastChannel) {
|
||||
this.channelName = `BroadcastDataChannel.${channelName}`;
|
||||
this.createBroadcastChannel();
|
||||
}
|
||||
}
|
||||
|
||||
private createBroadcastChannel(): void {
|
||||
const listener = (event: StorageEvent) => {
|
||||
if (event.key === this.channelName && event.newValue) {
|
||||
this._onDidReceiveData.fire(JSON.parse(event.newValue));
|
||||
}
|
||||
};
|
||||
mainWindow.addEventListener('storage', listener);
|
||||
this._register(toDisposable(() => mainWindow.removeEventListener('storage', listener)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the data to other BroadcastChannel objects set up for this channel. Data can be structured objects, e.g. nested objects and arrays.
|
||||
* @param data data to broadcast
|
||||
*/
|
||||
postData(data: T): void {
|
||||
if (this.broadcastChannel) {
|
||||
this.broadcastChannel.postMessage(data);
|
||||
} else {
|
||||
// remove previous changes so that event is triggered even if new changes are same as old changes
|
||||
localStorage.removeItem(this.channelName);
|
||||
localStorage.setItem(this.channelName, JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createTrustedTypesPolicy } from 'vs/base/browser/trustedTypes';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { COI } from 'vs/base/common/network';
|
||||
import { IWorker, IWorkerCallback, IWorkerFactory, logOnceWebWorkerWarning } from 'vs/base/common/worker/simpleWorker';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
const ttPolicy = createTrustedTypesPolicy('defaultWorkerFactory', { createScriptURL: value => value });
|
||||
|
||||
export function createBlobWorker(blobUrl: string, options?: WorkerOptions): Worker {
|
||||
if (!blobUrl.startsWith('blob:')) {
|
||||
throw new URIError('Not a blob-url: ' + blobUrl);
|
||||
}
|
||||
return new Worker(ttPolicy ? ttPolicy.createScriptURL(blobUrl) as unknown as string : blobUrl, options);
|
||||
}
|
||||
|
||||
function getWorker(label: string): Worker | Promise<Worker> {
|
||||
// Option for hosts to overwrite the worker script (used in the standalone editor)
|
||||
interface IMonacoEnvironment {
|
||||
getWorker?(moduleId: string, label: string): Worker | Promise<Worker>;
|
||||
getWorkerUrl?(moduleId: string, label: string): string;
|
||||
}
|
||||
const monacoEnvironment: IMonacoEnvironment | undefined = (globalThis as any).MonacoEnvironment;
|
||||
if (monacoEnvironment) {
|
||||
if (typeof monacoEnvironment.getWorker === 'function') {
|
||||
return monacoEnvironment.getWorker('workerMain.js', label);
|
||||
}
|
||||
if (typeof monacoEnvironment.getWorkerUrl === 'function') {
|
||||
const workerUrl = monacoEnvironment.getWorkerUrl('workerMain.js', label);
|
||||
return new Worker(ttPolicy ? ttPolicy.createScriptURL(workerUrl) as unknown as string : workerUrl, { name: label });
|
||||
}
|
||||
}
|
||||
// ESM-comment-begin
|
||||
if (typeof require === 'function') {
|
||||
// check if the JS lives on a different origin
|
||||
const workerMain = require.toUrl('vs/base/worker/workerMain.js'); // explicitly using require.toUrl(), see https://github.com/microsoft/vscode/issues/107440#issuecomment-698982321
|
||||
const workerUrl = getWorkerBootstrapUrl(workerMain, label);
|
||||
return new Worker(ttPolicy ? ttPolicy.createScriptURL(workerUrl) as unknown as string : workerUrl, { name: label });
|
||||
}
|
||||
// ESM-comment-end
|
||||
throw new Error(`You must define a function MonacoEnvironment.getWorkerUrl or MonacoEnvironment.getWorker`);
|
||||
}
|
||||
|
||||
// ESM-comment-begin
|
||||
export function getWorkerBootstrapUrl(scriptPath: string, label: string): string {
|
||||
if (/^((http:)|(https:)|(file:))/.test(scriptPath) && scriptPath.substring(0, globalThis.origin.length) !== globalThis.origin) {
|
||||
// this is the cross-origin case
|
||||
// i.e. the webpage is running at a different origin than where the scripts are loaded from
|
||||
} else {
|
||||
const start = scriptPath.lastIndexOf('?');
|
||||
const end = scriptPath.lastIndexOf('#', start);
|
||||
const params = start > 0
|
||||
? new URLSearchParams(scriptPath.substring(start + 1, ~end ? end : undefined))
|
||||
: new URLSearchParams();
|
||||
|
||||
COI.addSearchParam(params, true, true);
|
||||
const search = params.toString();
|
||||
if (!search) {
|
||||
scriptPath = `${scriptPath}#${label}`;
|
||||
} else {
|
||||
scriptPath = `${scriptPath}?${params.toString()}#${label}`;
|
||||
}
|
||||
}
|
||||
|
||||
const factoryModuleId = 'vs/base/worker/defaultWorkerFactory.js';
|
||||
const workerBaseUrl = require.toUrl(factoryModuleId).slice(0, -factoryModuleId.length); // explicitly using require.toUrl(), see https://github.com/microsoft/vscode/issues/107440#issuecomment-698982321
|
||||
const blob = new Blob([[
|
||||
`/*${label}*/`,
|
||||
`globalThis.MonacoEnvironment = { baseUrl: '${workerBaseUrl}' };`,
|
||||
// VSCODE_GLOBALS: NLS
|
||||
`globalThis._VSCODE_NLS_MESSAGES = ${JSON.stringify(globalThis._VSCODE_NLS_MESSAGES)};`,
|
||||
`globalThis._VSCODE_NLS_LANGUAGE = ${JSON.stringify(globalThis._VSCODE_NLS_LANGUAGE)};`,
|
||||
`const ttPolicy = globalThis.trustedTypes?.createPolicy('defaultWorkerFactory', { createScriptURL: value => value });`,
|
||||
`importScripts(ttPolicy?.createScriptURL('${scriptPath}') ?? '${scriptPath}');`,
|
||||
`/*${label}*/`
|
||||
].join('')], { type: 'application/javascript' });
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
// ESM-comment-end
|
||||
|
||||
function isPromiseLike<T>(obj: any): obj is PromiseLike<T> {
|
||||
if (typeof obj.then === 'function') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* A worker that uses HTML5 web workers so that is has
|
||||
* its own global scope and its own thread.
|
||||
*/
|
||||
class WebWorker extends Disposable implements IWorker {
|
||||
|
||||
private readonly id: number;
|
||||
private readonly label: string;
|
||||
private worker: Promise<Worker> | null;
|
||||
|
||||
constructor(moduleId: string, id: number, label: string, onMessageCallback: IWorkerCallback, onErrorCallback: (err: any) => void) {
|
||||
super();
|
||||
this.id = id;
|
||||
this.label = label;
|
||||
const workerOrPromise = getWorker(label);
|
||||
if (isPromiseLike(workerOrPromise)) {
|
||||
this.worker = workerOrPromise;
|
||||
} else {
|
||||
this.worker = Promise.resolve(workerOrPromise);
|
||||
}
|
||||
this.postMessage(moduleId, []);
|
||||
this.worker.then((w) => {
|
||||
w.onmessage = function (ev) {
|
||||
onMessageCallback(ev.data);
|
||||
};
|
||||
w.onmessageerror = onErrorCallback;
|
||||
if (typeof w.addEventListener === 'function') {
|
||||
w.addEventListener('error', onErrorCallback);
|
||||
}
|
||||
});
|
||||
this._register(toDisposable(() => {
|
||||
this.worker?.then(w => {
|
||||
w.onmessage = null;
|
||||
w.onmessageerror = null;
|
||||
w.removeEventListener('error', onErrorCallback);
|
||||
w.terminate();
|
||||
});
|
||||
this.worker = null;
|
||||
}));
|
||||
}
|
||||
|
||||
public getId(): number {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public postMessage(message: any, transfer: Transferable[]): void {
|
||||
this.worker?.then(w => {
|
||||
try {
|
||||
w.postMessage(message, transfer);
|
||||
} catch (err) {
|
||||
onUnexpectedError(err);
|
||||
onUnexpectedError(new Error(`FAILED to post message to '${this.label}'-worker`, { cause: err }));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class DefaultWorkerFactory implements IWorkerFactory {
|
||||
|
||||
private static LAST_WORKER_ID = 0;
|
||||
|
||||
private _label: string | undefined;
|
||||
private _webWorkerFailedBeforeError: any;
|
||||
|
||||
constructor(label: string | undefined) {
|
||||
this._label = label;
|
||||
this._webWorkerFailedBeforeError = false;
|
||||
}
|
||||
|
||||
public create(moduleId: string, onMessageCallback: IWorkerCallback, onErrorCallback: (err: any) => void): IWorker {
|
||||
const workerId = (++DefaultWorkerFactory.LAST_WORKER_ID);
|
||||
|
||||
if (this._webWorkerFailedBeforeError) {
|
||||
throw this._webWorkerFailedBeforeError;
|
||||
}
|
||||
|
||||
return new WebWorker(moduleId, workerId, this._label || 'anonymous' + workerId, onMessageCallback, (err) => {
|
||||
logOnceWebWorkerWarning(err);
|
||||
this._webWorkerFailedBeforeError = err;
|
||||
onErrorCallback(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// https://wicg.github.io/webusb/
|
||||
|
||||
export interface UsbDeviceData {
|
||||
readonly deviceClass: number;
|
||||
readonly deviceProtocol: number;
|
||||
readonly deviceSubclass: number;
|
||||
readonly deviceVersionMajor: number;
|
||||
readonly deviceVersionMinor: number;
|
||||
readonly deviceVersionSubminor: number;
|
||||
readonly manufacturerName?: string;
|
||||
readonly productId: number;
|
||||
readonly productName?: string;
|
||||
readonly serialNumber?: string;
|
||||
readonly usbVersionMajor: number;
|
||||
readonly usbVersionMinor: number;
|
||||
readonly usbVersionSubminor: number;
|
||||
readonly vendorId: number;
|
||||
}
|
||||
|
||||
export async function requestUsbDevice(options?: { filters?: unknown[] }): Promise<UsbDeviceData | undefined> {
|
||||
const usb = (navigator as any).usb;
|
||||
if (!usb) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const device = await usb.requestDevice({ filters: options?.filters ?? [] });
|
||||
if (!device) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
deviceClass: device.deviceClass,
|
||||
deviceProtocol: device.deviceProtocol,
|
||||
deviceSubclass: device.deviceSubclass,
|
||||
deviceVersionMajor: device.deviceVersionMajor,
|
||||
deviceVersionMinor: device.deviceVersionMinor,
|
||||
deviceVersionSubminor: device.deviceVersionSubminor,
|
||||
manufacturerName: device.manufacturerName,
|
||||
productId: device.productId,
|
||||
productName: device.productName,
|
||||
serialNumber: device.serialNumber,
|
||||
usbVersionMajor: device.usbVersionMajor,
|
||||
usbVersionMinor: device.usbVersionMinor,
|
||||
usbVersionSubminor: device.usbVersionSubminor,
|
||||
vendorId: device.vendorId,
|
||||
};
|
||||
}
|
||||
|
||||
// https://wicg.github.io/serial/
|
||||
|
||||
export interface SerialPortData {
|
||||
readonly usbVendorId?: number | undefined;
|
||||
readonly usbProductId?: number | undefined;
|
||||
}
|
||||
|
||||
export async function requestSerialPort(options?: { filters?: unknown[] }): Promise<SerialPortData | undefined> {
|
||||
const serial = (navigator as any).serial;
|
||||
if (!serial) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const port = await serial.requestPort({ filters: options?.filters ?? [] });
|
||||
if (!port) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const info = port.getInfo();
|
||||
return {
|
||||
usbVendorId: info.usbVendorId,
|
||||
usbProductId: info.usbProductId
|
||||
};
|
||||
}
|
||||
|
||||
// https://wicg.github.io/webhid/
|
||||
|
||||
export interface HidDeviceData {
|
||||
readonly opened: boolean;
|
||||
readonly vendorId: number;
|
||||
readonly productId: number;
|
||||
readonly productName: string;
|
||||
readonly collections: [];
|
||||
}
|
||||
|
||||
export async function requestHidDevice(options?: { filters?: unknown[] }): Promise<HidDeviceData | undefined> {
|
||||
const hid = (navigator as any).hid;
|
||||
if (!hid) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const devices = await hid.requestDevice({ filters: options?.filters ?? [] });
|
||||
if (!devices.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const device = devices[0];
|
||||
return {
|
||||
opened: device.opened,
|
||||
vendorId: device.vendorId,
|
||||
productId: device.productId,
|
||||
productName: device.productName,
|
||||
collections: device.collections
|
||||
};
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { addDisposableListener, getWindow } from 'vs/base/browser/dom';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Mimes } from 'vs/base/common/mime';
|
||||
|
||||
/**
|
||||
* A helper that will execute a provided function when the provided HTMLElement receives
|
||||
* dragover event for 800ms. If the drag is aborted before, the callback will not be triggered.
|
||||
*/
|
||||
export class DelayedDragHandler extends Disposable {
|
||||
private timeout: any;
|
||||
|
||||
constructor(container: HTMLElement, callback: () => void) {
|
||||
super();
|
||||
|
||||
this._register(addDisposableListener(container, 'dragover', e => {
|
||||
e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)
|
||||
|
||||
if (!this.timeout) {
|
||||
this.timeout = setTimeout(() => {
|
||||
callback();
|
||||
|
||||
this.timeout = null;
|
||||
}, 800);
|
||||
}
|
||||
}));
|
||||
|
||||
['dragleave', 'drop', 'dragend'].forEach(type => {
|
||||
this._register(addDisposableListener(container, type, () => {
|
||||
this.clearDragTimeout();
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private clearDragTimeout(): void {
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this.clearDragTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
// Common data transfers
|
||||
export const DataTransfers = {
|
||||
|
||||
/**
|
||||
* Application specific resource transfer type
|
||||
*/
|
||||
RESOURCES: 'ResourceURLs',
|
||||
|
||||
/**
|
||||
* Browser specific transfer type to download
|
||||
*/
|
||||
DOWNLOAD_URL: 'DownloadURL',
|
||||
|
||||
/**
|
||||
* Browser specific transfer type for files
|
||||
*/
|
||||
FILES: 'Files',
|
||||
|
||||
/**
|
||||
* Typically transfer type for copy/paste transfers.
|
||||
*/
|
||||
TEXT: Mimes.text,
|
||||
|
||||
/**
|
||||
* Internal type used to pass around text/uri-list data.
|
||||
*
|
||||
* This is needed to work around https://bugs.chromium.org/p/chromium/issues/detail?id=239745.
|
||||
*/
|
||||
INTERNAL_URI_LIST: 'application/vnd.code.uri-list',
|
||||
};
|
||||
|
||||
export function applyDragImage(event: DragEvent, label: string | null, clazz: string, backgroundColor?: string | null, foregroundColor?: string | null): void {
|
||||
const dragImage = document.createElement('div');
|
||||
dragImage.className = clazz;
|
||||
dragImage.textContent = label;
|
||||
|
||||
if (foregroundColor) {
|
||||
dragImage.style.color = foregroundColor;
|
||||
}
|
||||
|
||||
if (backgroundColor) {
|
||||
dragImage.style.background = backgroundColor;
|
||||
}
|
||||
|
||||
if (event.dataTransfer) {
|
||||
const ownerDocument = getWindow(event).document;
|
||||
ownerDocument.body.appendChild(dragImage);
|
||||
event.dataTransfer.setDragImage(dragImage, -10, -10);
|
||||
|
||||
// Removes the element when the DND operation is done
|
||||
setTimeout(() => dragImage.remove(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IDragAndDropData {
|
||||
update(dataTransfer: DataTransfer): void;
|
||||
getData(): unknown;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createStyleSheet2 } from 'vs/base/browser/dom';
|
||||
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { autorun, IObservable } from 'vs/base/common/observable';
|
||||
|
||||
export function createStyleSheetFromObservable(css: IObservable<string>): IDisposable {
|
||||
const store = new DisposableStore();
|
||||
const w = store.add(createStyleSheet2());
|
||||
store.add(autorun(reader => {
|
||||
w.setStyle(css.read(reader));
|
||||
}));
|
||||
return store;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { GestureEvent } from 'vs/base/browser/touch';
|
||||
import { Emitter, Event as BaseEvent } from 'vs/base/common/event';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export type EventHandler = HTMLElement | HTMLDocument | Window;
|
||||
|
||||
export interface IDomEvent {
|
||||
<K extends keyof HTMLElementEventMap>(element: EventHandler, type: K, useCapture?: boolean): BaseEvent<HTMLElementEventMap[K]>;
|
||||
(element: EventHandler, type: string, useCapture?: boolean): BaseEvent<unknown>;
|
||||
}
|
||||
|
||||
export interface DOMEventMap extends HTMLElementEventMap, DocumentEventMap, WindowEventMap {
|
||||
'-monaco-gesturetap': GestureEvent;
|
||||
'-monaco-gesturechange': GestureEvent;
|
||||
'-monaco-gesturestart': GestureEvent;
|
||||
'-monaco-gesturesend': GestureEvent;
|
||||
'-monaco-gesturecontextmenu': GestureEvent;
|
||||
'compositionstart': CompositionEvent;
|
||||
'compositionupdate': CompositionEvent;
|
||||
'compositionend': CompositionEvent;
|
||||
}
|
||||
|
||||
export class DomEmitter<K extends keyof DOMEventMap> implements IDisposable {
|
||||
|
||||
private emitter: Emitter<DOMEventMap[K]>;
|
||||
|
||||
get event(): BaseEvent<DOMEventMap[K]> {
|
||||
return this.emitter.event;
|
||||
}
|
||||
|
||||
constructor(element: Window & typeof globalThis, type: WindowEventMap, useCapture?: boolean);
|
||||
constructor(element: Document, type: DocumentEventMap, useCapture?: boolean);
|
||||
constructor(element: EventHandler, type: K, useCapture?: boolean);
|
||||
constructor(element: EventHandler, type: K, useCapture?: boolean) {
|
||||
const fn = (e: Event) => this.emitter.fire(e as DOMEventMap[K]);
|
||||
this.emitter = new Emitter({
|
||||
onWillAddFirstListener: () => element.addEventListener(type, fn, useCapture),
|
||||
onDidRemoveLastListener: () => element.removeEventListener(type, fn, useCapture)
|
||||
});
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.emitter.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isMacintosh, isWindows } from 'vs/base/common/platform';
|
||||
|
||||
/**
|
||||
* The best font-family to be used in CSS based on the platform:
|
||||
* - Windows: Segoe preferred, fallback to sans-serif
|
||||
* - macOS: standard system font, fallback to sans-serif
|
||||
* - Linux: standard system font preferred, fallback to Ubuntu fonts
|
||||
*
|
||||
* Note: this currently does not adjust for different locales.
|
||||
*/
|
||||
export const DEFAULT_FONT_FAMILY = isWindows ? '"Segoe WPC", "Segoe UI", sans-serif' : isMacintosh ? '-apple-system, BlinkMacSystemFont, sans-serif' : 'system-ui, "Ubuntu", "Droid Sans", sans-serif';
|
||||
@@ -1,226 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
|
||||
export interface IContentActionHandler {
|
||||
callback: (content: string, event: IMouseEvent | IKeyboardEvent) => void;
|
||||
readonly disposables: DisposableStore;
|
||||
}
|
||||
|
||||
export interface FormattedTextRenderOptions {
|
||||
readonly className?: string;
|
||||
readonly inline?: boolean;
|
||||
readonly actionHandler?: IContentActionHandler;
|
||||
readonly renderCodeSegments?: boolean;
|
||||
}
|
||||
|
||||
export function renderText(text: string, options: FormattedTextRenderOptions = {}): HTMLElement {
|
||||
const element = createElement(options);
|
||||
element.textContent = text;
|
||||
return element;
|
||||
}
|
||||
|
||||
export function renderFormattedText(formattedText: string, options: FormattedTextRenderOptions = {}): HTMLElement {
|
||||
const element = createElement(options);
|
||||
_renderFormattedText(element, parseFormattedText(formattedText, !!options.renderCodeSegments), options.actionHandler, options.renderCodeSegments);
|
||||
return element;
|
||||
}
|
||||
|
||||
export function createElement(options: FormattedTextRenderOptions): HTMLElement {
|
||||
const tagName = options.inline ? 'span' : 'div';
|
||||
const element = document.createElement(tagName);
|
||||
if (options.className) {
|
||||
element.className = options.className;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
class StringStream {
|
||||
private source: string;
|
||||
private index: number;
|
||||
|
||||
constructor(source: string) {
|
||||
this.source = source;
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
public eos(): boolean {
|
||||
return this.index >= this.source.length;
|
||||
}
|
||||
|
||||
public next(): string {
|
||||
const next = this.peek();
|
||||
this.advance();
|
||||
return next;
|
||||
}
|
||||
|
||||
public peek(): string {
|
||||
return this.source[this.index];
|
||||
}
|
||||
|
||||
public advance(): void {
|
||||
this.index++;
|
||||
}
|
||||
}
|
||||
|
||||
const enum FormatType {
|
||||
Invalid,
|
||||
Root,
|
||||
Text,
|
||||
Bold,
|
||||
Italics,
|
||||
Action,
|
||||
ActionClose,
|
||||
Code,
|
||||
NewLine
|
||||
}
|
||||
|
||||
interface IFormatParseTree {
|
||||
type: FormatType;
|
||||
content?: string;
|
||||
index?: number;
|
||||
children?: IFormatParseTree[];
|
||||
}
|
||||
|
||||
function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionHandler?: IContentActionHandler, renderCodeSegments?: boolean) {
|
||||
let child: Node | undefined;
|
||||
|
||||
if (treeNode.type === FormatType.Text) {
|
||||
child = document.createTextNode(treeNode.content || '');
|
||||
} else if (treeNode.type === FormatType.Bold) {
|
||||
child = document.createElement('b');
|
||||
} else if (treeNode.type === FormatType.Italics) {
|
||||
child = document.createElement('i');
|
||||
} else if (treeNode.type === FormatType.Code && renderCodeSegments) {
|
||||
child = document.createElement('code');
|
||||
} else if (treeNode.type === FormatType.Action && actionHandler) {
|
||||
const a = document.createElement('a');
|
||||
actionHandler.disposables.add(DOM.addStandardDisposableListener(a, 'click', (event) => {
|
||||
actionHandler.callback(String(treeNode.index), event);
|
||||
}));
|
||||
|
||||
child = a;
|
||||
} else if (treeNode.type === FormatType.NewLine) {
|
||||
child = document.createElement('br');
|
||||
} else if (treeNode.type === FormatType.Root) {
|
||||
child = element;
|
||||
}
|
||||
|
||||
if (child && element !== child) {
|
||||
element.appendChild(child);
|
||||
}
|
||||
|
||||
if (child && Array.isArray(treeNode.children)) {
|
||||
treeNode.children.forEach((nodeChild) => {
|
||||
_renderFormattedText(child, nodeChild, actionHandler, renderCodeSegments);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function parseFormattedText(content: string, parseCodeSegments: boolean): IFormatParseTree {
|
||||
|
||||
const root: IFormatParseTree = {
|
||||
type: FormatType.Root,
|
||||
children: []
|
||||
};
|
||||
|
||||
let actionViewItemIndex = 0;
|
||||
let current = root;
|
||||
const stack: IFormatParseTree[] = [];
|
||||
const stream = new StringStream(content);
|
||||
|
||||
while (!stream.eos()) {
|
||||
let next = stream.next();
|
||||
|
||||
const isEscapedFormatType = (next === '\\' && formatTagType(stream.peek(), parseCodeSegments) !== FormatType.Invalid);
|
||||
if (isEscapedFormatType) {
|
||||
next = stream.next(); // unread the backslash if it escapes a format tag type
|
||||
}
|
||||
|
||||
if (!isEscapedFormatType && isFormatTag(next, parseCodeSegments) && next === stream.peek()) {
|
||||
stream.advance();
|
||||
|
||||
if (current.type === FormatType.Text) {
|
||||
current = stack.pop()!;
|
||||
}
|
||||
|
||||
const type = formatTagType(next, parseCodeSegments);
|
||||
if (current.type === type || (current.type === FormatType.Action && type === FormatType.ActionClose)) {
|
||||
current = stack.pop()!;
|
||||
} else {
|
||||
const newCurrent: IFormatParseTree = {
|
||||
type: type,
|
||||
children: []
|
||||
};
|
||||
|
||||
if (type === FormatType.Action) {
|
||||
newCurrent.index = actionViewItemIndex;
|
||||
actionViewItemIndex++;
|
||||
}
|
||||
|
||||
current.children!.push(newCurrent);
|
||||
stack.push(current);
|
||||
current = newCurrent;
|
||||
}
|
||||
} else if (next === '\n') {
|
||||
if (current.type === FormatType.Text) {
|
||||
current = stack.pop()!;
|
||||
}
|
||||
|
||||
current.children!.push({
|
||||
type: FormatType.NewLine
|
||||
});
|
||||
|
||||
} else {
|
||||
if (current.type !== FormatType.Text) {
|
||||
const textCurrent: IFormatParseTree = {
|
||||
type: FormatType.Text,
|
||||
content: next
|
||||
};
|
||||
current.children!.push(textCurrent);
|
||||
stack.push(current);
|
||||
current = textCurrent;
|
||||
|
||||
} else {
|
||||
current.content += next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (current.type === FormatType.Text) {
|
||||
current = stack.pop()!;
|
||||
}
|
||||
|
||||
if (stack.length) {
|
||||
// incorrectly formatted string literal
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
function isFormatTag(char: string, supportCodeSegments: boolean): boolean {
|
||||
return formatTagType(char, supportCodeSegments) !== FormatType.Invalid;
|
||||
}
|
||||
|
||||
function formatTagType(char: string, supportCodeSegments: boolean): FormatType {
|
||||
switch (char) {
|
||||
case '*':
|
||||
return FormatType.Bold;
|
||||
case '_':
|
||||
return FormatType.Italics;
|
||||
case '[':
|
||||
return FormatType.Action;
|
||||
case ']':
|
||||
return FormatType.ActionClose;
|
||||
case '`':
|
||||
return supportCodeSegments ? FormatType.Code : FormatType.Invalid;
|
||||
default:
|
||||
return FormatType.Invalid;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { StringSHA1, toHexString } from 'vs/base/common/hash';
|
||||
|
||||
export async function sha1Hex(str: string): Promise<string> {
|
||||
|
||||
// Prefer to use browser's crypto module
|
||||
if (globalThis?.crypto?.subtle) {
|
||||
|
||||
// Careful to use `dontUseNodeBuffer` when passing the
|
||||
// buffer to the browser `crypto` API. Users reported
|
||||
// native crashes in certain cases that we could trace
|
||||
// back to passing node.js `Buffer` around
|
||||
// (https://github.com/microsoft/vscode/issues/114227)
|
||||
const buffer = VSBuffer.fromString(str, { dontUseNodeBuffer: true }).buffer;
|
||||
const hash = await globalThis.crypto.subtle.digest({ name: 'sha-1' }, buffer);
|
||||
|
||||
return toHexString(hash);
|
||||
}
|
||||
|
||||
// Otherwise fallback to `StringSHA1`
|
||||
else {
|
||||
const computer = new StringSHA1();
|
||||
computer.update(str);
|
||||
|
||||
return computer.digest();
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
|
||||
export interface IHistoryNavigationWidget {
|
||||
|
||||
readonly element: HTMLElement;
|
||||
|
||||
showPreviousValue(): void;
|
||||
|
||||
showNextValue(): void;
|
||||
|
||||
onDidFocus: Event<void>;
|
||||
|
||||
onDidBlur: Event<void>;
|
||||
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { ErrorNoTelemetry, getErrorMessage } from 'vs/base/common/errors';
|
||||
import { mark } from 'vs/base/common/performance';
|
||||
|
||||
class MissingStoresError extends Error {
|
||||
constructor(readonly db: IDBDatabase) {
|
||||
super('Missing stores');
|
||||
}
|
||||
}
|
||||
|
||||
export class DBClosedError extends Error {
|
||||
readonly code = 'DBClosed';
|
||||
constructor(dbName: string) {
|
||||
super(`IndexedDB database '${dbName}' is closed.`);
|
||||
}
|
||||
}
|
||||
|
||||
export class IndexedDB {
|
||||
|
||||
static async create(name: string, version: number | undefined, stores: string[]): Promise<IndexedDB> {
|
||||
const database = await IndexedDB.openDatabase(name, version, stores);
|
||||
return new IndexedDB(database, name);
|
||||
}
|
||||
|
||||
private static async openDatabase(name: string, version: number | undefined, stores: string[]): Promise<IDBDatabase> {
|
||||
mark(`code/willOpenDatabase/${name}`);
|
||||
try {
|
||||
return await IndexedDB.doOpenDatabase(name, version, stores);
|
||||
} catch (err) {
|
||||
if (err instanceof MissingStoresError) {
|
||||
console.info(`Attempting to recreate the IndexedDB once.`, name);
|
||||
|
||||
try {
|
||||
// Try to delete the db
|
||||
await IndexedDB.deleteDatabase(err.db);
|
||||
} catch (error) {
|
||||
console.error(`Error while deleting the IndexedDB`, getErrorMessage(error));
|
||||
throw error;
|
||||
}
|
||||
|
||||
return await IndexedDB.doOpenDatabase(name, version, stores);
|
||||
}
|
||||
|
||||
throw err;
|
||||
} finally {
|
||||
mark(`code/didOpenDatabase/${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
private static doOpenDatabase(name: string, version: number | undefined, stores: string[]): Promise<IDBDatabase> {
|
||||
return new Promise((c, e) => {
|
||||
const request = indexedDB.open(name, version);
|
||||
request.onerror = () => e(request.error);
|
||||
request.onsuccess = () => {
|
||||
const db = request.result;
|
||||
for (const store of stores) {
|
||||
if (!db.objectStoreNames.contains(store)) {
|
||||
console.error(`Error while opening IndexedDB. Could not find '${store}'' object store`);
|
||||
e(new MissingStoresError(db));
|
||||
return;
|
||||
}
|
||||
}
|
||||
c(db);
|
||||
};
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
for (const store of stores) {
|
||||
if (!db.objectStoreNames.contains(store)) {
|
||||
db.createObjectStore(store);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private static deleteDatabase(database: IDBDatabase): Promise<void> {
|
||||
return new Promise((c, e) => {
|
||||
// Close any opened connections
|
||||
database.close();
|
||||
|
||||
// Delete the db
|
||||
const deleteRequest = indexedDB.deleteDatabase(database.name);
|
||||
deleteRequest.onerror = (err) => e(deleteRequest.error);
|
||||
deleteRequest.onsuccess = () => c();
|
||||
});
|
||||
}
|
||||
|
||||
private database: IDBDatabase | null = null;
|
||||
private readonly pendingTransactions: IDBTransaction[] = [];
|
||||
|
||||
constructor(database: IDBDatabase, private readonly name: string) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
hasPendingTransactions(): boolean {
|
||||
return this.pendingTransactions.length > 0;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (this.pendingTransactions.length) {
|
||||
this.pendingTransactions.splice(0, this.pendingTransactions.length).forEach(transaction => transaction.abort());
|
||||
}
|
||||
this.database?.close();
|
||||
this.database = null;
|
||||
}
|
||||
|
||||
runInTransaction<T>(store: string, transactionMode: IDBTransactionMode, dbRequestFn: (store: IDBObjectStore) => IDBRequest<T>[]): Promise<T[]>;
|
||||
runInTransaction<T>(store: string, transactionMode: IDBTransactionMode, dbRequestFn: (store: IDBObjectStore) => IDBRequest<T>): Promise<T>;
|
||||
async runInTransaction<T>(store: string, transactionMode: IDBTransactionMode, dbRequestFn: (store: IDBObjectStore) => IDBRequest<T> | IDBRequest<T>[]): Promise<T | T[]> {
|
||||
if (!this.database) {
|
||||
throw new DBClosedError(this.name);
|
||||
}
|
||||
const transaction = this.database.transaction(store, transactionMode);
|
||||
this.pendingTransactions.push(transaction);
|
||||
return new Promise<T | T[]>((c, e) => {
|
||||
transaction.oncomplete = () => {
|
||||
if (Array.isArray(request)) {
|
||||
c(request.map(r => r.result));
|
||||
} else {
|
||||
c(request.result);
|
||||
}
|
||||
};
|
||||
transaction.onerror = () => e(transaction.error ? ErrorNoTelemetry.fromError(transaction.error) : new ErrorNoTelemetry('unknown error'));
|
||||
transaction.onabort = () => e(transaction.error ? ErrorNoTelemetry.fromError(transaction.error) : new ErrorNoTelemetry('unknown error'));
|
||||
const request = dbRequestFn(transaction.objectStore(store));
|
||||
}).finally(() => this.pendingTransactions.splice(this.pendingTransactions.indexOf(transaction), 1));
|
||||
}
|
||||
|
||||
async getKeyValues<V>(store: string, isValid: (value: unknown) => value is V): Promise<Map<string, V>> {
|
||||
if (!this.database) {
|
||||
throw new DBClosedError(this.name);
|
||||
}
|
||||
const transaction = this.database.transaction(store, 'readonly');
|
||||
this.pendingTransactions.push(transaction);
|
||||
return new Promise<Map<string, V>>(resolve => {
|
||||
const items = new Map<string, V>();
|
||||
|
||||
const objectStore = transaction.objectStore(store);
|
||||
|
||||
// Open a IndexedDB Cursor to iterate over key/values
|
||||
const cursor = objectStore.openCursor();
|
||||
if (!cursor) {
|
||||
return resolve(items); // this means the `ItemTable` was empty
|
||||
}
|
||||
|
||||
// Iterate over rows of `ItemTable` until the end
|
||||
cursor.onsuccess = () => {
|
||||
if (cursor.result) {
|
||||
|
||||
// Keep cursor key/value in our map
|
||||
if (isValid(cursor.result.value)) {
|
||||
items.set(cursor.result.key.toString(), cursor.result.value);
|
||||
}
|
||||
|
||||
// Advance cursor to next row
|
||||
cursor.result.continue();
|
||||
} else {
|
||||
resolve(items); // reached end of table
|
||||
}
|
||||
};
|
||||
|
||||
// Error handlers
|
||||
const onError = (error: Error | null) => {
|
||||
console.error(`IndexedDB getKeyValues(): ${toErrorMessage(error, true)}`);
|
||||
|
||||
resolve(items);
|
||||
};
|
||||
cursor.onerror = () => onError(cursor.error);
|
||||
transaction.onerror = () => onError(transaction.error);
|
||||
}).finally(() => this.pendingTransactions.splice(this.pendingTransactions.indexOf(transaction), 1));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-aria-container {
|
||||
position: absolute; /* try to hide from window but not from screen readers */
|
||||
left:-999em;
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
// import 'vs/css!./aria';
|
||||
|
||||
// Use a max length since we are inserting the whole msg in the DOM and that can cause browsers to freeze for long messages #94233
|
||||
const MAX_MESSAGE_LENGTH = 20000;
|
||||
let ariaContainer: HTMLElement;
|
||||
let alertContainer: HTMLElement;
|
||||
let alertContainer2: HTMLElement;
|
||||
let statusContainer: HTMLElement;
|
||||
let statusContainer2: HTMLElement;
|
||||
export function setARIAContainer(parent: HTMLElement) {
|
||||
ariaContainer = document.createElement('div');
|
||||
ariaContainer.className = 'monaco-aria-container';
|
||||
|
||||
const createAlertContainer = () => {
|
||||
const element = document.createElement('div');
|
||||
element.className = 'monaco-alert';
|
||||
element.setAttribute('role', 'alert');
|
||||
element.setAttribute('aria-atomic', 'true');
|
||||
ariaContainer.appendChild(element);
|
||||
return element;
|
||||
};
|
||||
alertContainer = createAlertContainer();
|
||||
alertContainer2 = createAlertContainer();
|
||||
|
||||
const createStatusContainer = () => {
|
||||
const element = document.createElement('div');
|
||||
element.className = 'monaco-status';
|
||||
element.setAttribute('aria-live', 'polite');
|
||||
element.setAttribute('aria-atomic', 'true');
|
||||
ariaContainer.appendChild(element);
|
||||
return element;
|
||||
};
|
||||
statusContainer = createStatusContainer();
|
||||
statusContainer2 = createStatusContainer();
|
||||
|
||||
parent.appendChild(ariaContainer);
|
||||
}
|
||||
/**
|
||||
* Given the provided message, will make sure that it is read as alert to screen readers.
|
||||
*/
|
||||
export function alert(msg: string): void {
|
||||
if (!ariaContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use alternate containers such that duplicated messages get read out by screen readers #99466
|
||||
if (alertContainer.textContent !== msg) {
|
||||
dom.clearNode(alertContainer2);
|
||||
insertMessage(alertContainer, msg);
|
||||
} else {
|
||||
dom.clearNode(alertContainer);
|
||||
insertMessage(alertContainer2, msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the provided message, will make sure that it is read as status to screen readers.
|
||||
*/
|
||||
export function status(msg: string): void {
|
||||
if (!ariaContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusContainer.textContent !== msg) {
|
||||
dom.clearNode(statusContainer2);
|
||||
insertMessage(statusContainer, msg);
|
||||
} else {
|
||||
dom.clearNode(statusContainer);
|
||||
insertMessage(statusContainer2, msg);
|
||||
}
|
||||
}
|
||||
|
||||
function insertMessage(target: HTMLElement, msg: string): void {
|
||||
dom.clearNode(target);
|
||||
if (msg.length > MAX_MESSAGE_LENGTH) {
|
||||
msg = msg.substr(0, MAX_MESSAGE_LENGTH);
|
||||
}
|
||||
target.textContent = msg;
|
||||
|
||||
// See https://www.paciellogroup.com/blog/2012/06/html5-accessibility-chops-aria-rolealert-browser-support/
|
||||
target.style.visibility = 'hidden';
|
||||
target.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
// Copied from @types/react which original came from https://www.w3.org/TR/wai-aria-1.1/#role_definitions
|
||||
export type AriaRole =
|
||||
| 'alert'
|
||||
| 'alertdialog'
|
||||
| 'application'
|
||||
| 'article'
|
||||
| 'banner'
|
||||
| 'button'
|
||||
| 'cell'
|
||||
| 'checkbox'
|
||||
| 'columnheader'
|
||||
| 'combobox'
|
||||
| 'complementary'
|
||||
| 'contentinfo'
|
||||
| 'definition'
|
||||
| 'dialog'
|
||||
| 'directory'
|
||||
| 'document'
|
||||
| 'feed'
|
||||
| 'figure'
|
||||
| 'form'
|
||||
| 'grid'
|
||||
| 'gridcell'
|
||||
| 'group'
|
||||
| 'heading'
|
||||
| 'img'
|
||||
| 'link'
|
||||
| 'list'
|
||||
| 'listbox'
|
||||
| 'listitem'
|
||||
| 'log'
|
||||
| 'main'
|
||||
| 'marquee'
|
||||
| 'math'
|
||||
| 'menu'
|
||||
| 'menubar'
|
||||
| 'menuitem'
|
||||
| 'menuitemcheckbox'
|
||||
| 'menuitemradio'
|
||||
| 'navigation'
|
||||
| 'none'
|
||||
| 'note'
|
||||
| 'option'
|
||||
| 'presentation'
|
||||
| 'progressbar'
|
||||
| 'radio'
|
||||
| 'radiogroup'
|
||||
| 'region'
|
||||
| 'row'
|
||||
| 'rowgroup'
|
||||
| 'rowheader'
|
||||
| 'scrollbar'
|
||||
| 'search'
|
||||
| 'searchbox'
|
||||
| 'separator'
|
||||
| 'slider'
|
||||
| 'spinbutton'
|
||||
| 'status'
|
||||
| 'switch'
|
||||
| 'tab'
|
||||
| 'table'
|
||||
| 'tablist'
|
||||
| 'tabpanel'
|
||||
| 'term'
|
||||
| 'textbox'
|
||||
| 'timer'
|
||||
| 'toolbar'
|
||||
| 'tooltip'
|
||||
| 'tree'
|
||||
| 'treegrid'
|
||||
| 'treeitem'
|
||||
| (string & {}) // Prevent type collapsing to `string`
|
||||
;
|
||||
@@ -1,36 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-breadcrumbs {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
outline-style: none;
|
||||
}
|
||||
|
||||
.monaco-breadcrumbs .monaco-breadcrumb-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 1 auto;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
align-self: center;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
}
|
||||
.monaco-breadcrumbs.disabled .monaco-breadcrumb-item {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-breadcrumbs .monaco-breadcrumb-item .codicon-breadcrumb-separator {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.monaco-breadcrumbs .monaco-breadcrumb-item:first-of-type::before {
|
||||
content: ' ';
|
||||
}
|
||||
@@ -1,356 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||
import { commonPrefixLength } from 'vs/base/common/arrays';
|
||||
import { ThemeIcon } from 'vs/base/common/themables';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
// import 'vs/css!./breadcrumbsWidget';
|
||||
|
||||
export abstract class BreadcrumbsItem {
|
||||
abstract dispose(): void;
|
||||
abstract equals(other: BreadcrumbsItem): boolean;
|
||||
abstract render(container: HTMLElement): void;
|
||||
}
|
||||
|
||||
export interface IBreadcrumbsWidgetStyles {
|
||||
readonly breadcrumbsBackground: string | undefined;
|
||||
readonly breadcrumbsForeground: string | undefined;
|
||||
readonly breadcrumbsHoverForeground: string | undefined;
|
||||
readonly breadcrumbsFocusForeground: string | undefined;
|
||||
readonly breadcrumbsFocusAndSelectionForeground: string | undefined;
|
||||
}
|
||||
|
||||
export interface IBreadcrumbsItemEvent {
|
||||
type: 'select' | 'focus';
|
||||
item: BreadcrumbsItem;
|
||||
node: HTMLElement;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
export class BreadcrumbsWidget {
|
||||
|
||||
private readonly _disposables = new DisposableStore();
|
||||
private readonly _domNode: HTMLDivElement;
|
||||
private readonly _scrollable: DomScrollableElement;
|
||||
|
||||
private readonly _onDidSelectItem = new Emitter<IBreadcrumbsItemEvent>();
|
||||
private readonly _onDidFocusItem = new Emitter<IBreadcrumbsItemEvent>();
|
||||
private readonly _onDidChangeFocus = new Emitter<boolean>();
|
||||
|
||||
readonly onDidSelectItem: Event<IBreadcrumbsItemEvent> = this._onDidSelectItem.event;
|
||||
readonly onDidFocusItem: Event<IBreadcrumbsItemEvent> = this._onDidFocusItem.event;
|
||||
readonly onDidChangeFocus: Event<boolean> = this._onDidChangeFocus.event;
|
||||
|
||||
private readonly _items = new Array<BreadcrumbsItem>();
|
||||
private readonly _nodes = new Array<HTMLDivElement>();
|
||||
private readonly _freeNodes = new Array<HTMLDivElement>();
|
||||
private readonly _separatorIcon: ThemeIcon;
|
||||
|
||||
private _enabled: boolean = true;
|
||||
private _focusedItemIdx: number = -1;
|
||||
private _selectedItemIdx: number = -1;
|
||||
|
||||
private _pendingDimLayout: IDisposable | undefined;
|
||||
private _pendingLayout: IDisposable | undefined;
|
||||
private _dimension: dom.Dimension | undefined;
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
horizontalScrollbarSize: number,
|
||||
separatorIcon: ThemeIcon,
|
||||
styles: IBreadcrumbsWidgetStyles
|
||||
) {
|
||||
this._domNode = document.createElement('div');
|
||||
this._domNode.className = 'monaco-breadcrumbs';
|
||||
this._domNode.tabIndex = 0;
|
||||
this._domNode.setAttribute('role', 'list');
|
||||
this._scrollable = new DomScrollableElement(this._domNode, {
|
||||
vertical: ScrollbarVisibility.Hidden,
|
||||
horizontal: ScrollbarVisibility.Auto,
|
||||
horizontalScrollbarSize,
|
||||
useShadows: false,
|
||||
scrollYToX: true
|
||||
});
|
||||
this._separatorIcon = separatorIcon;
|
||||
this._disposables.add(this._scrollable);
|
||||
this._disposables.add(dom.addStandardDisposableListener(this._domNode, 'click', e => this._onClick(e)));
|
||||
container.appendChild(this._scrollable.getDomNode());
|
||||
|
||||
const styleElement = dom.createStyleSheet(this._domNode);
|
||||
this._style(styleElement, styles);
|
||||
|
||||
const focusTracker = dom.trackFocus(this._domNode);
|
||||
this._disposables.add(focusTracker);
|
||||
this._disposables.add(focusTracker.onDidBlur(_ => this._onDidChangeFocus.fire(false)));
|
||||
this._disposables.add(focusTracker.onDidFocus(_ => this._onDidChangeFocus.fire(true)));
|
||||
}
|
||||
|
||||
setHorizontalScrollbarSize(size: number) {
|
||||
this._scrollable.updateOptions({
|
||||
horizontalScrollbarSize: size
|
||||
});
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposables.dispose();
|
||||
this._pendingLayout?.dispose();
|
||||
this._pendingDimLayout?.dispose();
|
||||
this._onDidSelectItem.dispose();
|
||||
this._onDidFocusItem.dispose();
|
||||
this._onDidChangeFocus.dispose();
|
||||
this._domNode.remove();
|
||||
this._nodes.length = 0;
|
||||
this._freeNodes.length = 0;
|
||||
}
|
||||
|
||||
layout(dim: dom.Dimension | undefined): void {
|
||||
if (dim && dom.Dimension.equals(dim, this._dimension)) {
|
||||
return;
|
||||
}
|
||||
if (dim) {
|
||||
// only measure
|
||||
this._pendingDimLayout?.dispose();
|
||||
this._pendingDimLayout = this._updateDimensions(dim);
|
||||
} else {
|
||||
this._pendingLayout?.dispose();
|
||||
this._pendingLayout = this._updateScrollbar();
|
||||
}
|
||||
}
|
||||
|
||||
private _updateDimensions(dim: dom.Dimension): IDisposable {
|
||||
const disposables = new DisposableStore();
|
||||
disposables.add(dom.modify(dom.getWindow(this._domNode), () => {
|
||||
this._dimension = dim;
|
||||
this._domNode.style.width = `${dim.width}px`;
|
||||
this._domNode.style.height = `${dim.height}px`;
|
||||
disposables.add(this._updateScrollbar());
|
||||
}));
|
||||
return disposables;
|
||||
}
|
||||
|
||||
private _updateScrollbar(): IDisposable {
|
||||
return dom.measure(dom.getWindow(this._domNode), () => {
|
||||
dom.measure(dom.getWindow(this._domNode), () => { // double RAF
|
||||
this._scrollable.setRevealOnScroll(false);
|
||||
this._scrollable.scanDomNode();
|
||||
this._scrollable.setRevealOnScroll(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _style(styleElement: HTMLStyleElement, style: IBreadcrumbsWidgetStyles): void {
|
||||
let content = '';
|
||||
if (style.breadcrumbsBackground) {
|
||||
content += `.monaco-breadcrumbs { background-color: ${style.breadcrumbsBackground}}`;
|
||||
}
|
||||
if (style.breadcrumbsForeground) {
|
||||
content += `.monaco-breadcrumbs .monaco-breadcrumb-item { color: ${style.breadcrumbsForeground}}\n`;
|
||||
}
|
||||
if (style.breadcrumbsFocusForeground) {
|
||||
content += `.monaco-breadcrumbs .monaco-breadcrumb-item.focused { color: ${style.breadcrumbsFocusForeground}}\n`;
|
||||
}
|
||||
if (style.breadcrumbsFocusAndSelectionForeground) {
|
||||
content += `.monaco-breadcrumbs .monaco-breadcrumb-item.focused.selected { color: ${style.breadcrumbsFocusAndSelectionForeground}}\n`;
|
||||
}
|
||||
if (style.breadcrumbsHoverForeground) {
|
||||
content += `.monaco-breadcrumbs:not(.disabled ) .monaco-breadcrumb-item:hover:not(.focused):not(.selected) { color: ${style.breadcrumbsHoverForeground}}\n`;
|
||||
}
|
||||
styleElement.innerText = content;
|
||||
}
|
||||
|
||||
setEnabled(value: boolean) {
|
||||
this._enabled = value;
|
||||
this._domNode.classList.toggle('disabled', !this._enabled);
|
||||
}
|
||||
|
||||
domFocus(): void {
|
||||
const idx = this._focusedItemIdx >= 0 ? this._focusedItemIdx : this._items.length - 1;
|
||||
if (idx >= 0 && idx < this._items.length) {
|
||||
this._focus(idx, undefined);
|
||||
} else {
|
||||
this._domNode.focus();
|
||||
}
|
||||
}
|
||||
|
||||
isDOMFocused(): boolean {
|
||||
return dom.isAncestorOfActiveElement(this._domNode);
|
||||
}
|
||||
|
||||
getFocused(): BreadcrumbsItem {
|
||||
return this._items[this._focusedItemIdx];
|
||||
}
|
||||
|
||||
setFocused(item: BreadcrumbsItem | undefined, payload?: any): void {
|
||||
this._focus(this._items.indexOf(item!), payload);
|
||||
}
|
||||
|
||||
focusPrev(payload?: any): any {
|
||||
if (this._focusedItemIdx > 0) {
|
||||
this._focus(this._focusedItemIdx - 1, payload);
|
||||
}
|
||||
}
|
||||
|
||||
focusNext(payload?: any): any {
|
||||
if (this._focusedItemIdx + 1 < this._nodes.length) {
|
||||
this._focus(this._focusedItemIdx + 1, payload);
|
||||
}
|
||||
}
|
||||
|
||||
private _focus(nth: number, payload: any): void {
|
||||
this._focusedItemIdx = -1;
|
||||
for (let i = 0; i < this._nodes.length; i++) {
|
||||
const node = this._nodes[i];
|
||||
if (i !== nth) {
|
||||
node.classList.remove('focused');
|
||||
} else {
|
||||
this._focusedItemIdx = i;
|
||||
node.classList.add('focused');
|
||||
node.focus();
|
||||
}
|
||||
}
|
||||
this._reveal(this._focusedItemIdx, true);
|
||||
this._onDidFocusItem.fire({ type: 'focus', item: this._items[this._focusedItemIdx], node: this._nodes[this._focusedItemIdx], payload });
|
||||
}
|
||||
|
||||
reveal(item: BreadcrumbsItem): void {
|
||||
const idx = this._items.indexOf(item);
|
||||
if (idx >= 0) {
|
||||
this._reveal(idx, false);
|
||||
}
|
||||
}
|
||||
|
||||
revealLast(): void {
|
||||
this._reveal(this._items.length - 1, false);
|
||||
}
|
||||
|
||||
private _reveal(nth: number, minimal: boolean): void {
|
||||
if (nth < 0 || nth >= this._nodes.length) {
|
||||
return;
|
||||
}
|
||||
const node = this._nodes[nth];
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const { width } = this._scrollable.getScrollDimensions();
|
||||
const { scrollLeft } = this._scrollable.getScrollPosition();
|
||||
if (!minimal || node.offsetLeft > scrollLeft + width || node.offsetLeft < scrollLeft) {
|
||||
this._scrollable.setRevealOnScroll(false);
|
||||
this._scrollable.setScrollPosition({ scrollLeft: node.offsetLeft });
|
||||
this._scrollable.setRevealOnScroll(true);
|
||||
}
|
||||
}
|
||||
|
||||
getSelection(): BreadcrumbsItem {
|
||||
return this._items[this._selectedItemIdx];
|
||||
}
|
||||
|
||||
setSelection(item: BreadcrumbsItem | undefined, payload?: any): void {
|
||||
this._select(this._items.indexOf(item!), payload);
|
||||
}
|
||||
|
||||
private _select(nth: number, payload: any): void {
|
||||
this._selectedItemIdx = -1;
|
||||
for (let i = 0; i < this._nodes.length; i++) {
|
||||
const node = this._nodes[i];
|
||||
if (i !== nth) {
|
||||
node.classList.remove('selected');
|
||||
} else {
|
||||
this._selectedItemIdx = i;
|
||||
node.classList.add('selected');
|
||||
}
|
||||
}
|
||||
this._onDidSelectItem.fire({ type: 'select', item: this._items[this._selectedItemIdx], node: this._nodes[this._selectedItemIdx], payload });
|
||||
}
|
||||
|
||||
getItems(): readonly BreadcrumbsItem[] {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
setItems(items: BreadcrumbsItem[]): void {
|
||||
let prefix: number | undefined;
|
||||
let removed: BreadcrumbsItem[] = [];
|
||||
try {
|
||||
prefix = commonPrefixLength(this._items, items, (a, b) => a.equals(b));
|
||||
removed = this._items.splice(prefix, this._items.length - prefix, ...items.slice(prefix));
|
||||
this._render(prefix);
|
||||
dispose(removed);
|
||||
this._focus(-1, undefined);
|
||||
} catch (e) {
|
||||
const newError = new Error(`BreadcrumbsItem#setItems: newItems: ${items.length}, prefix: ${prefix}, removed: ${removed.length}`);
|
||||
newError.name = e.name;
|
||||
newError.stack = e.stack;
|
||||
throw newError;
|
||||
}
|
||||
}
|
||||
|
||||
private _render(start: number): void {
|
||||
let didChange = false;
|
||||
for (; start < this._items.length && start < this._nodes.length; start++) {
|
||||
const item = this._items[start];
|
||||
const node = this._nodes[start];
|
||||
this._renderItem(item, node);
|
||||
didChange = true;
|
||||
}
|
||||
// case a: more nodes -> remove them
|
||||
while (start < this._nodes.length) {
|
||||
const free = this._nodes.pop();
|
||||
if (free) {
|
||||
this._freeNodes.push(free);
|
||||
free.remove();
|
||||
didChange = true;
|
||||
}
|
||||
}
|
||||
|
||||
// case b: more items -> render them
|
||||
for (; start < this._items.length; start++) {
|
||||
const item = this._items[start];
|
||||
const node = this._freeNodes.length > 0 ? this._freeNodes.pop() : document.createElement('div');
|
||||
if (node) {
|
||||
this._renderItem(item, node);
|
||||
this._domNode.appendChild(node);
|
||||
this._nodes.push(node);
|
||||
didChange = true;
|
||||
}
|
||||
}
|
||||
if (didChange) {
|
||||
this.layout(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
private _renderItem(item: BreadcrumbsItem, container: HTMLDivElement): void {
|
||||
dom.clearNode(container);
|
||||
container.className = '';
|
||||
try {
|
||||
item.render(container);
|
||||
} catch (err) {
|
||||
container.innerText = '<<RENDER ERROR>>';
|
||||
console.error(err);
|
||||
}
|
||||
container.tabIndex = -1;
|
||||
container.setAttribute('role', 'listitem');
|
||||
container.classList.add('monaco-breadcrumb-item');
|
||||
const iconContainer = dom.$(ThemeIcon.asCSSSelector(this._separatorIcon));
|
||||
container.appendChild(iconContainer);
|
||||
}
|
||||
|
||||
private _onClick(event: IMouseEvent): void {
|
||||
if (!this._enabled) {
|
||||
return;
|
||||
}
|
||||
for (let el: HTMLElement | null = event.target; el; el = el.parentElement) {
|
||||
const idx = this._nodes.indexOf(el as HTMLDivElement);
|
||||
if (idx >= 0) {
|
||||
this._focus(idx, event);
|
||||
this._select(idx, event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { $, IDomNodePagePosition } from 'vs/base/browser/dom';
|
||||
import { IView, IViewSize } from 'vs/base/browser/ui/grid/grid';
|
||||
import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash';
|
||||
import { DistributeSizing, ISplitViewStyles, IView as ISplitViewView, Orientation, SplitView } from 'vs/base/browser/ui/splitview/splitview';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export interface CenteredViewState {
|
||||
// width of the fixed centered layout
|
||||
targetWidth: number;
|
||||
// proportional size of left margin
|
||||
leftMarginRatio: number;
|
||||
// proportional size of right margin
|
||||
rightMarginRatio: number;
|
||||
}
|
||||
|
||||
const defaultState: CenteredViewState = {
|
||||
targetWidth: 900,
|
||||
leftMarginRatio: 0.1909,
|
||||
rightMarginRatio: 0.1909,
|
||||
};
|
||||
|
||||
const distributeSizing: DistributeSizing = { type: 'distribute' };
|
||||
|
||||
function createEmptyView(background: Color | undefined): ISplitViewView<{ top: number; left: number }> {
|
||||
const element = $('.centered-layout-margin');
|
||||
element.style.height = '100%';
|
||||
if (background) {
|
||||
element.style.backgroundColor = background.toString();
|
||||
}
|
||||
|
||||
return {
|
||||
element,
|
||||
layout: () => undefined,
|
||||
minimumSize: 60,
|
||||
maximumSize: Number.POSITIVE_INFINITY,
|
||||
onDidChange: Event.None
|
||||
};
|
||||
}
|
||||
|
||||
function toSplitViewView(view: IView, getHeight: () => number): ISplitViewView<{ top: number; left: number }> {
|
||||
return {
|
||||
element: view.element,
|
||||
get maximumSize() { return view.maximumWidth; },
|
||||
get minimumSize() { return view.minimumWidth; },
|
||||
onDidChange: Event.map(view.onDidChange, e => e && e.width),
|
||||
layout: (size, offset, ctx) => view.layout(size, getHeight(), ctx?.top ?? 0, (ctx?.left ?? 0) + offset)
|
||||
};
|
||||
}
|
||||
|
||||
export interface ICenteredViewStyles extends ISplitViewStyles {
|
||||
background: Color;
|
||||
}
|
||||
|
||||
export class CenteredViewLayout implements IDisposable {
|
||||
|
||||
private splitView?: SplitView<{ top: number; left: number }>;
|
||||
private lastLayoutPosition: IDomNodePagePosition = { width: 0, height: 0, left: 0, top: 0 };
|
||||
private style!: ICenteredViewStyles;
|
||||
private didLayout = false;
|
||||
private emptyViews: ISplitViewView<{ top: number; left: number }>[] | undefined;
|
||||
private readonly splitViewDisposables = new DisposableStore();
|
||||
|
||||
constructor(
|
||||
private container: HTMLElement,
|
||||
private view: IView,
|
||||
public state: CenteredViewState = { ...defaultState },
|
||||
private centeredLayoutFixedWidth: boolean = false
|
||||
) {
|
||||
this.container.appendChild(this.view.element);
|
||||
// Make sure to hide the split view overflow like sashes #52892
|
||||
this.container.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
get minimumWidth(): number { return this.splitView ? this.splitView.minimumSize : this.view.minimumWidth; }
|
||||
get maximumWidth(): number { return this.splitView ? this.splitView.maximumSize : this.view.maximumWidth; }
|
||||
get minimumHeight(): number { return this.view.minimumHeight; }
|
||||
get maximumHeight(): number { return this.view.maximumHeight; }
|
||||
get onDidChange(): Event<IViewSize | undefined> { return this.view.onDidChange; }
|
||||
|
||||
private _boundarySashes: IBoundarySashes = {};
|
||||
get boundarySashes(): IBoundarySashes { return this._boundarySashes; }
|
||||
set boundarySashes(boundarySashes: IBoundarySashes) {
|
||||
this._boundarySashes = boundarySashes;
|
||||
|
||||
if (!this.splitView) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.splitView.orthogonalStartSash = boundarySashes.top;
|
||||
this.splitView.orthogonalEndSash = boundarySashes.bottom;
|
||||
}
|
||||
|
||||
layout(width: number, height: number, top: number, left: number): void {
|
||||
this.lastLayoutPosition = { width, height, top, left };
|
||||
if (this.splitView) {
|
||||
this.splitView.layout(width, this.lastLayoutPosition);
|
||||
if (!this.didLayout || this.centeredLayoutFixedWidth) {
|
||||
this.resizeSplitViews();
|
||||
}
|
||||
} else {
|
||||
this.view.layout(width, height, top, left);
|
||||
}
|
||||
|
||||
this.didLayout = true;
|
||||
}
|
||||
|
||||
private resizeSplitViews(): void {
|
||||
if (!this.splitView) {
|
||||
return;
|
||||
}
|
||||
if (this.centeredLayoutFixedWidth) {
|
||||
const centerViewWidth = Math.min(this.lastLayoutPosition.width, this.state.targetWidth);
|
||||
const marginWidthFloat = (this.lastLayoutPosition.width - centerViewWidth) / 2;
|
||||
this.splitView.resizeView(0, Math.floor(marginWidthFloat));
|
||||
this.splitView.resizeView(1, centerViewWidth);
|
||||
this.splitView.resizeView(2, Math.ceil(marginWidthFloat));
|
||||
} else {
|
||||
const leftMargin = this.state.leftMarginRatio * this.lastLayoutPosition.width;
|
||||
const rightMargin = this.state.rightMarginRatio * this.lastLayoutPosition.width;
|
||||
const center = this.lastLayoutPosition.width - leftMargin - rightMargin;
|
||||
this.splitView.resizeView(0, leftMargin);
|
||||
this.splitView.resizeView(1, center);
|
||||
this.splitView.resizeView(2, rightMargin);
|
||||
}
|
||||
}
|
||||
|
||||
setFixedWidth(option: boolean) {
|
||||
this.centeredLayoutFixedWidth = option;
|
||||
if (!!this.splitView) {
|
||||
this.updateState();
|
||||
this.resizeSplitViews();
|
||||
}
|
||||
}
|
||||
|
||||
private updateState() {
|
||||
if (!!this.splitView) {
|
||||
this.state.targetWidth = this.splitView.getViewSize(1);
|
||||
this.state.leftMarginRatio = this.splitView.getViewSize(0) / this.lastLayoutPosition.width;
|
||||
this.state.rightMarginRatio = this.splitView.getViewSize(2) / this.lastLayoutPosition.width;
|
||||
}
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return !!this.splitView;
|
||||
}
|
||||
|
||||
styles(style: ICenteredViewStyles): void {
|
||||
this.style = style;
|
||||
if (this.splitView && this.emptyViews) {
|
||||
this.splitView.style(this.style);
|
||||
this.emptyViews[0].element.style.backgroundColor = this.style.background.toString();
|
||||
this.emptyViews[1].element.style.backgroundColor = this.style.background.toString();
|
||||
}
|
||||
}
|
||||
|
||||
activate(active: boolean): void {
|
||||
if (active === this.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (active) {
|
||||
this.view.element.remove();
|
||||
this.splitView = new SplitView(this.container, {
|
||||
inverseAltBehavior: true,
|
||||
orientation: Orientation.HORIZONTAL,
|
||||
styles: this.style
|
||||
});
|
||||
this.splitView.orthogonalStartSash = this.boundarySashes.top;
|
||||
this.splitView.orthogonalEndSash = this.boundarySashes.bottom;
|
||||
|
||||
this.splitViewDisposables.add(this.splitView.onDidSashChange(() => {
|
||||
if (!!this.splitView) {
|
||||
this.updateState();
|
||||
}
|
||||
}));
|
||||
this.splitViewDisposables.add(this.splitView.onDidSashReset(() => {
|
||||
this.state = { ...defaultState };
|
||||
this.resizeSplitViews();
|
||||
}));
|
||||
|
||||
this.splitView.layout(this.lastLayoutPosition.width, this.lastLayoutPosition);
|
||||
const backgroundColor = this.style ? this.style.background : undefined;
|
||||
this.emptyViews = [createEmptyView(backgroundColor), createEmptyView(backgroundColor)];
|
||||
|
||||
this.splitView.addView(this.emptyViews[0], distributeSizing, 0);
|
||||
this.splitView.addView(toSplitViewView(this.view, () => this.lastLayoutPosition.height), distributeSizing, 1);
|
||||
this.splitView.addView(this.emptyViews[1], distributeSizing, 2);
|
||||
|
||||
this.resizeSplitViews();
|
||||
} else {
|
||||
this.splitView?.el.remove();
|
||||
this.splitViewDisposables.clear();
|
||||
this.splitView?.dispose();
|
||||
this.splitView = undefined;
|
||||
this.emptyViews = undefined;
|
||||
this.container.appendChild(this.view.element);
|
||||
this.view.layout(this.lastLayoutPosition.width, this.lastLayoutPosition.height, this.lastLayoutPosition.top, this.lastLayoutPosition.left);
|
||||
}
|
||||
}
|
||||
|
||||
isDefault(state: CenteredViewState): boolean {
|
||||
if (this.centeredLayoutFixedWidth) {
|
||||
return state.targetWidth === defaultState.targetWidth;
|
||||
} else {
|
||||
return state.leftMarginRatio === defaultState.leftMarginRatio
|
||||
&& state.rightMarginRatio === defaultState.rightMarginRatio;
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.splitViewDisposables.dispose();
|
||||
|
||||
if (this.splitView) {
|
||||
this.splitView.dispose();
|
||||
this.splitView = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.codicon-wrench-subaction {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@keyframes codicon-spin {
|
||||
100% {
|
||||
transform:rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.codicon-sync.codicon-modifier-spin,
|
||||
.codicon-loading.codicon-modifier-spin,
|
||||
.codicon-gear.codicon-modifier-spin,
|
||||
.codicon-notebook-state-executing.codicon-modifier-spin {
|
||||
/* Use steps to throttle FPS to reduce CPU usage */
|
||||
animation: codicon-spin 1.5s steps(30) infinite;
|
||||
}
|
||||
|
||||
.codicon-modifier-disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* custom speed & easing for loading icon */
|
||||
.codicon-loading,
|
||||
.codicon-tree-item-loading::before {
|
||||
animation-duration: 1s !important;
|
||||
animation-timing-function: cubic-bezier(0.53, 0.21, 0.29, 0.67) !important;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
@font-face {
|
||||
font-family: "codicon";
|
||||
font-display: block;
|
||||
src: url("./codicon.ttf?5d4d76ab2ce5108968ad644d591a16a6") format("truetype");
|
||||
}
|
||||
|
||||
.codicon[class*='codicon-'] {
|
||||
font: normal normal normal 16px/1 codicon;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
text-rendering: auto;
|
||||
text-align: center;
|
||||
text-transform: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
/* icon rules are dynamically created by the platform theme service (see iconsStyleSheet.ts) */
|
||||
@@ -1,7 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// import 'vs/css!./codicon/codicon';
|
||||
// import 'vs/css!./codicon/codicon-modifiers';
|
||||
@@ -1,24 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-count-badge {
|
||||
padding: 3px 6px;
|
||||
border-radius: 11px;
|
||||
font-size: 11px;
|
||||
min-width: 18px;
|
||||
min-height: 18px;
|
||||
line-height: 11px;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.monaco-count-badge.long {
|
||||
padding: 2px 3px;
|
||||
border-radius: 2px;
|
||||
min-height: auto;
|
||||
line-height: normal;
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { $, append } from 'vs/base/browser/dom';
|
||||
import { format } from 'vs/base/common/strings';
|
||||
// import 'vs/css!./countBadge';
|
||||
|
||||
export interface ICountBadgeOptions {
|
||||
readonly count?: number;
|
||||
readonly countFormat?: string;
|
||||
readonly titleFormat?: string;
|
||||
}
|
||||
|
||||
export interface ICountBadgeStyles {
|
||||
readonly badgeBackground: string | undefined;
|
||||
readonly badgeForeground: string | undefined;
|
||||
readonly badgeBorder: string | undefined;
|
||||
}
|
||||
|
||||
export const unthemedCountStyles: ICountBadgeStyles = {
|
||||
badgeBackground: '#4D4D4D',
|
||||
badgeForeground: '#FFFFFF',
|
||||
badgeBorder: undefined
|
||||
};
|
||||
|
||||
export class CountBadge {
|
||||
|
||||
private element: HTMLElement;
|
||||
private count: number = 0;
|
||||
private countFormat: string;
|
||||
private titleFormat: string;
|
||||
|
||||
constructor(container: HTMLElement, private readonly options: ICountBadgeOptions, private readonly styles: ICountBadgeStyles) {
|
||||
|
||||
this.element = append(container, $('.monaco-count-badge'));
|
||||
this.countFormat = this.options.countFormat || '{0}';
|
||||
this.titleFormat = this.options.titleFormat || '';
|
||||
this.setCount(this.options.count || 0);
|
||||
}
|
||||
|
||||
setCount(count: number) {
|
||||
this.count = count;
|
||||
this.render();
|
||||
}
|
||||
|
||||
setCountFormat(countFormat: string) {
|
||||
this.countFormat = countFormat;
|
||||
this.render();
|
||||
}
|
||||
|
||||
setTitleFormat(titleFormat: string) {
|
||||
this.titleFormat = titleFormat;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private render() {
|
||||
this.element.textContent = format(this.countFormat, this.count);
|
||||
this.element.title = format(this.titleFormat, this.count);
|
||||
|
||||
this.element.style.backgroundColor = this.styles.badgeBackground ?? '';
|
||||
this.element.style.color = this.styles.badgeForeground ?? '';
|
||||
|
||||
if (this.styles.badgeBorder) {
|
||||
this.element.style.border = `1px solid ${this.styles.badgeBorder}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,947 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IBoundarySashes, Orientation } from 'vs/base/browser/ui/sash/sash';
|
||||
import { equals, tail2 as tail } from 'vs/base/common/arrays';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
// import 'vs/css!./gridview';
|
||||
import { Box, GridView, IGridViewOptions, IGridViewStyles, IView as IGridViewView, IViewSize, orthogonal, Sizing as GridViewSizing, GridLocation } from './gridview';
|
||||
import type { SplitView, AutoSizing as SplitViewAutoSizing } from 'vs/base/browser/ui/splitview/splitview';
|
||||
|
||||
export type { IViewSize };
|
||||
export { LayoutPriority, Orientation, orthogonal } from './gridview';
|
||||
|
||||
export const enum Direction {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right
|
||||
}
|
||||
|
||||
function oppositeDirection(direction: Direction): Direction {
|
||||
switch (direction) {
|
||||
case Direction.Up: return Direction.Down;
|
||||
case Direction.Down: return Direction.Up;
|
||||
case Direction.Left: return Direction.Right;
|
||||
case Direction.Right: return Direction.Left;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface to implement for views within a {@link Grid}.
|
||||
*/
|
||||
export interface IView extends IGridViewView {
|
||||
|
||||
/**
|
||||
* The preferred width for when the user double clicks a sash
|
||||
* adjacent to this view.
|
||||
*/
|
||||
readonly preferredWidth?: number;
|
||||
|
||||
/**
|
||||
* The preferred height for when the user double clicks a sash
|
||||
* adjacent to this view.
|
||||
*/
|
||||
readonly preferredHeight?: number;
|
||||
}
|
||||
|
||||
export interface GridLeafNode<T extends IView> {
|
||||
readonly view: T;
|
||||
readonly box: Box;
|
||||
readonly cachedVisibleSize: number | undefined;
|
||||
readonly maximized: boolean;
|
||||
}
|
||||
|
||||
export interface GridBranchNode<T extends IView> {
|
||||
readonly children: GridNode<T>[];
|
||||
readonly box: Box;
|
||||
}
|
||||
|
||||
export type GridNode<T extends IView> = GridLeafNode<T> | GridBranchNode<T>;
|
||||
|
||||
export function isGridBranchNode<T extends IView>(node: GridNode<T>): node is GridBranchNode<T> {
|
||||
return !!(node as any).children;
|
||||
}
|
||||
|
||||
function getGridNode<T extends IView>(node: GridNode<T>, location: GridLocation): GridNode<T> {
|
||||
if (location.length === 0) {
|
||||
return node;
|
||||
}
|
||||
|
||||
if (!isGridBranchNode(node)) {
|
||||
throw new Error('Invalid location');
|
||||
}
|
||||
|
||||
const [index, ...rest] = location;
|
||||
return getGridNode(node.children[index], rest);
|
||||
}
|
||||
|
||||
interface Range {
|
||||
readonly start: number;
|
||||
readonly end: number;
|
||||
}
|
||||
|
||||
function intersects(one: Range, other: Range): boolean {
|
||||
return !(one.start >= other.end || other.start >= one.end);
|
||||
}
|
||||
|
||||
interface Boundary {
|
||||
readonly offset: number;
|
||||
readonly range: Range;
|
||||
}
|
||||
|
||||
function getBoxBoundary(box: Box, direction: Direction): Boundary {
|
||||
const orientation = getDirectionOrientation(direction);
|
||||
const offset = direction === Direction.Up ? box.top :
|
||||
direction === Direction.Right ? box.left + box.width :
|
||||
direction === Direction.Down ? box.top + box.height :
|
||||
box.left;
|
||||
|
||||
const range = {
|
||||
start: orientation === Orientation.HORIZONTAL ? box.top : box.left,
|
||||
end: orientation === Orientation.HORIZONTAL ? box.top + box.height : box.left + box.width
|
||||
};
|
||||
|
||||
return { offset, range };
|
||||
}
|
||||
|
||||
function findAdjacentBoxLeafNodes<T extends IView>(boxNode: GridNode<T>, direction: Direction, boundary: Boundary): GridLeafNode<T>[] {
|
||||
const result: GridLeafNode<T>[] = [];
|
||||
|
||||
function _(boxNode: GridNode<T>, direction: Direction, boundary: Boundary): void {
|
||||
if (isGridBranchNode(boxNode)) {
|
||||
for (const child of boxNode.children) {
|
||||
_(child, direction, boundary);
|
||||
}
|
||||
} else {
|
||||
const { offset, range } = getBoxBoundary(boxNode.box, direction);
|
||||
|
||||
if (offset === boundary.offset && intersects(range, boundary.range)) {
|
||||
result.push(boxNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_(boxNode, direction, boundary);
|
||||
return result;
|
||||
}
|
||||
|
||||
function getLocationOrientation(rootOrientation: Orientation, location: GridLocation): Orientation {
|
||||
return location.length % 2 === 0 ? orthogonal(rootOrientation) : rootOrientation;
|
||||
}
|
||||
|
||||
function getDirectionOrientation(direction: Direction): Orientation {
|
||||
return direction === Direction.Up || direction === Direction.Down ? Orientation.VERTICAL : Orientation.HORIZONTAL;
|
||||
}
|
||||
|
||||
export function getRelativeLocation(rootOrientation: Orientation, location: GridLocation, direction: Direction): GridLocation {
|
||||
const orientation = getLocationOrientation(rootOrientation, location);
|
||||
const directionOrientation = getDirectionOrientation(direction);
|
||||
|
||||
if (orientation === directionOrientation) {
|
||||
let [rest, index] = tail(location);
|
||||
|
||||
if (direction === Direction.Right || direction === Direction.Down) {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return [...rest, index];
|
||||
} else {
|
||||
const index = (direction === Direction.Right || direction === Direction.Down) ? 1 : 0;
|
||||
return [...location, index];
|
||||
}
|
||||
}
|
||||
|
||||
function indexInParent(element: HTMLElement): number {
|
||||
const parentElement = element.parentElement;
|
||||
|
||||
if (!parentElement) {
|
||||
throw new Error('Invalid grid element');
|
||||
}
|
||||
|
||||
let el = parentElement.firstElementChild;
|
||||
let index = 0;
|
||||
|
||||
while (el !== element && el !== parentElement.lastElementChild && el) {
|
||||
el = el.nextElementSibling;
|
||||
index++;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the grid location of a specific DOM element by traversing the parent
|
||||
* chain and finding each child index on the way.
|
||||
*
|
||||
* This will break as soon as DOM structures of the Splitview or Gridview change.
|
||||
*/
|
||||
function getGridLocation(element: HTMLElement): GridLocation {
|
||||
const parentElement = element.parentElement;
|
||||
|
||||
if (!parentElement) {
|
||||
throw new Error('Invalid grid element');
|
||||
}
|
||||
|
||||
if (/\bmonaco-grid-view\b/.test(parentElement.className)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const index = indexInParent(parentElement);
|
||||
const ancestor = parentElement.parentElement!.parentElement!.parentElement!.parentElement!;
|
||||
return [...getGridLocation(ancestor), index];
|
||||
}
|
||||
|
||||
export type DistributeSizing = { type: 'distribute' };
|
||||
export type SplitSizing = { type: 'split' };
|
||||
export type AutoSizing = { type: 'auto' };
|
||||
export type InvisibleSizing = { type: 'invisible'; cachedVisibleSize: number };
|
||||
export type Sizing = DistributeSizing | SplitSizing | AutoSizing | InvisibleSizing;
|
||||
|
||||
export namespace Sizing {
|
||||
export const Distribute: DistributeSizing = { type: 'distribute' };
|
||||
export const Split: SplitSizing = { type: 'split' };
|
||||
export const Auto: AutoSizing = { type: 'auto' };
|
||||
export function Invisible(cachedVisibleSize: number): InvisibleSizing { return { type: 'invisible', cachedVisibleSize }; }
|
||||
}
|
||||
|
||||
export interface IGridStyles extends IGridViewStyles { }
|
||||
export interface IGridOptions extends IGridViewOptions { }
|
||||
|
||||
/**
|
||||
* The {@link Grid} exposes a Grid widget in a friendlier API than the underlying
|
||||
* {@link GridView} widget. Namely, all mutation operations are addressed by the
|
||||
* model elements, rather than indexes.
|
||||
*
|
||||
* It support the same features as the {@link GridView}.
|
||||
*/
|
||||
export class Grid<T extends IView = IView> extends Disposable {
|
||||
|
||||
protected gridview: GridView;
|
||||
private views = new Map<T, HTMLElement>();
|
||||
|
||||
/**
|
||||
* The orientation of the grid. Matches the orientation of the root
|
||||
* {@link SplitView} in the grid's {@link GridLocation} model.
|
||||
*/
|
||||
get orientation(): Orientation { return this.gridview.orientation; }
|
||||
set orientation(orientation: Orientation) { this.gridview.orientation = orientation; }
|
||||
|
||||
/**
|
||||
* The width of the grid.
|
||||
*/
|
||||
get width(): number { return this.gridview.width; }
|
||||
|
||||
/**
|
||||
* The height of the grid.
|
||||
*/
|
||||
get height(): number { return this.gridview.height; }
|
||||
|
||||
/**
|
||||
* The minimum width of the grid.
|
||||
*/
|
||||
get minimumWidth(): number { return this.gridview.minimumWidth; }
|
||||
|
||||
/**
|
||||
* The minimum height of the grid.
|
||||
*/
|
||||
get minimumHeight(): number { return this.gridview.minimumHeight; }
|
||||
|
||||
/**
|
||||
* The maximum width of the grid.
|
||||
*/
|
||||
get maximumWidth(): number { return this.gridview.maximumWidth; }
|
||||
|
||||
/**
|
||||
* The maximum height of the grid.
|
||||
*/
|
||||
get maximumHeight(): number { return this.gridview.maximumHeight; }
|
||||
|
||||
/**
|
||||
* Fires whenever a view within the grid changes its size constraints.
|
||||
*/
|
||||
readonly onDidChange: Event<{ width: number; height: number } | undefined>;
|
||||
|
||||
/**
|
||||
* Fires whenever the user scrolls a {@link SplitView} within
|
||||
* the grid.
|
||||
*/
|
||||
readonly onDidScroll: Event<void>;
|
||||
|
||||
/**
|
||||
* A collection of sashes perpendicular to each edge of the grid.
|
||||
* Corner sashes will be created for each intersection.
|
||||
*/
|
||||
get boundarySashes(): IBoundarySashes { return this.gridview.boundarySashes; }
|
||||
set boundarySashes(boundarySashes: IBoundarySashes) { this.gridview.boundarySashes = boundarySashes; }
|
||||
|
||||
/**
|
||||
* Enable/disable edge snapping across all grid views.
|
||||
*/
|
||||
set edgeSnapping(edgeSnapping: boolean) { this.gridview.edgeSnapping = edgeSnapping; }
|
||||
|
||||
/**
|
||||
* The DOM element for this view.
|
||||
*/
|
||||
get element(): HTMLElement { return this.gridview.element; }
|
||||
|
||||
private didLayout = false;
|
||||
|
||||
readonly onDidChangeViewMaximized: Event<boolean>;
|
||||
/**
|
||||
* Create a new {@link Grid}. A grid must *always* have a view
|
||||
* inside.
|
||||
*
|
||||
* @param view An initial view for this Grid.
|
||||
*/
|
||||
constructor(view: T | GridView, options: IGridOptions = {}) {
|
||||
super();
|
||||
|
||||
if (view instanceof GridView) {
|
||||
this.gridview = view;
|
||||
this.gridview.getViewMap(this.views);
|
||||
} else {
|
||||
this.gridview = new GridView(options);
|
||||
}
|
||||
|
||||
this._register(this.gridview);
|
||||
this._register(this.gridview.onDidSashReset(this.onDidSashReset, this));
|
||||
|
||||
if (!(view instanceof GridView)) {
|
||||
this._addView(view, 0, [0]);
|
||||
}
|
||||
|
||||
this.onDidChange = this.gridview.onDidChange;
|
||||
this.onDidScroll = this.gridview.onDidScroll;
|
||||
this.onDidChangeViewMaximized = this.gridview.onDidChangeViewMaximized;
|
||||
}
|
||||
|
||||
style(styles: IGridStyles): void {
|
||||
this.gridview.style(styles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout the {@link Grid}.
|
||||
*
|
||||
* Optionally provide a `top` and `left` positions, those will propagate
|
||||
* as an origin for positions passed to {@link IView.layout}.
|
||||
*
|
||||
* @param width The width of the {@link Grid}.
|
||||
* @param height The height of the {@link Grid}.
|
||||
* @param top Optional, the top location of the {@link Grid}.
|
||||
* @param left Optional, the left location of the {@link Grid}.
|
||||
*/
|
||||
layout(width: number, height: number, top: number = 0, left: number = 0): void {
|
||||
this.gridview.layout(width, height, top, left);
|
||||
this.didLayout = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a {@link IView view} to this {@link Grid}, based on another reference view.
|
||||
*
|
||||
* Take this grid as an example:
|
||||
*
|
||||
* ```
|
||||
* +-----+---------------+
|
||||
* | A | B |
|
||||
* +-----+---------+-----+
|
||||
* | C | |
|
||||
* +---------------+ D |
|
||||
* | E | |
|
||||
* +---------------+-----+
|
||||
* ```
|
||||
*
|
||||
* Calling `addView(X, Sizing.Distribute, C, Direction.Right)` will make the following
|
||||
* changes:
|
||||
*
|
||||
* ```
|
||||
* +-----+---------------+
|
||||
* | A | B |
|
||||
* +-----+-+-------+-----+
|
||||
* | C | X | |
|
||||
* +-------+-------+ D |
|
||||
* | E | |
|
||||
* +---------------+-----+
|
||||
* ```
|
||||
*
|
||||
* Or `addView(X, Sizing.Distribute, D, Direction.Down)`:
|
||||
*
|
||||
* ```
|
||||
* +-----+---------------+
|
||||
* | A | B |
|
||||
* +-----+---------+-----+
|
||||
* | C | D |
|
||||
* +---------------+-----+
|
||||
* | E | X |
|
||||
* +---------------+-----+
|
||||
* ```
|
||||
*
|
||||
* @param newView The view to add.
|
||||
* @param size Either a fixed size, or a dynamic {@link Sizing} strategy.
|
||||
* @param referenceView Another view to place this new view next to.
|
||||
* @param direction The direction the new view should be placed next to the reference view.
|
||||
*/
|
||||
addView(newView: T, size: number | Sizing, referenceView: T, direction: Direction): void {
|
||||
if (this.views.has(newView)) {
|
||||
throw new Error('Can\'t add same view twice');
|
||||
}
|
||||
|
||||
const orientation = getDirectionOrientation(direction);
|
||||
|
||||
if (this.views.size === 1 && this.orientation !== orientation) {
|
||||
this.orientation = orientation;
|
||||
}
|
||||
|
||||
const referenceLocation = this.getViewLocation(referenceView);
|
||||
const location = getRelativeLocation(this.gridview.orientation, referenceLocation, direction);
|
||||
|
||||
let viewSize: number | GridViewSizing;
|
||||
|
||||
if (typeof size === 'number') {
|
||||
viewSize = size;
|
||||
} else if (size.type === 'split') {
|
||||
const [, index] = tail(referenceLocation);
|
||||
viewSize = GridViewSizing.Split(index);
|
||||
} else if (size.type === 'distribute') {
|
||||
viewSize = GridViewSizing.Distribute;
|
||||
} else if (size.type === 'auto') {
|
||||
const [, index] = tail(referenceLocation);
|
||||
viewSize = GridViewSizing.Auto(index);
|
||||
} else {
|
||||
viewSize = size;
|
||||
}
|
||||
|
||||
this._addView(newView, viewSize, location);
|
||||
}
|
||||
|
||||
private addViewAt(newView: T, size: number | DistributeSizing | InvisibleSizing, location: GridLocation): void {
|
||||
if (this.views.has(newView)) {
|
||||
throw new Error('Can\'t add same view twice');
|
||||
}
|
||||
|
||||
let viewSize: number | GridViewSizing;
|
||||
|
||||
if (typeof size === 'number') {
|
||||
viewSize = size;
|
||||
} else if (size.type === 'distribute') {
|
||||
viewSize = GridViewSizing.Distribute;
|
||||
} else {
|
||||
viewSize = size;
|
||||
}
|
||||
|
||||
this._addView(newView, viewSize, location);
|
||||
}
|
||||
|
||||
protected _addView(newView: T, size: number | GridViewSizing, location: GridLocation): void {
|
||||
this.views.set(newView, newView.element);
|
||||
this.gridview.addView(newView, size, location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a {@link IView view} from this {@link Grid}.
|
||||
*
|
||||
* @param view The {@link IView view} to remove.
|
||||
* @param sizing Whether to distribute other {@link IView view}'s sizes.
|
||||
*/
|
||||
removeView(view: T, sizing?: Sizing): void {
|
||||
if (this.views.size === 1) {
|
||||
throw new Error('Can\'t remove last view');
|
||||
}
|
||||
|
||||
const location = this.getViewLocation(view);
|
||||
|
||||
let gridViewSizing: DistributeSizing | SplitViewAutoSizing | undefined;
|
||||
|
||||
if (sizing?.type === 'distribute') {
|
||||
gridViewSizing = GridViewSizing.Distribute;
|
||||
} else if (sizing?.type === 'auto') {
|
||||
const index = location[location.length - 1];
|
||||
gridViewSizing = GridViewSizing.Auto(index === 0 ? 1 : index - 1);
|
||||
}
|
||||
|
||||
this.gridview.removeView(location, gridViewSizing);
|
||||
this.views.delete(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a {@link IView view} to another location in the grid.
|
||||
*
|
||||
* @remarks See {@link Grid.addView}.
|
||||
*
|
||||
* @param view The {@link IView view} to move.
|
||||
* @param sizing Either a fixed size, or a dynamic {@link Sizing} strategy.
|
||||
* @param referenceView Another view to place the view next to.
|
||||
* @param direction The direction the view should be placed next to the reference view.
|
||||
*/
|
||||
moveView(view: T, sizing: number | Sizing, referenceView: T, direction: Direction): void {
|
||||
const sourceLocation = this.getViewLocation(view);
|
||||
const [sourceParentLocation, from] = tail(sourceLocation);
|
||||
|
||||
const referenceLocation = this.getViewLocation(referenceView);
|
||||
const targetLocation = getRelativeLocation(this.gridview.orientation, referenceLocation, direction);
|
||||
const [targetParentLocation, to] = tail(targetLocation);
|
||||
|
||||
if (equals(sourceParentLocation, targetParentLocation)) {
|
||||
this.gridview.moveView(sourceParentLocation, from, to);
|
||||
} else {
|
||||
this.removeView(view, typeof sizing === 'number' ? undefined : sizing);
|
||||
this.addView(view, sizing, referenceView, direction);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a {@link IView view} to another location in the grid.
|
||||
*
|
||||
* @remarks Internal method, do not use without knowing what you're doing.
|
||||
* @remarks See {@link GridView.moveView}.
|
||||
*
|
||||
* @param view The {@link IView view} to move.
|
||||
* @param location The {@link GridLocation location} to insert the view on.
|
||||
*/
|
||||
moveViewTo(view: T, location: GridLocation): void {
|
||||
const sourceLocation = this.getViewLocation(view);
|
||||
const [sourceParentLocation, from] = tail(sourceLocation);
|
||||
const [targetParentLocation, to] = tail(location);
|
||||
|
||||
if (equals(sourceParentLocation, targetParentLocation)) {
|
||||
this.gridview.moveView(sourceParentLocation, from, to);
|
||||
} else {
|
||||
const size = this.getViewSize(view);
|
||||
const orientation = getLocationOrientation(this.gridview.orientation, sourceLocation);
|
||||
const cachedViewSize = this.getViewCachedVisibleSize(view);
|
||||
const sizing = typeof cachedViewSize === 'undefined'
|
||||
? (orientation === Orientation.HORIZONTAL ? size.width : size.height)
|
||||
: Sizing.Invisible(cachedViewSize);
|
||||
|
||||
this.removeView(view);
|
||||
this.addViewAt(view, sizing, location);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap two {@link IView views} within the {@link Grid}.
|
||||
*
|
||||
* @param from One {@link IView view}.
|
||||
* @param to Another {@link IView view}.
|
||||
*/
|
||||
swapViews(from: T, to: T): void {
|
||||
const fromLocation = this.getViewLocation(from);
|
||||
const toLocation = this.getViewLocation(to);
|
||||
return this.gridview.swapViews(fromLocation, toLocation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize a {@link IView view}.
|
||||
*
|
||||
* @param view The {@link IView view} to resize.
|
||||
* @param size The size the view should be.
|
||||
*/
|
||||
resizeView(view: T, size: IViewSize): void {
|
||||
const location = this.getViewLocation(view);
|
||||
return this.gridview.resizeView(location, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether all other {@link IView views} are at their minimum size.
|
||||
*
|
||||
* @param view The reference {@link IView view}.
|
||||
*/
|
||||
isViewExpanded(view: T): boolean {
|
||||
const location = this.getViewLocation(view);
|
||||
return this.gridview.isViewExpanded(location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the {@link IView view} is maximized.
|
||||
*
|
||||
* @param view The reference {@link IView view}.
|
||||
*/
|
||||
isViewMaximized(view: T): boolean {
|
||||
const location = this.getViewLocation(view);
|
||||
return this.gridview.isViewMaximized(location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the {@link IView view} is maximized.
|
||||
*
|
||||
* @param view The reference {@link IView view}.
|
||||
*/
|
||||
hasMaximizedView(): boolean {
|
||||
return this.gridview.hasMaximizedView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of a {@link IView view}.
|
||||
*
|
||||
* @param view The {@link IView view}. Provide `undefined` to get the size
|
||||
* of the grid itself.
|
||||
*/
|
||||
getViewSize(view?: T): IViewSize {
|
||||
if (!view) {
|
||||
return this.gridview.getViewSize();
|
||||
}
|
||||
|
||||
const location = this.getViewLocation(view);
|
||||
return this.gridview.getViewSize(location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cached visible size of a {@link IView view}. This was the size
|
||||
* of the view at the moment it last became hidden.
|
||||
*
|
||||
* @param view The {@link IView view}.
|
||||
*/
|
||||
getViewCachedVisibleSize(view: T): number | undefined {
|
||||
const location = this.getViewLocation(view);
|
||||
return this.gridview.getViewCachedVisibleSize(location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximizes the specified view and hides all other views.
|
||||
* @param view The view to maximize.
|
||||
*/
|
||||
maximizeView(view: T) {
|
||||
if (this.views.size < 2) {
|
||||
throw new Error('At least two views are required to maximize a view');
|
||||
}
|
||||
const location = this.getViewLocation(view);
|
||||
this.gridview.maximizeView(location);
|
||||
}
|
||||
|
||||
exitMaximizedView(): void {
|
||||
this.gridview.exitMaximizedView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand the size of a {@link IView view} by collapsing all other views
|
||||
* to their minimum sizes.
|
||||
*
|
||||
* @param view The {@link IView view}.
|
||||
*/
|
||||
expandView(view: T): void {
|
||||
const location = this.getViewLocation(view);
|
||||
this.gridview.expandView(location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Distribute the size among all {@link IView views} within the entire
|
||||
* grid or within a single {@link SplitView}.
|
||||
*/
|
||||
distributeViewSizes(): void {
|
||||
this.gridview.distributeViewSizes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a {@link IView view} is visible.
|
||||
*
|
||||
* @param view The {@link IView view}.
|
||||
*/
|
||||
isViewVisible(view: T): boolean {
|
||||
const location = this.getViewLocation(view);
|
||||
return this.gridview.isViewVisible(location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the visibility state of a {@link IView view}.
|
||||
*
|
||||
* @param view The {@link IView view}.
|
||||
*/
|
||||
setViewVisible(view: T, visible: boolean): void {
|
||||
const location = this.getViewLocation(view);
|
||||
this.gridview.setViewVisible(location, visible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a descriptor for the entire grid.
|
||||
*/
|
||||
getViews(): GridBranchNode<T> {
|
||||
return this.gridview.getView() as GridBranchNode<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to return the collection all views which intersect
|
||||
* a view's edge.
|
||||
*
|
||||
* @param view The {@link IView view}.
|
||||
* @param direction Which direction edge to be considered.
|
||||
* @param wrap Whether the grid wraps around (from right to left, from bottom to top).
|
||||
*/
|
||||
getNeighborViews(view: T, direction: Direction, wrap: boolean = false): T[] {
|
||||
if (!this.didLayout) {
|
||||
throw new Error('Can\'t call getNeighborViews before first layout');
|
||||
}
|
||||
|
||||
const location = this.getViewLocation(view);
|
||||
const root = this.getViews();
|
||||
const node = getGridNode(root, location);
|
||||
let boundary = getBoxBoundary(node.box, direction);
|
||||
|
||||
if (wrap) {
|
||||
if (direction === Direction.Up && node.box.top === 0) {
|
||||
boundary = { offset: root.box.top + root.box.height, range: boundary.range };
|
||||
} else if (direction === Direction.Right && node.box.left + node.box.width === root.box.width) {
|
||||
boundary = { offset: 0, range: boundary.range };
|
||||
} else if (direction === Direction.Down && node.box.top + node.box.height === root.box.height) {
|
||||
boundary = { offset: 0, range: boundary.range };
|
||||
} else if (direction === Direction.Left && node.box.left === 0) {
|
||||
boundary = { offset: root.box.left + root.box.width, range: boundary.range };
|
||||
}
|
||||
}
|
||||
|
||||
return findAdjacentBoxLeafNodes(root, oppositeDirection(direction), boundary)
|
||||
.map(node => node.view);
|
||||
}
|
||||
|
||||
private getViewLocation(view: T): GridLocation {
|
||||
const element = this.views.get(view);
|
||||
|
||||
if (!element) {
|
||||
throw new Error('View not found');
|
||||
}
|
||||
|
||||
return getGridLocation(element);
|
||||
}
|
||||
|
||||
private onDidSashReset(location: GridLocation): void {
|
||||
const resizeToPreferredSize = (location: GridLocation): boolean => {
|
||||
const node = this.gridview.getView(location) as GridNode<T>;
|
||||
|
||||
if (isGridBranchNode(node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const direction = getLocationOrientation(this.orientation, location);
|
||||
const size = direction === Orientation.HORIZONTAL ? node.view.preferredWidth : node.view.preferredHeight;
|
||||
|
||||
if (typeof size !== 'number') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const viewSize = direction === Orientation.HORIZONTAL ? { width: Math.round(size) } : { height: Math.round(size) };
|
||||
this.gridview.resizeView(location, viewSize);
|
||||
return true;
|
||||
};
|
||||
|
||||
if (resizeToPreferredSize(location)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [parentLocation, index] = tail(location);
|
||||
|
||||
if (resizeToPreferredSize([...parentLocation, index + 1])) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.gridview.distributeViewSizes(parentLocation);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ISerializableView extends IView {
|
||||
toJSON(): object;
|
||||
}
|
||||
|
||||
export interface IViewDeserializer<T extends ISerializableView> {
|
||||
fromJSON(json: any): T;
|
||||
}
|
||||
|
||||
export interface ISerializedLeafNode {
|
||||
type: 'leaf';
|
||||
data: any;
|
||||
size: number;
|
||||
visible?: boolean;
|
||||
maximized?: boolean;
|
||||
}
|
||||
|
||||
export interface ISerializedBranchNode {
|
||||
type: 'branch';
|
||||
data: ISerializedNode[];
|
||||
size: number;
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
export type ISerializedNode = ISerializedLeafNode | ISerializedBranchNode;
|
||||
|
||||
export interface ISerializedGrid {
|
||||
root: ISerializedNode;
|
||||
orientation: Orientation;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Grid} which can serialize itself.
|
||||
*/
|
||||
export class SerializableGrid<T extends ISerializableView> extends Grid<T> {
|
||||
|
||||
private static serializeNode<T extends ISerializableView>(node: GridNode<T>, orientation: Orientation): ISerializedNode {
|
||||
const size = orientation === Orientation.VERTICAL ? node.box.width : node.box.height;
|
||||
|
||||
if (!isGridBranchNode(node)) {
|
||||
const serializedLeafNode: ISerializedLeafNode = { type: 'leaf', data: node.view.toJSON(), size };
|
||||
|
||||
if (typeof node.cachedVisibleSize === 'number') {
|
||||
serializedLeafNode.size = node.cachedVisibleSize;
|
||||
serializedLeafNode.visible = false;
|
||||
} else if (node.maximized) {
|
||||
serializedLeafNode.maximized = true;
|
||||
}
|
||||
|
||||
return serializedLeafNode;
|
||||
}
|
||||
|
||||
const data = node.children.map(c => SerializableGrid.serializeNode(c, orthogonal(orientation)));
|
||||
if (data.some(c => c.visible !== false)) {
|
||||
return { type: 'branch', data: data, size };
|
||||
}
|
||||
return { type: 'branch', data: data, size, visible: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new {@link SerializableGrid} from a JSON object.
|
||||
*
|
||||
* @param json The JSON object.
|
||||
* @param deserializer A deserializer which can revive each view.
|
||||
* @returns A new {@link SerializableGrid} instance.
|
||||
*/
|
||||
static deserialize<T extends ISerializableView>(json: ISerializedGrid, deserializer: IViewDeserializer<T>, options: IGridOptions = {}): SerializableGrid<T> {
|
||||
if (typeof json.orientation !== 'number') {
|
||||
throw new Error('Invalid JSON: \'orientation\' property must be a number.');
|
||||
} else if (typeof json.width !== 'number') {
|
||||
throw new Error('Invalid JSON: \'width\' property must be a number.');
|
||||
} else if (typeof json.height !== 'number') {
|
||||
throw new Error('Invalid JSON: \'height\' property must be a number.');
|
||||
}
|
||||
|
||||
const gridview = GridView.deserialize(json, deserializer, options);
|
||||
const result = new SerializableGrid<T>(gridview, options);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new {@link SerializableGrid} from a grid descriptor.
|
||||
*
|
||||
* @param gridDescriptor A grid descriptor in which leaf nodes point to actual views.
|
||||
* @returns A new {@link SerializableGrid} instance.
|
||||
*/
|
||||
static from<T extends ISerializableView>(gridDescriptor: GridDescriptor<T>, options: IGridOptions = {}): SerializableGrid<T> {
|
||||
return SerializableGrid.deserialize(createSerializedGrid(gridDescriptor), { fromJSON: view => view }, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Useful information in order to proportionally restore view sizes
|
||||
* upon the very first layout call.
|
||||
*/
|
||||
private initialLayoutContext: boolean = true;
|
||||
|
||||
/**
|
||||
* Serialize this grid into a JSON object.
|
||||
*/
|
||||
serialize(): ISerializedGrid {
|
||||
return {
|
||||
root: SerializableGrid.serializeNode(this.getViews(), this.orientation),
|
||||
orientation: this.orientation,
|
||||
width: this.width,
|
||||
height: this.height
|
||||
};
|
||||
}
|
||||
|
||||
override layout(width: number, height: number, top: number = 0, left: number = 0): void {
|
||||
super.layout(width, height, top, left);
|
||||
|
||||
if (this.initialLayoutContext) {
|
||||
this.initialLayoutContext = false;
|
||||
this.gridview.trySet2x2();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type GridLeafNodeDescriptor<T> = { size?: number; data?: any };
|
||||
export type GridBranchNodeDescriptor<T> = { size?: number; groups: GridNodeDescriptor<T>[] };
|
||||
export type GridNodeDescriptor<T> = GridBranchNodeDescriptor<T> | GridLeafNodeDescriptor<T>;
|
||||
export type GridDescriptor<T> = { orientation: Orientation } & GridBranchNodeDescriptor<T>;
|
||||
|
||||
function isGridBranchNodeDescriptor<T>(nodeDescriptor: GridNodeDescriptor<T>): nodeDescriptor is GridBranchNodeDescriptor<T> {
|
||||
return !!(nodeDescriptor as GridBranchNodeDescriptor<T>).groups;
|
||||
}
|
||||
|
||||
export function sanitizeGridNodeDescriptor<T>(nodeDescriptor: GridNodeDescriptor<T>, rootNode: boolean): void {
|
||||
if (!rootNode && (nodeDescriptor as any).groups && (nodeDescriptor as any).groups.length <= 1) {
|
||||
(nodeDescriptor as any).groups = undefined;
|
||||
}
|
||||
|
||||
if (!isGridBranchNodeDescriptor(nodeDescriptor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let totalDefinedSize = 0;
|
||||
let totalDefinedSizeCount = 0;
|
||||
|
||||
for (const child of nodeDescriptor.groups) {
|
||||
sanitizeGridNodeDescriptor(child, false);
|
||||
|
||||
if (child.size) {
|
||||
totalDefinedSize += child.size;
|
||||
totalDefinedSizeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const totalUndefinedSize = totalDefinedSizeCount > 0 ? totalDefinedSize : 1;
|
||||
const totalUndefinedSizeCount = nodeDescriptor.groups.length - totalDefinedSizeCount;
|
||||
const eachUndefinedSize = totalUndefinedSize / totalUndefinedSizeCount;
|
||||
|
||||
for (const child of nodeDescriptor.groups) {
|
||||
if (!child.size) {
|
||||
child.size = eachUndefinedSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createSerializedNode<T>(nodeDescriptor: GridNodeDescriptor<T>): ISerializedNode {
|
||||
if (isGridBranchNodeDescriptor(nodeDescriptor)) {
|
||||
return { type: 'branch', data: nodeDescriptor.groups.map(c => createSerializedNode(c)), size: nodeDescriptor.size! };
|
||||
} else {
|
||||
return { type: 'leaf', data: nodeDescriptor.data, size: nodeDescriptor.size! };
|
||||
}
|
||||
}
|
||||
|
||||
function getDimensions(node: ISerializedNode, orientation: Orientation): { width?: number; height?: number } {
|
||||
if (node.type === 'branch') {
|
||||
const childrenDimensions = node.data.map(c => getDimensions(c, orthogonal(orientation)));
|
||||
|
||||
if (orientation === Orientation.VERTICAL) {
|
||||
const width = node.size || (childrenDimensions.length === 0 ? undefined : Math.max(...childrenDimensions.map(d => d.width || 0)));
|
||||
const height = childrenDimensions.length === 0 ? undefined : childrenDimensions.reduce((r, d) => r + (d.height || 0), 0);
|
||||
return { width, height };
|
||||
} else {
|
||||
const width = childrenDimensions.length === 0 ? undefined : childrenDimensions.reduce((r, d) => r + (d.width || 0), 0);
|
||||
const height = node.size || (childrenDimensions.length === 0 ? undefined : Math.max(...childrenDimensions.map(d => d.height || 0)));
|
||||
return { width, height };
|
||||
}
|
||||
} else {
|
||||
const width = orientation === Orientation.VERTICAL ? node.size : undefined;
|
||||
const height = orientation === Orientation.VERTICAL ? undefined : node.size;
|
||||
return { width, height };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new JSON object from a {@link GridDescriptor}, which can
|
||||
* be deserialized by {@link SerializableGrid.deserialize}.
|
||||
*/
|
||||
export function createSerializedGrid<T>(gridDescriptor: GridDescriptor<T>): ISerializedGrid {
|
||||
sanitizeGridNodeDescriptor(gridDescriptor, true);
|
||||
|
||||
const root = createSerializedNode(gridDescriptor);
|
||||
const { width, height } = getDimensions(root, gridDescriptor.orientation);
|
||||
|
||||
return {
|
||||
root,
|
||||
orientation: gridDescriptor.orientation,
|
||||
width: width || 1,
|
||||
height: height || 1
|
||||
};
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-grid-view {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-grid-branch-node {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-mouse-cursor-text {
|
||||
cursor: text;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// import 'vs/css!./mouseCursor';
|
||||
|
||||
export const MOUSE_CURSOR_TEXT_CSS_CLASS_NAME = `monaco-mouse-cursor-text`;
|
||||
@@ -1,23 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export interface IScopedAccessibilityProgressSignalDelegate extends IDisposable { }
|
||||
|
||||
const nullScopedAccessibilityProgressSignalFactory = () => ({
|
||||
msLoopTime: -1,
|
||||
msDelayTime: -1,
|
||||
dispose: () => { },
|
||||
});
|
||||
let progressAccessibilitySignalSchedulerFactory: (msDelayTime: number, msLoopTime?: number) => IScopedAccessibilityProgressSignalDelegate = nullScopedAccessibilityProgressSignalFactory;
|
||||
|
||||
export function setProgressAcccessibilitySignalScheduler(progressAccessibilitySignalScheduler: (msDelayTime: number, msLoopTime?: number) => IScopedAccessibilityProgressSignalDelegate) {
|
||||
progressAccessibilitySignalSchedulerFactory = progressAccessibilitySignalScheduler;
|
||||
}
|
||||
|
||||
export function getProgressAcccessibilitySignalScheduler(msDelayTime: number, msLoopTime?: number): IScopedAccessibilityProgressSignalDelegate {
|
||||
return progressAccessibilitySignalSchedulerFactory(msDelayTime, msLoopTime);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-progress-container {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
overflow: hidden; /* keep progress bit in bounds */
|
||||
}
|
||||
|
||||
.monaco-progress-container .progress-bit {
|
||||
width: 2%;
|
||||
height: 2px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-progress-container.active .progress-bit {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
.monaco-progress-container.discrete .progress-bit {
|
||||
left: 0;
|
||||
transition: width 100ms linear;
|
||||
}
|
||||
|
||||
.monaco-progress-container.discrete.done .progress-bit {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monaco-progress-container.infinite .progress-bit {
|
||||
animation-name: progress;
|
||||
animation-duration: 4s;
|
||||
animation-iteration-count: infinite;
|
||||
transform: translate3d(0px, 0px, 0px);
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
|
||||
.monaco-progress-container.infinite.infinite-long-running .progress-bit {
|
||||
/*
|
||||
The more smooth `linear` timing function can cause
|
||||
higher GPU consumption as indicated in
|
||||
https://github.com/microsoft/vscode/issues/97900 &
|
||||
https://github.com/microsoft/vscode/issues/138396
|
||||
*/
|
||||
animation-timing-function: steps(100);
|
||||
}
|
||||
|
||||
/**
|
||||
* The progress bit has a width: 2% (1/50) of the parent container. The animation moves it from 0% to 100% of
|
||||
* that container. Since translateX is relative to the progress bit size, we have to multiple it with
|
||||
* its relative size to the parent container:
|
||||
* parent width: 5000%
|
||||
* bit width: 100%
|
||||
* translateX should be as follow:
|
||||
* 50%: 5000% * 50% - 50% (set to center) = 2450%
|
||||
* 100%: 5000% * 100% - 100% (do not overflow) = 4900%
|
||||
*/
|
||||
@keyframes progress { from { transform: translateX(0%) scaleX(1) } 50% { transform: translateX(2500%) scaleX(3) } to { transform: translateX(4900%) scaleX(1) } }
|
||||
@@ -1,224 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { hide, show } from 'vs/base/browser/dom';
|
||||
import { getProgressAcccessibilitySignalScheduler } from 'vs/base/browser/ui/progressbar/progressAccessibilitySignal';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { Disposable, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||
import { isNumber } from 'vs/base/common/types';
|
||||
// import 'vs/css!./progressbar';
|
||||
|
||||
const CSS_DONE = 'done';
|
||||
const CSS_ACTIVE = 'active';
|
||||
const CSS_INFINITE = 'infinite';
|
||||
const CSS_INFINITE_LONG_RUNNING = 'infinite-long-running';
|
||||
const CSS_DISCRETE = 'discrete';
|
||||
|
||||
export interface IProgressBarOptions extends IProgressBarStyles {
|
||||
}
|
||||
|
||||
export interface IProgressBarStyles {
|
||||
progressBarBackground: string | undefined;
|
||||
}
|
||||
|
||||
export const unthemedProgressBarOptions: IProgressBarOptions = {
|
||||
progressBarBackground: undefined
|
||||
};
|
||||
|
||||
/**
|
||||
* A progress bar with support for infinite or discrete progress.
|
||||
*/
|
||||
export class ProgressBar extends Disposable {
|
||||
|
||||
/**
|
||||
* After a certain time of showing the progress bar, switch
|
||||
* to long-running mode and throttle animations to reduce
|
||||
* the pressure on the GPU process.
|
||||
*
|
||||
* https://github.com/microsoft/vscode/issues/97900
|
||||
* https://github.com/microsoft/vscode/issues/138396
|
||||
*/
|
||||
private static readonly LONG_RUNNING_INFINITE_THRESHOLD = 10000;
|
||||
|
||||
private static readonly PROGRESS_SIGNAL_DEFAULT_DELAY = 3000;
|
||||
|
||||
private workedVal: number;
|
||||
private element!: HTMLElement;
|
||||
private bit!: HTMLElement;
|
||||
private totalWork: number | undefined;
|
||||
private showDelayedScheduler: RunOnceScheduler;
|
||||
private longRunningScheduler: RunOnceScheduler;
|
||||
private readonly progressSignal = this._register(new MutableDisposable<IDisposable>());
|
||||
|
||||
constructor(container: HTMLElement, options?: IProgressBarOptions) {
|
||||
super();
|
||||
|
||||
this.workedVal = 0;
|
||||
|
||||
this.showDelayedScheduler = this._register(new RunOnceScheduler(() => show(this.element), 0));
|
||||
this.longRunningScheduler = this._register(new RunOnceScheduler(() => this.infiniteLongRunning(), ProgressBar.LONG_RUNNING_INFINITE_THRESHOLD));
|
||||
|
||||
this.create(container, options);
|
||||
}
|
||||
|
||||
private create(container: HTMLElement, options?: IProgressBarOptions): void {
|
||||
this.element = document.createElement('div');
|
||||
this.element.classList.add('monaco-progress-container');
|
||||
this.element.setAttribute('role', 'progressbar');
|
||||
this.element.setAttribute('aria-valuemin', '0');
|
||||
container.appendChild(this.element);
|
||||
|
||||
this.bit = document.createElement('div');
|
||||
this.bit.classList.add('progress-bit');
|
||||
this.bit.style.backgroundColor = options?.progressBarBackground || '#0E70C0';
|
||||
this.element.appendChild(this.bit);
|
||||
}
|
||||
|
||||
private off(): void {
|
||||
this.bit.style.width = 'inherit';
|
||||
this.bit.style.opacity = '1';
|
||||
this.element.classList.remove(CSS_ACTIVE, CSS_INFINITE, CSS_INFINITE_LONG_RUNNING, CSS_DISCRETE);
|
||||
|
||||
this.workedVal = 0;
|
||||
this.totalWork = undefined;
|
||||
|
||||
this.longRunningScheduler.cancel();
|
||||
this.progressSignal.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates to the progress bar that all work is done.
|
||||
*/
|
||||
done(): ProgressBar {
|
||||
return this.doDone(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the progressbar from showing any progress instantly without fading out.
|
||||
*/
|
||||
stop(): ProgressBar {
|
||||
return this.doDone(false);
|
||||
}
|
||||
|
||||
private doDone(delayed: boolean): ProgressBar {
|
||||
this.element.classList.add(CSS_DONE);
|
||||
|
||||
// discrete: let it grow to 100% width and hide afterwards
|
||||
if (!this.element.classList.contains(CSS_INFINITE)) {
|
||||
this.bit.style.width = 'inherit';
|
||||
|
||||
if (delayed) {
|
||||
setTimeout(() => this.off(), 200);
|
||||
} else {
|
||||
this.off();
|
||||
}
|
||||
}
|
||||
|
||||
// infinite: let it fade out and hide afterwards
|
||||
else {
|
||||
this.bit.style.opacity = '0';
|
||||
if (delayed) {
|
||||
setTimeout(() => this.off(), 200);
|
||||
} else {
|
||||
this.off();
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this mode to indicate progress that has no total number of work units.
|
||||
*/
|
||||
infinite(): ProgressBar {
|
||||
this.bit.style.width = '2%';
|
||||
this.bit.style.opacity = '1';
|
||||
|
||||
this.element.classList.remove(CSS_DISCRETE, CSS_DONE, CSS_INFINITE_LONG_RUNNING);
|
||||
this.element.classList.add(CSS_ACTIVE, CSS_INFINITE);
|
||||
|
||||
this.longRunningScheduler.schedule();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private infiniteLongRunning(): void {
|
||||
this.element.classList.add(CSS_INFINITE_LONG_RUNNING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the progress bar the total number of work. Use in combination with workedVal() to let
|
||||
* the progress bar show the actual progress based on the work that is done.
|
||||
*/
|
||||
total(value: number): ProgressBar {
|
||||
this.workedVal = 0;
|
||||
this.totalWork = value;
|
||||
this.element.setAttribute('aria-valuemax', value.toString());
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds out if this progress bar is configured with total work
|
||||
*/
|
||||
hasTotal(): boolean {
|
||||
return isNumber(this.totalWork);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the progress bar that an increment of work has been completed.
|
||||
*/
|
||||
worked(value: number): ProgressBar {
|
||||
value = Math.max(1, Number(value));
|
||||
|
||||
return this.doSetWorked(this.workedVal + value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the progress bar the total amount of work that has been completed.
|
||||
*/
|
||||
setWorked(value: number): ProgressBar {
|
||||
value = Math.max(1, Number(value));
|
||||
|
||||
return this.doSetWorked(value);
|
||||
}
|
||||
|
||||
private doSetWorked(value: number): ProgressBar {
|
||||
const totalWork = this.totalWork || 100;
|
||||
|
||||
this.workedVal = value;
|
||||
this.workedVal = Math.min(totalWork, this.workedVal);
|
||||
|
||||
this.element.classList.remove(CSS_INFINITE, CSS_INFINITE_LONG_RUNNING, CSS_DONE);
|
||||
this.element.classList.add(CSS_ACTIVE, CSS_DISCRETE);
|
||||
this.element.setAttribute('aria-valuenow', value.toString());
|
||||
|
||||
this.bit.style.width = 100 * (this.workedVal / (totalWork)) + '%';
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
getContainer(): HTMLElement {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
show(delay?: number): void {
|
||||
this.showDelayedScheduler.cancel();
|
||||
this.progressSignal.value = getProgressAcccessibilitySignalScheduler(ProgressBar.PROGRESS_SIGNAL_DEFAULT_DELAY);
|
||||
|
||||
if (typeof delay === 'number') {
|
||||
this.showDelayedScheduler.schedule(delay);
|
||||
} else {
|
||||
show(this.element);
|
||||
}
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
hide(this.element);
|
||||
|
||||
this.showDelayedScheduler.cancel();
|
||||
this.progressSignal.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Dimension } from 'vs/base/browser/dom';
|
||||
import { Orientation, OrthogonalEdge, Sash, SashState } from 'vs/base/browser/ui/sash/sash';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
|
||||
|
||||
export interface IResizeEvent {
|
||||
dimension: Dimension;
|
||||
done: boolean;
|
||||
north?: boolean;
|
||||
east?: boolean;
|
||||
south?: boolean;
|
||||
west?: boolean;
|
||||
}
|
||||
|
||||
export class ResizableHTMLElement {
|
||||
|
||||
readonly domNode: HTMLElement;
|
||||
|
||||
private readonly _onDidWillResize = new Emitter<void>();
|
||||
readonly onDidWillResize: Event<void> = this._onDidWillResize.event;
|
||||
|
||||
private readonly _onDidResize = new Emitter<IResizeEvent>();
|
||||
readonly onDidResize: Event<IResizeEvent> = this._onDidResize.event;
|
||||
|
||||
private readonly _northSash: Sash;
|
||||
private readonly _eastSash: Sash;
|
||||
private readonly _southSash: Sash;
|
||||
private readonly _westSash: Sash;
|
||||
private readonly _sashListener = new DisposableStore();
|
||||
|
||||
private _size = new Dimension(0, 0);
|
||||
private _minSize = new Dimension(0, 0);
|
||||
private _maxSize = new Dimension(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
|
||||
private _preferredSize?: Dimension;
|
||||
|
||||
constructor() {
|
||||
this.domNode = document.createElement('div');
|
||||
this._eastSash = new Sash(this.domNode, { getVerticalSashLeft: () => this._size.width }, { orientation: Orientation.VERTICAL });
|
||||
this._westSash = new Sash(this.domNode, { getVerticalSashLeft: () => 0 }, { orientation: Orientation.VERTICAL });
|
||||
this._northSash = new Sash(this.domNode, { getHorizontalSashTop: () => 0 }, { orientation: Orientation.HORIZONTAL, orthogonalEdge: OrthogonalEdge.North });
|
||||
this._southSash = new Sash(this.domNode, { getHorizontalSashTop: () => this._size.height }, { orientation: Orientation.HORIZONTAL, orthogonalEdge: OrthogonalEdge.South });
|
||||
|
||||
this._northSash.orthogonalStartSash = this._westSash;
|
||||
this._northSash.orthogonalEndSash = this._eastSash;
|
||||
this._southSash.orthogonalStartSash = this._westSash;
|
||||
this._southSash.orthogonalEndSash = this._eastSash;
|
||||
|
||||
let currentSize: Dimension | undefined;
|
||||
let deltaY = 0;
|
||||
let deltaX = 0;
|
||||
|
||||
this._sashListener.add(Event.any(this._northSash.onDidStart, this._eastSash.onDidStart, this._southSash.onDidStart, this._westSash.onDidStart)(() => {
|
||||
if (currentSize === undefined) {
|
||||
this._onDidWillResize.fire();
|
||||
currentSize = this._size;
|
||||
deltaY = 0;
|
||||
deltaX = 0;
|
||||
}
|
||||
}));
|
||||
this._sashListener.add(Event.any(this._northSash.onDidEnd, this._eastSash.onDidEnd, this._southSash.onDidEnd, this._westSash.onDidEnd)(() => {
|
||||
if (currentSize !== undefined) {
|
||||
currentSize = undefined;
|
||||
deltaY = 0;
|
||||
deltaX = 0;
|
||||
this._onDidResize.fire({ dimension: this._size, done: true });
|
||||
}
|
||||
}));
|
||||
|
||||
this._sashListener.add(this._eastSash.onDidChange(e => {
|
||||
if (currentSize) {
|
||||
deltaX = e.currentX - e.startX;
|
||||
this.layout(currentSize.height + deltaY, currentSize.width + deltaX);
|
||||
this._onDidResize.fire({ dimension: this._size, done: false, east: true });
|
||||
}
|
||||
}));
|
||||
this._sashListener.add(this._westSash.onDidChange(e => {
|
||||
if (currentSize) {
|
||||
deltaX = -(e.currentX - e.startX);
|
||||
this.layout(currentSize.height + deltaY, currentSize.width + deltaX);
|
||||
this._onDidResize.fire({ dimension: this._size, done: false, west: true });
|
||||
}
|
||||
}));
|
||||
this._sashListener.add(this._northSash.onDidChange(e => {
|
||||
if (currentSize) {
|
||||
deltaY = -(e.currentY - e.startY);
|
||||
this.layout(currentSize.height + deltaY, currentSize.width + deltaX);
|
||||
this._onDidResize.fire({ dimension: this._size, done: false, north: true });
|
||||
}
|
||||
}));
|
||||
this._sashListener.add(this._southSash.onDidChange(e => {
|
||||
if (currentSize) {
|
||||
deltaY = e.currentY - e.startY;
|
||||
this.layout(currentSize.height + deltaY, currentSize.width + deltaX);
|
||||
this._onDidResize.fire({ dimension: this._size, done: false, south: true });
|
||||
}
|
||||
}));
|
||||
|
||||
this._sashListener.add(Event.any(this._eastSash.onDidReset, this._westSash.onDidReset)(e => {
|
||||
if (this._preferredSize) {
|
||||
this.layout(this._size.height, this._preferredSize.width);
|
||||
this._onDidResize.fire({ dimension: this._size, done: true });
|
||||
}
|
||||
}));
|
||||
this._sashListener.add(Event.any(this._northSash.onDidReset, this._southSash.onDidReset)(e => {
|
||||
if (this._preferredSize) {
|
||||
this.layout(this._preferredSize.height, this._size.width);
|
||||
this._onDidResize.fire({ dimension: this._size, done: true });
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._northSash.dispose();
|
||||
this._southSash.dispose();
|
||||
this._eastSash.dispose();
|
||||
this._westSash.dispose();
|
||||
this._sashListener.dispose();
|
||||
this._onDidResize.dispose();
|
||||
this._onDidWillResize.dispose();
|
||||
this.domNode.remove();
|
||||
}
|
||||
|
||||
enableSashes(north: boolean, east: boolean, south: boolean, west: boolean): void {
|
||||
this._northSash.state = north ? SashState.Enabled : SashState.Disabled;
|
||||
this._eastSash.state = east ? SashState.Enabled : SashState.Disabled;
|
||||
this._southSash.state = south ? SashState.Enabled : SashState.Disabled;
|
||||
this._westSash.state = west ? SashState.Enabled : SashState.Disabled;
|
||||
}
|
||||
|
||||
layout(height: number = this.size.height, width: number = this.size.width): void {
|
||||
|
||||
const { height: minHeight, width: minWidth } = this._minSize;
|
||||
const { height: maxHeight, width: maxWidth } = this._maxSize;
|
||||
|
||||
height = Math.max(minHeight, Math.min(maxHeight, height));
|
||||
width = Math.max(minWidth, Math.min(maxWidth, width));
|
||||
|
||||
const newSize = new Dimension(width, height);
|
||||
if (!Dimension.equals(newSize, this._size)) {
|
||||
this.domNode.style.height = height + 'px';
|
||||
this.domNode.style.width = width + 'px';
|
||||
this._size = newSize;
|
||||
this._northSash.layout();
|
||||
this._eastSash.layout();
|
||||
this._southSash.layout();
|
||||
this._westSash.layout();
|
||||
}
|
||||
}
|
||||
|
||||
clearSashHoverState(): void {
|
||||
this._eastSash.clearSashHoverState();
|
||||
this._westSash.clearSashHoverState();
|
||||
this._northSash.clearSashHoverState();
|
||||
this._southSash.clearSashHoverState();
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
set maxSize(value: Dimension) {
|
||||
this._maxSize = value;
|
||||
}
|
||||
|
||||
get maxSize() {
|
||||
return this._maxSize;
|
||||
}
|
||||
|
||||
set minSize(value: Dimension) {
|
||||
this._minSize = value;
|
||||
}
|
||||
|
||||
get minSize() {
|
||||
return this._minSize;
|
||||
}
|
||||
|
||||
set preferredSize(value: Dimension | undefined) {
|
||||
this._preferredSize = value;
|
||||
}
|
||||
|
||||
get preferredSize() {
|
||||
return this._preferredSize;
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
:root {
|
||||
--vscode-sash-size: 4px;
|
||||
--vscode-sash-hover-size: 4px;
|
||||
}
|
||||
|
||||
.monaco-sash {
|
||||
position: absolute;
|
||||
z-index: 35;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.monaco-sash.disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.monaco-sash.mac.vertical {
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.vertical.minimum {
|
||||
cursor: e-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.vertical.maximum {
|
||||
cursor: w-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.mac.horizontal {
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.horizontal.minimum {
|
||||
cursor: s-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.horizontal.maximum {
|
||||
cursor: n-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.disabled {
|
||||
cursor: default !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.monaco-sash.vertical {
|
||||
cursor: ew-resize;
|
||||
top: 0;
|
||||
width: var(--vscode-sash-size);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-sash.horizontal {
|
||||
cursor: ns-resize;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: var(--vscode-sash-size);
|
||||
}
|
||||
|
||||
.monaco-sash:not(.disabled) > .orthogonal-drag-handle {
|
||||
content: " ";
|
||||
height: calc(var(--vscode-sash-size) * 2);
|
||||
width: calc(var(--vscode-sash-size) * 2);
|
||||
z-index: 100;
|
||||
display: block;
|
||||
cursor: all-scroll;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.monaco-sash.horizontal.orthogonal-edge-north:not(.disabled)
|
||||
> .orthogonal-drag-handle.start,
|
||||
.monaco-sash.horizontal.orthogonal-edge-south:not(.disabled)
|
||||
> .orthogonal-drag-handle.end {
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.horizontal.orthogonal-edge-north:not(.disabled)
|
||||
> .orthogonal-drag-handle.end,
|
||||
.monaco-sash.horizontal.orthogonal-edge-south:not(.disabled)
|
||||
> .orthogonal-drag-handle.start {
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.vertical > .orthogonal-drag-handle.start {
|
||||
left: calc(var(--vscode-sash-size) * -0.5);
|
||||
top: calc(var(--vscode-sash-size) * -1);
|
||||
}
|
||||
.monaco-sash.vertical > .orthogonal-drag-handle.end {
|
||||
left: calc(var(--vscode-sash-size) * -0.5);
|
||||
bottom: calc(var(--vscode-sash-size) * -1);
|
||||
}
|
||||
.monaco-sash.horizontal > .orthogonal-drag-handle.start {
|
||||
top: calc(var(--vscode-sash-size) * -0.5);
|
||||
left: calc(var(--vscode-sash-size) * -1);
|
||||
}
|
||||
.monaco-sash.horizontal > .orthogonal-drag-handle.end {
|
||||
top: calc(var(--vscode-sash-size) * -0.5);
|
||||
right: calc(var(--vscode-sash-size) * -1);
|
||||
}
|
||||
|
||||
.monaco-sash:before {
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.monaco-workbench:not(.reduce-motion) .monaco-sash:before {
|
||||
transition: background-color 0.1s ease-out;
|
||||
}
|
||||
|
||||
.monaco-sash.hover:before,
|
||||
.monaco-sash.active:before {
|
||||
background: var(--vscode-sash-hoverBorder);
|
||||
}
|
||||
|
||||
.monaco-sash.vertical:before {
|
||||
width: var(--vscode-sash-hover-size);
|
||||
left: calc(50% - (var(--vscode-sash-hover-size) / 2));
|
||||
}
|
||||
|
||||
.monaco-sash.horizontal:before {
|
||||
height: var(--vscode-sash-hover-size);
|
||||
top: calc(50% - (var(--vscode-sash-hover-size) / 2));
|
||||
}
|
||||
|
||||
.pointer-events-disabled {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/** Debug **/
|
||||
|
||||
.monaco-sash.debug {
|
||||
background: cyan;
|
||||
}
|
||||
|
||||
.monaco-sash.debug.disabled {
|
||||
background: rgba(0, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.monaco-sash.debug:not(.disabled) > .orthogonal-drag-handle {
|
||||
background: red;
|
||||
}
|
||||
@@ -1,688 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { $, append, createStyleSheet, EventHelper, EventLike, getWindow, isHTMLElement } from 'vs/base/browser/dom';
|
||||
import { DomEmitter } from 'vs/base/browser/event';
|
||||
import { EventType, Gesture } from 'vs/base/browser/touch';
|
||||
import { Delayer } from 'vs/base/common/async';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
// import 'vs/css!./sash';
|
||||
|
||||
/**
|
||||
* Allow the sashes to be visible at runtime.
|
||||
* @remark Use for development purposes only.
|
||||
*/
|
||||
const DEBUG = false;
|
||||
// DEBUG = Boolean("true"); // done "weirdly" so that a lint warning prevents you from pushing this
|
||||
|
||||
/**
|
||||
* A vertical sash layout provider provides position and height for a sash.
|
||||
*/
|
||||
export interface IVerticalSashLayoutProvider {
|
||||
getVerticalSashLeft(sash: Sash): number;
|
||||
getVerticalSashTop?(sash: Sash): number;
|
||||
getVerticalSashHeight?(sash: Sash): number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A vertical sash layout provider provides position and width for a sash.
|
||||
*/
|
||||
export interface IHorizontalSashLayoutProvider {
|
||||
getHorizontalSashTop(sash: Sash): number;
|
||||
getHorizontalSashLeft?(sash: Sash): number;
|
||||
getHorizontalSashWidth?(sash: Sash): number;
|
||||
}
|
||||
|
||||
type ISashLayoutProvider = IVerticalSashLayoutProvider | IHorizontalSashLayoutProvider;
|
||||
|
||||
export interface ISashEvent {
|
||||
readonly startX: number;
|
||||
readonly currentX: number;
|
||||
readonly startY: number;
|
||||
readonly currentY: number;
|
||||
readonly altKey: boolean;
|
||||
}
|
||||
|
||||
export enum OrthogonalEdge {
|
||||
North = 'north',
|
||||
South = 'south',
|
||||
East = 'east',
|
||||
West = 'west'
|
||||
}
|
||||
|
||||
export interface IBoundarySashes {
|
||||
readonly top?: Sash;
|
||||
readonly right?: Sash;
|
||||
readonly bottom?: Sash;
|
||||
readonly left?: Sash;
|
||||
}
|
||||
|
||||
export interface ISashOptions {
|
||||
|
||||
/**
|
||||
* Whether a sash is horizontal or vertical.
|
||||
*/
|
||||
readonly orientation: Orientation;
|
||||
|
||||
/**
|
||||
* The width or height of a vertical or horizontal sash, respectively.
|
||||
*/
|
||||
readonly size?: number;
|
||||
|
||||
/**
|
||||
* A reference to another sash, perpendicular to this one, which
|
||||
* aligns at the start of this one. A corner sash will be created
|
||||
* automatically at that location.
|
||||
*
|
||||
* The start of a horizontal sash is its left-most position.
|
||||
* The start of a vertical sash is its top-most position.
|
||||
*/
|
||||
readonly orthogonalStartSash?: Sash;
|
||||
|
||||
/**
|
||||
* A reference to another sash, perpendicular to this one, which
|
||||
* aligns at the end of this one. A corner sash will be created
|
||||
* automatically at that location.
|
||||
*
|
||||
* The end of a horizontal sash is its right-most position.
|
||||
* The end of a vertical sash is its bottom-most position.
|
||||
*/
|
||||
readonly orthogonalEndSash?: Sash;
|
||||
|
||||
/**
|
||||
* Provides a hint as to what mouse cursor to use whenever the user
|
||||
* hovers over a corner sash provided by this and an orthogonal sash.
|
||||
*/
|
||||
readonly orthogonalEdge?: OrthogonalEdge;
|
||||
}
|
||||
|
||||
export interface IVerticalSashOptions extends ISashOptions {
|
||||
readonly orientation: Orientation.VERTICAL;
|
||||
}
|
||||
|
||||
export interface IHorizontalSashOptions extends ISashOptions {
|
||||
readonly orientation: Orientation.HORIZONTAL;
|
||||
}
|
||||
|
||||
export const enum Orientation {
|
||||
VERTICAL,
|
||||
HORIZONTAL
|
||||
}
|
||||
|
||||
export const enum SashState {
|
||||
|
||||
/**
|
||||
* Disable any UI interaction.
|
||||
*/
|
||||
Disabled,
|
||||
|
||||
/**
|
||||
* Allow dragging down or to the right, depending on the sash orientation.
|
||||
*
|
||||
* Some OSs allow customizing the mouse cursor differently whenever
|
||||
* some resizable component can't be any smaller, but can be larger.
|
||||
*/
|
||||
AtMinimum,
|
||||
|
||||
/**
|
||||
* Allow dragging up or to the left, depending on the sash orientation.
|
||||
*
|
||||
* Some OSs allow customizing the mouse cursor differently whenever
|
||||
* some resizable component can't be any larger, but can be smaller.
|
||||
*/
|
||||
AtMaximum,
|
||||
|
||||
/**
|
||||
* Enable dragging.
|
||||
*/
|
||||
Enabled
|
||||
}
|
||||
|
||||
let globalSize = 4;
|
||||
const onDidChangeGlobalSize = new Emitter<number>();
|
||||
export function setGlobalSashSize(size: number): void {
|
||||
globalSize = size;
|
||||
onDidChangeGlobalSize.fire(size);
|
||||
}
|
||||
|
||||
let globalHoverDelay = 300;
|
||||
const onDidChangeHoverDelay = new Emitter<number>();
|
||||
export function setGlobalHoverDelay(size: number): void {
|
||||
globalHoverDelay = size;
|
||||
onDidChangeHoverDelay.fire(size);
|
||||
}
|
||||
|
||||
interface PointerEvent extends EventLike {
|
||||
readonly pageX: number;
|
||||
readonly pageY: number;
|
||||
readonly altKey: boolean;
|
||||
readonly target: EventTarget | null;
|
||||
readonly initialTarget?: EventTarget | undefined;
|
||||
}
|
||||
|
||||
interface IPointerEventFactory {
|
||||
readonly onPointerMove: Event<PointerEvent>;
|
||||
readonly onPointerUp: Event<PointerEvent>;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
class MouseEventFactory implements IPointerEventFactory {
|
||||
|
||||
private readonly disposables = new DisposableStore();
|
||||
|
||||
constructor(private el: HTMLElement) { }
|
||||
|
||||
@memoize
|
||||
get onPointerMove(): Event<PointerEvent> {
|
||||
return this.disposables.add(new DomEmitter(getWindow(this.el), 'mousemove')).event;
|
||||
}
|
||||
|
||||
@memoize
|
||||
get onPointerUp(): Event<PointerEvent> {
|
||||
return this.disposables.add(new DomEmitter(getWindow(this.el), 'mouseup')).event;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposables.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class GestureEventFactory implements IPointerEventFactory {
|
||||
|
||||
private readonly disposables = new DisposableStore();
|
||||
|
||||
@memoize
|
||||
get onPointerMove(): Event<PointerEvent> {
|
||||
return this.disposables.add(new DomEmitter(this.el, EventType.Change)).event;
|
||||
}
|
||||
|
||||
@memoize
|
||||
get onPointerUp(): Event<PointerEvent> {
|
||||
return this.disposables.add(new DomEmitter(this.el, EventType.End)).event;
|
||||
}
|
||||
|
||||
constructor(private el: HTMLElement) { }
|
||||
|
||||
dispose(): void {
|
||||
this.disposables.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class OrthogonalPointerEventFactory implements IPointerEventFactory {
|
||||
|
||||
@memoize
|
||||
get onPointerMove(): Event<PointerEvent> {
|
||||
return this.factory.onPointerMove;
|
||||
}
|
||||
|
||||
@memoize
|
||||
get onPointerUp(): Event<PointerEvent> {
|
||||
return this.factory.onPointerUp;
|
||||
}
|
||||
|
||||
constructor(private factory: IPointerEventFactory) { }
|
||||
|
||||
dispose(): void {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
const PointerEventsDisabledCssClass = 'pointer-events-disabled';
|
||||
|
||||
/**
|
||||
* The {@link Sash} is the UI component which allows the user to resize other
|
||||
* components. It's usually an invisible horizontal or vertical line which, when
|
||||
* hovered, becomes highlighted and can be dragged along the perpendicular dimension
|
||||
* to its direction.
|
||||
*
|
||||
* Features:
|
||||
* - Touch event handling
|
||||
* - Corner sash support
|
||||
* - Hover with different mouse cursor support
|
||||
* - Configurable hover size
|
||||
* - Linked sash support, for 2x2 corner sashes
|
||||
*/
|
||||
export class Sash extends Disposable {
|
||||
|
||||
private el: HTMLElement;
|
||||
private layoutProvider: ISashLayoutProvider;
|
||||
private orientation: Orientation;
|
||||
private size: number;
|
||||
private hoverDelay = globalHoverDelay;
|
||||
private hoverDelayer = this._register(new Delayer(this.hoverDelay));
|
||||
|
||||
private _state: SashState = SashState.Enabled;
|
||||
private readonly onDidEnablementChange = this._register(new Emitter<SashState>());
|
||||
private readonly _onDidStart = this._register(new Emitter<ISashEvent>());
|
||||
private readonly _onDidChange = this._register(new Emitter<ISashEvent>());
|
||||
private readonly _onDidReset = this._register(new Emitter<void>());
|
||||
private readonly _onDidEnd = this._register(new Emitter<void>());
|
||||
private readonly orthogonalStartSashDisposables = this._register(new DisposableStore());
|
||||
private _orthogonalStartSash: Sash | undefined;
|
||||
private readonly orthogonalStartDragHandleDisposables = this._register(new DisposableStore());
|
||||
private _orthogonalStartDragHandle: HTMLElement | undefined;
|
||||
private readonly orthogonalEndSashDisposables = this._register(new DisposableStore());
|
||||
private _orthogonalEndSash: Sash | undefined;
|
||||
private readonly orthogonalEndDragHandleDisposables = this._register(new DisposableStore());
|
||||
private _orthogonalEndDragHandle: HTMLElement | undefined;
|
||||
|
||||
get state(): SashState { return this._state; }
|
||||
get orthogonalStartSash(): Sash | undefined { return this._orthogonalStartSash; }
|
||||
get orthogonalEndSash(): Sash | undefined { return this._orthogonalEndSash; }
|
||||
|
||||
/**
|
||||
* The state of a sash defines whether it can be interacted with by the user
|
||||
* as well as what mouse cursor to use, when hovered.
|
||||
*/
|
||||
set state(state: SashState) {
|
||||
if (this._state === state) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.el.classList.toggle('disabled', state === SashState.Disabled);
|
||||
this.el.classList.toggle('minimum', state === SashState.AtMinimum);
|
||||
this.el.classList.toggle('maximum', state === SashState.AtMaximum);
|
||||
|
||||
this._state = state;
|
||||
this.onDidEnablementChange.fire(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* An event which fires whenever the user starts dragging this sash.
|
||||
*/
|
||||
readonly onDidStart: Event<ISashEvent> = this._onDidStart.event;
|
||||
|
||||
/**
|
||||
* An event which fires whenever the user moves the mouse while
|
||||
* dragging this sash.
|
||||
*/
|
||||
readonly onDidChange: Event<ISashEvent> = this._onDidChange.event;
|
||||
|
||||
/**
|
||||
* An event which fires whenever the user double clicks this sash.
|
||||
*/
|
||||
readonly onDidReset: Event<void> = this._onDidReset.event;
|
||||
|
||||
/**
|
||||
* An event which fires whenever the user stops dragging this sash.
|
||||
*/
|
||||
readonly onDidEnd: Event<void> = this._onDidEnd.event;
|
||||
|
||||
/**
|
||||
* A linked sash will be forwarded the same user interactions and events
|
||||
* so it moves exactly the same way as this sash.
|
||||
*
|
||||
* Useful in 2x2 grids. Not meant for widespread usage.
|
||||
*/
|
||||
linkedSash: Sash | undefined = undefined;
|
||||
|
||||
/**
|
||||
* A reference to another sash, perpendicular to this one, which
|
||||
* aligns at the start of this one. A corner sash will be created
|
||||
* automatically at that location.
|
||||
*
|
||||
* The start of a horizontal sash is its left-most position.
|
||||
* The start of a vertical sash is its top-most position.
|
||||
*/
|
||||
set orthogonalStartSash(sash: Sash | undefined) {
|
||||
if (this._orthogonalStartSash === sash) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.orthogonalStartDragHandleDisposables.clear();
|
||||
this.orthogonalStartSashDisposables.clear();
|
||||
|
||||
if (sash) {
|
||||
const onChange = (state: SashState) => {
|
||||
this.orthogonalStartDragHandleDisposables.clear();
|
||||
|
||||
if (state !== SashState.Disabled) {
|
||||
this._orthogonalStartDragHandle = append(this.el, $('.orthogonal-drag-handle.start'));
|
||||
this.orthogonalStartDragHandleDisposables.add(toDisposable(() => this._orthogonalStartDragHandle!.remove()));
|
||||
this.orthogonalStartDragHandleDisposables.add(new DomEmitter(this._orthogonalStartDragHandle, 'mouseenter')).event
|
||||
(() => Sash.onMouseEnter(sash), undefined, this.orthogonalStartDragHandleDisposables);
|
||||
this.orthogonalStartDragHandleDisposables.add(new DomEmitter(this._orthogonalStartDragHandle, 'mouseleave')).event
|
||||
(() => Sash.onMouseLeave(sash), undefined, this.orthogonalStartDragHandleDisposables);
|
||||
}
|
||||
};
|
||||
|
||||
this.orthogonalStartSashDisposables.add(sash.onDidEnablementChange.event(onChange, this));
|
||||
onChange(sash.state);
|
||||
}
|
||||
|
||||
this._orthogonalStartSash = sash;
|
||||
}
|
||||
|
||||
/**
|
||||
* A reference to another sash, perpendicular to this one, which
|
||||
* aligns at the end of this one. A corner sash will be created
|
||||
* automatically at that location.
|
||||
*
|
||||
* The end of a horizontal sash is its right-most position.
|
||||
* The end of a vertical sash is its bottom-most position.
|
||||
*/
|
||||
|
||||
set orthogonalEndSash(sash: Sash | undefined) {
|
||||
if (this._orthogonalEndSash === sash) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.orthogonalEndDragHandleDisposables.clear();
|
||||
this.orthogonalEndSashDisposables.clear();
|
||||
|
||||
if (sash) {
|
||||
const onChange = (state: SashState) => {
|
||||
this.orthogonalEndDragHandleDisposables.clear();
|
||||
|
||||
if (state !== SashState.Disabled) {
|
||||
this._orthogonalEndDragHandle = append(this.el, $('.orthogonal-drag-handle.end'));
|
||||
this.orthogonalEndDragHandleDisposables.add(toDisposable(() => this._orthogonalEndDragHandle!.remove()));
|
||||
this.orthogonalEndDragHandleDisposables.add(new DomEmitter(this._orthogonalEndDragHandle, 'mouseenter')).event
|
||||
(() => Sash.onMouseEnter(sash), undefined, this.orthogonalEndDragHandleDisposables);
|
||||
this.orthogonalEndDragHandleDisposables.add(new DomEmitter(this._orthogonalEndDragHandle, 'mouseleave')).event
|
||||
(() => Sash.onMouseLeave(sash), undefined, this.orthogonalEndDragHandleDisposables);
|
||||
}
|
||||
};
|
||||
|
||||
this.orthogonalEndSashDisposables.add(sash.onDidEnablementChange.event(onChange, this));
|
||||
onChange(sash.state);
|
||||
}
|
||||
|
||||
this._orthogonalEndSash = sash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new vertical sash.
|
||||
*
|
||||
* @param container A DOM node to append the sash to.
|
||||
* @param verticalLayoutProvider A vertical layout provider.
|
||||
* @param options The options.
|
||||
*/
|
||||
constructor(container: HTMLElement, verticalLayoutProvider: IVerticalSashLayoutProvider, options: IVerticalSashOptions);
|
||||
|
||||
/**
|
||||
* Create a new horizontal sash.
|
||||
*
|
||||
* @param container A DOM node to append the sash to.
|
||||
* @param horizontalLayoutProvider A horizontal layout provider.
|
||||
* @param options The options.
|
||||
*/
|
||||
constructor(container: HTMLElement, horizontalLayoutProvider: IHorizontalSashLayoutProvider, options: IHorizontalSashOptions);
|
||||
constructor(container: HTMLElement, layoutProvider: ISashLayoutProvider, options: ISashOptions) {
|
||||
super();
|
||||
|
||||
this.el = append(container, $('.monaco-sash'));
|
||||
|
||||
if (options.orthogonalEdge) {
|
||||
this.el.classList.add(`orthogonal-edge-${options.orthogonalEdge}`);
|
||||
}
|
||||
|
||||
if (isMacintosh) {
|
||||
this.el.classList.add('mac');
|
||||
}
|
||||
|
||||
const onMouseDown = this._register(new DomEmitter(this.el, 'mousedown')).event;
|
||||
this._register(onMouseDown(e => this.onPointerStart(e, new MouseEventFactory(container)), this));
|
||||
const onMouseDoubleClick = this._register(new DomEmitter(this.el, 'dblclick')).event;
|
||||
this._register(onMouseDoubleClick(this.onPointerDoublePress, this));
|
||||
const onMouseEnter = this._register(new DomEmitter(this.el, 'mouseenter')).event;
|
||||
this._register(onMouseEnter(() => Sash.onMouseEnter(this)));
|
||||
const onMouseLeave = this._register(new DomEmitter(this.el, 'mouseleave')).event;
|
||||
this._register(onMouseLeave(() => Sash.onMouseLeave(this)));
|
||||
|
||||
this._register(Gesture.addTarget(this.el));
|
||||
|
||||
const onTouchStart = this._register(new DomEmitter(this.el, EventType.Start)).event;
|
||||
this._register(onTouchStart(e => this.onPointerStart(e, new GestureEventFactory(this.el)), this));
|
||||
const onTap = this._register(new DomEmitter(this.el, EventType.Tap)).event;
|
||||
|
||||
let doubleTapTimeout: any = undefined;
|
||||
this._register(onTap(event => {
|
||||
if (doubleTapTimeout) {
|
||||
clearTimeout(doubleTapTimeout);
|
||||
doubleTapTimeout = undefined;
|
||||
this.onPointerDoublePress(event);
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(doubleTapTimeout);
|
||||
doubleTapTimeout = setTimeout(() => doubleTapTimeout = undefined, 250);
|
||||
}, this));
|
||||
|
||||
if (typeof options.size === 'number') {
|
||||
this.size = options.size;
|
||||
|
||||
if (options.orientation === Orientation.VERTICAL) {
|
||||
this.el.style.width = `${this.size}px`;
|
||||
} else {
|
||||
this.el.style.height = `${this.size}px`;
|
||||
}
|
||||
} else {
|
||||
this.size = globalSize;
|
||||
this._register(onDidChangeGlobalSize.event(size => {
|
||||
this.size = size;
|
||||
this.layout();
|
||||
}));
|
||||
}
|
||||
|
||||
this._register(onDidChangeHoverDelay.event(delay => this.hoverDelay = delay));
|
||||
|
||||
this.layoutProvider = layoutProvider;
|
||||
|
||||
this.orthogonalStartSash = options.orthogonalStartSash;
|
||||
this.orthogonalEndSash = options.orthogonalEndSash;
|
||||
|
||||
this.orientation = options.orientation || Orientation.VERTICAL;
|
||||
|
||||
if (this.orientation === Orientation.HORIZONTAL) {
|
||||
this.el.classList.add('horizontal');
|
||||
this.el.classList.remove('vertical');
|
||||
} else {
|
||||
this.el.classList.remove('horizontal');
|
||||
this.el.classList.add('vertical');
|
||||
}
|
||||
|
||||
this.el.classList.toggle('debug', DEBUG);
|
||||
|
||||
this.layout();
|
||||
}
|
||||
|
||||
private onPointerStart(event: PointerEvent, pointerEventFactory: IPointerEventFactory): void {
|
||||
EventHelper.stop(event);
|
||||
|
||||
let isMultisashResize = false;
|
||||
|
||||
if (!(event as any).__orthogonalSashEvent) {
|
||||
const orthogonalSash = this.getOrthogonalSash(event);
|
||||
|
||||
if (orthogonalSash) {
|
||||
isMultisashResize = true;
|
||||
(event as any).__orthogonalSashEvent = true;
|
||||
orthogonalSash.onPointerStart(event, new OrthogonalPointerEventFactory(pointerEventFactory));
|
||||
}
|
||||
}
|
||||
|
||||
if (this.linkedSash && !(event as any).__linkedSashEvent) {
|
||||
(event as any).__linkedSashEvent = true;
|
||||
this.linkedSash.onPointerStart(event, new OrthogonalPointerEventFactory(pointerEventFactory));
|
||||
}
|
||||
|
||||
if (!this.state) {
|
||||
return;
|
||||
}
|
||||
|
||||
const iframes = this.el.ownerDocument.getElementsByTagName('iframe');
|
||||
for (const iframe of iframes) {
|
||||
iframe.classList.add(PointerEventsDisabledCssClass); // disable mouse events on iframes as long as we drag the sash
|
||||
}
|
||||
|
||||
const startX = event.pageX;
|
||||
const startY = event.pageY;
|
||||
const altKey = event.altKey;
|
||||
const startEvent: ISashEvent = { startX, currentX: startX, startY, currentY: startY, altKey };
|
||||
|
||||
this.el.classList.add('active');
|
||||
this._onDidStart.fire(startEvent);
|
||||
|
||||
// fix https://github.com/microsoft/vscode/issues/21675
|
||||
const style = createStyleSheet(this.el);
|
||||
const updateStyle = () => {
|
||||
let cursor = '';
|
||||
|
||||
if (isMultisashResize) {
|
||||
cursor = 'all-scroll';
|
||||
} else if (this.orientation === Orientation.HORIZONTAL) {
|
||||
if (this.state === SashState.AtMinimum) {
|
||||
cursor = 's-resize';
|
||||
} else if (this.state === SashState.AtMaximum) {
|
||||
cursor = 'n-resize';
|
||||
} else {
|
||||
cursor = isMacintosh ? 'row-resize' : 'ns-resize';
|
||||
}
|
||||
} else {
|
||||
if (this.state === SashState.AtMinimum) {
|
||||
cursor = 'e-resize';
|
||||
} else if (this.state === SashState.AtMaximum) {
|
||||
cursor = 'w-resize';
|
||||
} else {
|
||||
cursor = isMacintosh ? 'col-resize' : 'ew-resize';
|
||||
}
|
||||
}
|
||||
|
||||
style.textContent = `* { cursor: ${cursor} !important; }`;
|
||||
};
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
updateStyle();
|
||||
|
||||
if (!isMultisashResize) {
|
||||
this.onDidEnablementChange.event(updateStyle, null, disposables);
|
||||
}
|
||||
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
EventHelper.stop(e, false);
|
||||
const event: ISashEvent = { startX, currentX: e.pageX, startY, currentY: e.pageY, altKey };
|
||||
|
||||
this._onDidChange.fire(event);
|
||||
};
|
||||
|
||||
const onPointerUp = (e: PointerEvent) => {
|
||||
EventHelper.stop(e, false);
|
||||
|
||||
style.remove();
|
||||
|
||||
this.el.classList.remove('active');
|
||||
this._onDidEnd.fire();
|
||||
|
||||
disposables.dispose();
|
||||
|
||||
for (const iframe of iframes) {
|
||||
iframe.classList.remove(PointerEventsDisabledCssClass);
|
||||
}
|
||||
};
|
||||
|
||||
pointerEventFactory.onPointerMove(onPointerMove, null, disposables);
|
||||
pointerEventFactory.onPointerUp(onPointerUp, null, disposables);
|
||||
disposables.add(pointerEventFactory);
|
||||
}
|
||||
|
||||
private onPointerDoublePress(e: MouseEvent): void {
|
||||
const orthogonalSash = this.getOrthogonalSash(e);
|
||||
|
||||
if (orthogonalSash) {
|
||||
orthogonalSash._onDidReset.fire();
|
||||
}
|
||||
|
||||
if (this.linkedSash) {
|
||||
this.linkedSash._onDidReset.fire();
|
||||
}
|
||||
|
||||
this._onDidReset.fire();
|
||||
}
|
||||
|
||||
private static onMouseEnter(sash: Sash, fromLinkedSash: boolean = false): void {
|
||||
if (sash.el.classList.contains('active')) {
|
||||
sash.hoverDelayer.cancel();
|
||||
sash.el.classList.add('hover');
|
||||
} else {
|
||||
sash.hoverDelayer.trigger(() => sash.el.classList.add('hover'), sash.hoverDelay).then(undefined, () => { });
|
||||
}
|
||||
|
||||
if (!fromLinkedSash && sash.linkedSash) {
|
||||
Sash.onMouseEnter(sash.linkedSash, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static onMouseLeave(sash: Sash, fromLinkedSash: boolean = false): void {
|
||||
sash.hoverDelayer.cancel();
|
||||
sash.el.classList.remove('hover');
|
||||
|
||||
if (!fromLinkedSash && sash.linkedSash) {
|
||||
Sash.onMouseLeave(sash.linkedSash, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forcefully stop any user interactions with this sash.
|
||||
* Useful when hiding a parent component, while the user is still
|
||||
* interacting with the sash.
|
||||
*/
|
||||
clearSashHoverState(): void {
|
||||
Sash.onMouseLeave(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout the sash. The sash will size and position itself
|
||||
* based on its provided {@link ISashLayoutProvider layout provider}.
|
||||
*/
|
||||
layout(): void {
|
||||
if (this.orientation === Orientation.VERTICAL) {
|
||||
const verticalProvider = (<IVerticalSashLayoutProvider>this.layoutProvider);
|
||||
this.el.style.left = verticalProvider.getVerticalSashLeft(this) - (this.size / 2) + 'px';
|
||||
|
||||
if (verticalProvider.getVerticalSashTop) {
|
||||
this.el.style.top = verticalProvider.getVerticalSashTop(this) + 'px';
|
||||
}
|
||||
|
||||
if (verticalProvider.getVerticalSashHeight) {
|
||||
this.el.style.height = verticalProvider.getVerticalSashHeight(this) + 'px';
|
||||
}
|
||||
} else {
|
||||
const horizontalProvider = (<IHorizontalSashLayoutProvider>this.layoutProvider);
|
||||
this.el.style.top = horizontalProvider.getHorizontalSashTop(this) - (this.size / 2) + 'px';
|
||||
|
||||
if (horizontalProvider.getHorizontalSashLeft) {
|
||||
this.el.style.left = horizontalProvider.getHorizontalSashLeft(this) + 'px';
|
||||
}
|
||||
|
||||
if (horizontalProvider.getHorizontalSashWidth) {
|
||||
this.el.style.width = horizontalProvider.getHorizontalSashWidth(this) + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getOrthogonalSash(e: PointerEvent): Sash | undefined {
|
||||
const target = e.initialTarget ?? e.target;
|
||||
|
||||
if (!target || !(isHTMLElement(target))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (target.classList.contains('orthogonal-drag-handle')) {
|
||||
return target.classList.contains('start') ? this.orthogonalStartSash : this.orthogonalEndSash;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
this.el.remove();
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-pane-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-pane-view .pane {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.monaco-pane-view .pane.horizontal:not(.expanded) {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.monaco-pane-view .pane > .pane-header {
|
||||
height: 22px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.monaco-pane-view .pane > .pane-header.not-collapsible {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-pane-view .pane > .pane-header > .title {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.monaco-pane-view .pane.horizontal:not(.expanded) > .pane-header {
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
.monaco-pane-view .pane > .pane-header > .codicon:first-of-type {
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.monaco-pane-view .pane.horizontal:not(.expanded) > .pane-header > .codicon:first-of-type {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
/* TODO: actions should be part of the pane, but they aren't yet */
|
||||
.monaco-pane-view .pane > .pane-header > .actions {
|
||||
display: none;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.monaco-pane-view .pane > .pane-header > .actions .action-item {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.monaco-pane-view .pane > .pane-header > .actions .action-label {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
/* TODO: actions should be part of the pane, but they aren't yet */
|
||||
.monaco-pane-view .pane:hover > .pane-header.expanded > .actions,
|
||||
.monaco-pane-view .pane:focus-within > .pane-header.expanded > .actions,
|
||||
.monaco-pane-view .pane > .pane-header.actions-always-visible.expanded > .actions,
|
||||
.monaco-pane-view .pane > .pane-header.focused.expanded > .actions {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
.monaco-pane-view .pane > .pane-header .monaco-action-bar .action-item.select-container {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-pane-view .pane > .pane-header .action-item .monaco-select-box {
|
||||
cursor: pointer;
|
||||
min-width: 110px;
|
||||
min-height: 18px;
|
||||
padding: 2px 23px 2px 8px;
|
||||
}
|
||||
|
||||
.linux .monaco-pane-view .pane > .pane-header .action-item .monaco-select-box,
|
||||
.windows .monaco-pane-view .pane > .pane-header .action-item .monaco-select-box {
|
||||
padding: 0px 23px 0px 8px;
|
||||
}
|
||||
|
||||
/* Bold font style does not go well with CJK fonts */
|
||||
.monaco-pane-view:lang(zh-Hans) .pane > .pane-header,
|
||||
.monaco-pane-view:lang(zh-Hant) .pane > .pane-header,
|
||||
.monaco-pane-view:lang(ja) .pane > .pane-header,
|
||||
.monaco-pane-view:lang(ko) .pane > .pane-header {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.monaco-pane-view .pane > .pane-header.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-pane-view .pane > .pane-body {
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Animation */
|
||||
|
||||
.monaco-pane-view.animated .split-view-view {
|
||||
transition-duration: 0.15s;
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
|
||||
.reduce-motion .monaco-pane-view .split-view-view {
|
||||
transition-duration: 0s !important;
|
||||
}
|
||||
|
||||
.monaco-pane-view.animated.vertical .split-view-view {
|
||||
transition-property: height;
|
||||
}
|
||||
|
||||
.monaco-pane-view.animated.horizontal .split-view-view {
|
||||
transition-property: width;
|
||||
}
|
||||
|
||||
#monaco-pane-drop-overlay {
|
||||
position: absolute;
|
||||
z-index: 10000;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#monaco-pane-drop-overlay > .pane-overlay-indicator {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 22px;
|
||||
min-width: 19px;
|
||||
|
||||
pointer-events: none; /* very important to not take events away from the parent */
|
||||
transition: opacity 150ms ease-out;
|
||||
}
|
||||
|
||||
#monaco-pane-drop-overlay > .pane-overlay-indicator.overlay-move-transition {
|
||||
transition: top 70ms ease-out, left 70ms ease-out, width 70ms ease-out, height 70ms ease-out, opacity 150ms ease-out;
|
||||
}
|
||||
@@ -1,681 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isFirefox } from 'vs/base/browser/browser';
|
||||
import { DataTransfers } from 'vs/base/browser/dnd';
|
||||
import { $, addDisposableListener, append, clearNode, EventHelper, EventType, getWindow, isHTMLElement, trackFocus } from 'vs/base/browser/dom';
|
||||
import { DomEmitter } from 'vs/base/browser/event';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { Gesture, EventType as TouchEventType } from 'vs/base/browser/touch';
|
||||
import { IBoundarySashes, Orientation } from 'vs/base/browser/ui/sash/sash';
|
||||
import { Color, RGBA } from 'vs/base/common/color';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ScrollEvent } from 'vs/base/common/scrollable';
|
||||
// import 'vs/css!./paneview';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IView, Sizing, SplitView } from './splitview';
|
||||
|
||||
export interface IPaneOptions {
|
||||
minimumBodySize?: number;
|
||||
maximumBodySize?: number;
|
||||
expanded?: boolean;
|
||||
orientation?: Orientation;
|
||||
title: string;
|
||||
titleDescription?: string;
|
||||
}
|
||||
|
||||
export interface IPaneStyles {
|
||||
readonly dropBackground: string | undefined;
|
||||
readonly headerForeground: string | undefined;
|
||||
readonly headerBackground: string | undefined;
|
||||
readonly headerBorder: string | undefined;
|
||||
readonly leftBorder: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* A Pane is a structured SplitView view.
|
||||
*
|
||||
* WARNING: You must call `render()` after you construct it.
|
||||
* It can't be done automatically at the end of the ctor
|
||||
* because of the order of property initialization in TypeScript.
|
||||
* Subclasses wouldn't be able to set own properties
|
||||
* before the `render()` call, thus forbidding their use.
|
||||
*/
|
||||
export abstract class Pane extends Disposable implements IView {
|
||||
|
||||
private static readonly HEADER_SIZE = 22;
|
||||
|
||||
readonly element: HTMLElement;
|
||||
private header!: HTMLElement;
|
||||
private body!: HTMLElement;
|
||||
|
||||
protected _expanded: boolean;
|
||||
protected _orientation: Orientation;
|
||||
|
||||
private expandedSize: number | undefined = undefined;
|
||||
private _headerVisible = true;
|
||||
private _collapsible = true;
|
||||
private _bodyRendered = false;
|
||||
private _minimumBodySize: number;
|
||||
private _maximumBodySize: number;
|
||||
private _ariaHeaderLabel: string;
|
||||
private styles: IPaneStyles = {
|
||||
dropBackground: undefined,
|
||||
headerBackground: undefined,
|
||||
headerBorder: undefined,
|
||||
headerForeground: undefined,
|
||||
leftBorder: undefined
|
||||
};
|
||||
private animationTimer: number | undefined = undefined;
|
||||
|
||||
private readonly _onDidChange = this._register(new Emitter<number | undefined>());
|
||||
readonly onDidChange: Event<number | undefined> = this._onDidChange.event;
|
||||
|
||||
private readonly _onDidChangeExpansionState = this._register(new Emitter<boolean>());
|
||||
readonly onDidChangeExpansionState: Event<boolean> = this._onDidChangeExpansionState.event;
|
||||
|
||||
get ariaHeaderLabel(): string {
|
||||
return this._ariaHeaderLabel;
|
||||
}
|
||||
|
||||
set ariaHeaderLabel(newLabel: string) {
|
||||
this._ariaHeaderLabel = newLabel;
|
||||
this.header.setAttribute('aria-label', this.ariaHeaderLabel);
|
||||
}
|
||||
|
||||
get draggableElement(): HTMLElement {
|
||||
return this.header;
|
||||
}
|
||||
|
||||
get dropTargetElement(): HTMLElement {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
get dropBackground(): string | undefined {
|
||||
return this.styles.dropBackground;
|
||||
}
|
||||
|
||||
get minimumBodySize(): number {
|
||||
return this._minimumBodySize;
|
||||
}
|
||||
|
||||
set minimumBodySize(size: number) {
|
||||
this._minimumBodySize = size;
|
||||
this._onDidChange.fire(undefined);
|
||||
}
|
||||
|
||||
get maximumBodySize(): number {
|
||||
return this._maximumBodySize;
|
||||
}
|
||||
|
||||
set maximumBodySize(size: number) {
|
||||
this._maximumBodySize = size;
|
||||
this._onDidChange.fire(undefined);
|
||||
}
|
||||
|
||||
private get headerSize(): number {
|
||||
return this.headerVisible ? Pane.HEADER_SIZE : 0;
|
||||
}
|
||||
|
||||
get minimumSize(): number {
|
||||
const headerSize = this.headerSize;
|
||||
const expanded = !this.headerVisible || this.isExpanded();
|
||||
const minimumBodySize = expanded ? this.minimumBodySize : 0;
|
||||
|
||||
return headerSize + minimumBodySize;
|
||||
}
|
||||
|
||||
get maximumSize(): number {
|
||||
const headerSize = this.headerSize;
|
||||
const expanded = !this.headerVisible || this.isExpanded();
|
||||
const maximumBodySize = expanded ? this.maximumBodySize : 0;
|
||||
|
||||
return headerSize + maximumBodySize;
|
||||
}
|
||||
|
||||
orthogonalSize: number = 0;
|
||||
|
||||
constructor(options: IPaneOptions) {
|
||||
super();
|
||||
this._expanded = typeof options.expanded === 'undefined' ? true : !!options.expanded;
|
||||
this._orientation = typeof options.orientation === 'undefined' ? Orientation.VERTICAL : options.orientation;
|
||||
this._ariaHeaderLabel = localize('viewSection', "{0} Section", options.title);
|
||||
this._minimumBodySize = typeof options.minimumBodySize === 'number' ? options.minimumBodySize : this._orientation === Orientation.HORIZONTAL ? 200 : 120;
|
||||
this._maximumBodySize = typeof options.maximumBodySize === 'number' ? options.maximumBodySize : Number.POSITIVE_INFINITY;
|
||||
|
||||
this.element = $('.pane');
|
||||
}
|
||||
|
||||
isExpanded(): boolean {
|
||||
return this._expanded;
|
||||
}
|
||||
|
||||
setExpanded(expanded: boolean): boolean {
|
||||
if (!expanded && !this.collapsible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._expanded === !!expanded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.element?.classList.toggle('expanded', expanded);
|
||||
|
||||
this._expanded = !!expanded;
|
||||
this.updateHeader();
|
||||
|
||||
if (expanded) {
|
||||
if (!this._bodyRendered) {
|
||||
this.renderBody(this.body);
|
||||
this._bodyRendered = true;
|
||||
}
|
||||
|
||||
if (typeof this.animationTimer === 'number') {
|
||||
getWindow(this.element).clearTimeout(this.animationTimer);
|
||||
}
|
||||
append(this.element, this.body);
|
||||
} else {
|
||||
this.animationTimer = getWindow(this.element).setTimeout(() => {
|
||||
this.body.remove();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
this._onDidChangeExpansionState.fire(expanded);
|
||||
this._onDidChange.fire(expanded ? this.expandedSize : undefined);
|
||||
return true;
|
||||
}
|
||||
|
||||
get headerVisible(): boolean {
|
||||
return this._headerVisible;
|
||||
}
|
||||
|
||||
set headerVisible(visible: boolean) {
|
||||
if (this._headerVisible === !!visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._headerVisible = !!visible;
|
||||
this.updateHeader();
|
||||
this._onDidChange.fire(undefined);
|
||||
}
|
||||
|
||||
get collapsible(): boolean {
|
||||
return this._collapsible;
|
||||
}
|
||||
|
||||
set collapsible(collapsible: boolean) {
|
||||
if (this._collapsible === !!collapsible) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._collapsible = !!collapsible;
|
||||
this.updateHeader();
|
||||
}
|
||||
|
||||
get orientation(): Orientation {
|
||||
return this._orientation;
|
||||
}
|
||||
|
||||
set orientation(orientation: Orientation) {
|
||||
if (this._orientation === orientation) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._orientation = orientation;
|
||||
|
||||
if (this.element) {
|
||||
this.element.classList.toggle('horizontal', this.orientation === Orientation.HORIZONTAL);
|
||||
this.element.classList.toggle('vertical', this.orientation === Orientation.VERTICAL);
|
||||
}
|
||||
|
||||
if (this.header) {
|
||||
this.updateHeader();
|
||||
}
|
||||
}
|
||||
|
||||
render(): void {
|
||||
this.element.classList.toggle('expanded', this.isExpanded());
|
||||
this.element.classList.toggle('horizontal', this.orientation === Orientation.HORIZONTAL);
|
||||
this.element.classList.toggle('vertical', this.orientation === Orientation.VERTICAL);
|
||||
|
||||
this.header = $('.pane-header');
|
||||
append(this.element, this.header);
|
||||
this.header.setAttribute('tabindex', '0');
|
||||
// Use role button so the aria-expanded state gets read https://github.com/microsoft/vscode/issues/95996
|
||||
this.header.setAttribute('role', 'button');
|
||||
this.header.setAttribute('aria-label', this.ariaHeaderLabel);
|
||||
this.renderHeader(this.header);
|
||||
|
||||
const focusTracker = trackFocus(this.header);
|
||||
this._register(focusTracker);
|
||||
this._register(focusTracker.onDidFocus(() => this.header.classList.add('focused'), null));
|
||||
this._register(focusTracker.onDidBlur(() => this.header.classList.remove('focused'), null));
|
||||
|
||||
this.updateHeader();
|
||||
|
||||
const eventDisposables = this._register(new DisposableStore());
|
||||
const onKeyDown = this._register(new DomEmitter(this.header, 'keydown'));
|
||||
const onHeaderKeyDown = Event.map(onKeyDown.event, e => new StandardKeyboardEvent(e), eventDisposables);
|
||||
|
||||
this._register(Event.filter(onHeaderKeyDown, e => e.keyCode === KeyCode.Enter || e.keyCode === KeyCode.Space, eventDisposables)(() => this.setExpanded(!this.isExpanded()), null));
|
||||
|
||||
this._register(Event.filter(onHeaderKeyDown, e => e.keyCode === KeyCode.LeftArrow, eventDisposables)(() => this.setExpanded(false), null));
|
||||
|
||||
this._register(Event.filter(onHeaderKeyDown, e => e.keyCode === KeyCode.RightArrow, eventDisposables)(() => this.setExpanded(true), null));
|
||||
|
||||
this._register(Gesture.addTarget(this.header));
|
||||
|
||||
[EventType.CLICK, TouchEventType.Tap].forEach(eventType => {
|
||||
this._register(addDisposableListener(this.header, eventType, e => {
|
||||
if (!e.defaultPrevented) {
|
||||
this.setExpanded(!this.isExpanded());
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
this.body = append(this.element, $('.pane-body'));
|
||||
|
||||
// Only render the body if it will be visible
|
||||
// Otherwise, render it when the pane is expanded
|
||||
if (!this._bodyRendered && this.isExpanded()) {
|
||||
this.renderBody(this.body);
|
||||
this._bodyRendered = true;
|
||||
}
|
||||
|
||||
if (!this.isExpanded()) {
|
||||
this.body.remove();
|
||||
}
|
||||
}
|
||||
|
||||
layout(size: number): void {
|
||||
const headerSize = this.headerVisible ? Pane.HEADER_SIZE : 0;
|
||||
|
||||
const width = this._orientation === Orientation.VERTICAL ? this.orthogonalSize : size;
|
||||
const height = this._orientation === Orientation.VERTICAL ? size - headerSize : this.orthogonalSize - headerSize;
|
||||
|
||||
if (this.isExpanded()) {
|
||||
this.body.classList.toggle('wide', width >= 600);
|
||||
this.layoutBody(height, width);
|
||||
this.expandedSize = size;
|
||||
}
|
||||
}
|
||||
|
||||
style(styles: IPaneStyles): void {
|
||||
this.styles = styles;
|
||||
|
||||
if (!this.header) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateHeader();
|
||||
}
|
||||
|
||||
protected updateHeader(): void {
|
||||
const expanded = !this.headerVisible || this.isExpanded();
|
||||
|
||||
if (this.collapsible) {
|
||||
this.header.setAttribute('tabindex', '0');
|
||||
this.header.setAttribute('role', 'button');
|
||||
} else {
|
||||
this.header.removeAttribute('tabindex');
|
||||
this.header.removeAttribute('role');
|
||||
}
|
||||
|
||||
this.header.style.lineHeight = `${this.headerSize}px`;
|
||||
this.header.classList.toggle('hidden', !this.headerVisible);
|
||||
this.header.classList.toggle('expanded', expanded);
|
||||
this.header.classList.toggle('not-collapsible', !this.collapsible);
|
||||
this.header.setAttribute('aria-expanded', String(expanded));
|
||||
|
||||
this.header.style.color = this.collapsible ? this.styles.headerForeground ?? '' : '';
|
||||
this.header.style.backgroundColor = (this.collapsible ? this.styles.headerBackground : 'transparent') ?? '';
|
||||
this.header.style.borderTop = this.styles.headerBorder && this.orientation === Orientation.VERTICAL ? `1px solid ${this.styles.headerBorder}` : '';
|
||||
this.element.style.borderLeft = this.styles.leftBorder && this.orientation === Orientation.HORIZONTAL ? `1px solid ${this.styles.leftBorder}` : '';
|
||||
}
|
||||
|
||||
protected abstract renderHeader(container: HTMLElement): void;
|
||||
protected abstract renderBody(container: HTMLElement): void;
|
||||
protected abstract layoutBody(height: number, width: number): void;
|
||||
}
|
||||
|
||||
interface IDndContext {
|
||||
draggable: PaneDraggable | null;
|
||||
}
|
||||
|
||||
class PaneDraggable extends Disposable {
|
||||
|
||||
private static readonly DefaultDragOverBackgroundColor = new Color(new RGBA(128, 128, 128, 0.5));
|
||||
|
||||
private dragOverCounter = 0; // see https://github.com/microsoft/vscode/issues/14470
|
||||
|
||||
private _onDidDrop = this._register(new Emitter<{ from: Pane; to: Pane }>());
|
||||
readonly onDidDrop = this._onDidDrop.event;
|
||||
|
||||
constructor(private pane: Pane, private dnd: IPaneDndController, private context: IDndContext) {
|
||||
super();
|
||||
|
||||
pane.draggableElement.draggable = true;
|
||||
this._register(addDisposableListener(pane.draggableElement, 'dragstart', e => this.onDragStart(e)));
|
||||
this._register(addDisposableListener(pane.dropTargetElement, 'dragenter', e => this.onDragEnter(e)));
|
||||
this._register(addDisposableListener(pane.dropTargetElement, 'dragleave', e => this.onDragLeave(e)));
|
||||
this._register(addDisposableListener(pane.dropTargetElement, 'dragend', e => this.onDragEnd(e)));
|
||||
this._register(addDisposableListener(pane.dropTargetElement, 'drop', e => this.onDrop(e)));
|
||||
}
|
||||
|
||||
private onDragStart(e: DragEvent): void {
|
||||
if (!this.dnd.canDrag(this.pane) || !e.dataTransfer) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
|
||||
if (isFirefox) {
|
||||
// Firefox: requires to set a text data transfer to get going
|
||||
e.dataTransfer?.setData(DataTransfers.TEXT, this.pane.draggableElement.textContent || '');
|
||||
}
|
||||
|
||||
const dragImage = append(this.pane.element.ownerDocument.body, $('.monaco-drag-image', {}, this.pane.draggableElement.textContent || ''));
|
||||
e.dataTransfer.setDragImage(dragImage, -10, -10);
|
||||
setTimeout(() => dragImage.remove(), 0);
|
||||
|
||||
this.context.draggable = this;
|
||||
}
|
||||
|
||||
private onDragEnter(e: DragEvent): void {
|
||||
if (!this.context.draggable || this.context.draggable === this) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.dnd.canDrop(this.context.draggable.pane, this.pane)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dragOverCounter++;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private onDragLeave(e: DragEvent): void {
|
||||
if (!this.context.draggable || this.context.draggable === this) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.dnd.canDrop(this.context.draggable.pane, this.pane)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dragOverCounter--;
|
||||
|
||||
if (this.dragOverCounter === 0) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
private onDragEnd(e: DragEvent): void {
|
||||
if (!this.context.draggable) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dragOverCounter = 0;
|
||||
this.render();
|
||||
this.context.draggable = null;
|
||||
}
|
||||
|
||||
private onDrop(e: DragEvent): void {
|
||||
if (!this.context.draggable) {
|
||||
return;
|
||||
}
|
||||
|
||||
EventHelper.stop(e);
|
||||
|
||||
this.dragOverCounter = 0;
|
||||
this.render();
|
||||
|
||||
if (this.dnd.canDrop(this.context.draggable.pane, this.pane) && this.context.draggable !== this) {
|
||||
this._onDidDrop.fire({ from: this.context.draggable.pane, to: this.pane });
|
||||
}
|
||||
|
||||
this.context.draggable = null;
|
||||
}
|
||||
|
||||
private render(): void {
|
||||
let backgroundColor: string | null = null;
|
||||
|
||||
if (this.dragOverCounter > 0) {
|
||||
backgroundColor = this.pane.dropBackground ?? PaneDraggable.DefaultDragOverBackgroundColor.toString();
|
||||
}
|
||||
|
||||
this.pane.dropTargetElement.style.backgroundColor = backgroundColor || '';
|
||||
}
|
||||
}
|
||||
|
||||
export interface IPaneDndController {
|
||||
canDrag(pane: Pane): boolean;
|
||||
canDrop(pane: Pane, overPane: Pane): boolean;
|
||||
}
|
||||
|
||||
export class DefaultPaneDndController implements IPaneDndController {
|
||||
|
||||
canDrag(pane: Pane): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
canDrop(pane: Pane, overPane: Pane): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IPaneViewOptions {
|
||||
dnd?: IPaneDndController;
|
||||
orientation?: Orientation;
|
||||
}
|
||||
|
||||
interface IPaneItem {
|
||||
pane: Pane;
|
||||
disposable: IDisposable;
|
||||
}
|
||||
|
||||
export class PaneView extends Disposable {
|
||||
|
||||
private dnd: IPaneDndController | undefined;
|
||||
private dndContext: IDndContext = { draggable: null };
|
||||
readonly element: HTMLElement;
|
||||
private paneItems: IPaneItem[] = [];
|
||||
private orthogonalSize: number = 0;
|
||||
private size: number = 0;
|
||||
private splitview: SplitView;
|
||||
private animationTimer: number | undefined = undefined;
|
||||
|
||||
private _onDidDrop = this._register(new Emitter<{ from: Pane; to: Pane }>());
|
||||
readonly onDidDrop: Event<{ from: Pane; to: Pane }> = this._onDidDrop.event;
|
||||
|
||||
orientation: Orientation;
|
||||
private boundarySashes: IBoundarySashes | undefined;
|
||||
readonly onDidSashChange: Event<number>;
|
||||
readonly onDidSashReset: Event<number>;
|
||||
readonly onDidScroll: Event<ScrollEvent>;
|
||||
|
||||
constructor(container: HTMLElement, options: IPaneViewOptions = {}) {
|
||||
super();
|
||||
|
||||
this.dnd = options.dnd;
|
||||
this.orientation = options.orientation ?? Orientation.VERTICAL;
|
||||
this.element = append(container, $('.monaco-pane-view'));
|
||||
this.splitview = this._register(new SplitView(this.element, { orientation: this.orientation }));
|
||||
this.onDidSashReset = this.splitview.onDidSashReset;
|
||||
this.onDidSashChange = this.splitview.onDidSashChange;
|
||||
this.onDidScroll = this.splitview.onDidScroll;
|
||||
|
||||
const eventDisposables = this._register(new DisposableStore());
|
||||
const onKeyDown = this._register(new DomEmitter(this.element, 'keydown'));
|
||||
const onHeaderKeyDown = Event.map(Event.filter(onKeyDown.event, e => isHTMLElement(e.target) && e.target.classList.contains('pane-header'), eventDisposables), e => new StandardKeyboardEvent(e), eventDisposables);
|
||||
|
||||
this._register(Event.filter(onHeaderKeyDown, e => e.keyCode === KeyCode.UpArrow, eventDisposables)(() => this.focusPrevious()));
|
||||
this._register(Event.filter(onHeaderKeyDown, e => e.keyCode === KeyCode.DownArrow, eventDisposables)(() => this.focusNext()));
|
||||
}
|
||||
|
||||
addPane(pane: Pane, size: number, index = this.splitview.length): void {
|
||||
const disposables = new DisposableStore();
|
||||
pane.onDidChangeExpansionState(this.setupAnimation, this, disposables);
|
||||
|
||||
const paneItem = { pane: pane, disposable: disposables };
|
||||
this.paneItems.splice(index, 0, paneItem);
|
||||
pane.orientation = this.orientation;
|
||||
pane.orthogonalSize = this.orthogonalSize;
|
||||
this.splitview.addView(pane, size, index);
|
||||
|
||||
if (this.dnd) {
|
||||
const draggable = new PaneDraggable(pane, this.dnd, this.dndContext);
|
||||
disposables.add(draggable);
|
||||
disposables.add(draggable.onDidDrop(this._onDidDrop.fire, this._onDidDrop));
|
||||
}
|
||||
}
|
||||
|
||||
removePane(pane: Pane): void {
|
||||
const index = this.paneItems.findIndex(item => item.pane === pane);
|
||||
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.splitview.removeView(index, pane.isExpanded() ? Sizing.Distribute : undefined);
|
||||
const paneItem = this.paneItems.splice(index, 1)[0];
|
||||
paneItem.disposable.dispose();
|
||||
}
|
||||
|
||||
movePane(from: Pane, to: Pane): void {
|
||||
const fromIndex = this.paneItems.findIndex(item => item.pane === from);
|
||||
const toIndex = this.paneItems.findIndex(item => item.pane === to);
|
||||
|
||||
if (fromIndex === -1 || toIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [paneItem] = this.paneItems.splice(fromIndex, 1);
|
||||
this.paneItems.splice(toIndex, 0, paneItem);
|
||||
|
||||
this.splitview.moveView(fromIndex, toIndex);
|
||||
}
|
||||
|
||||
resizePane(pane: Pane, size: number): void {
|
||||
const index = this.paneItems.findIndex(item => item.pane === pane);
|
||||
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.splitview.resizeView(index, size);
|
||||
}
|
||||
|
||||
getPaneSize(pane: Pane): number {
|
||||
const index = this.paneItems.findIndex(item => item.pane === pane);
|
||||
|
||||
if (index === -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return this.splitview.getViewSize(index);
|
||||
}
|
||||
|
||||
layout(height: number, width: number): void {
|
||||
this.orthogonalSize = this.orientation === Orientation.VERTICAL ? width : height;
|
||||
this.size = this.orientation === Orientation.HORIZONTAL ? width : height;
|
||||
|
||||
for (const paneItem of this.paneItems) {
|
||||
paneItem.pane.orthogonalSize = this.orthogonalSize;
|
||||
}
|
||||
|
||||
this.splitview.layout(this.size);
|
||||
}
|
||||
|
||||
setBoundarySashes(sashes: IBoundarySashes) {
|
||||
this.boundarySashes = sashes;
|
||||
this.updateSplitviewOrthogonalSashes(sashes);
|
||||
}
|
||||
|
||||
private updateSplitviewOrthogonalSashes(sashes: IBoundarySashes | undefined) {
|
||||
if (this.orientation === Orientation.VERTICAL) {
|
||||
this.splitview.orthogonalStartSash = sashes?.left;
|
||||
this.splitview.orthogonalEndSash = sashes?.right;
|
||||
} else {
|
||||
this.splitview.orthogonalEndSash = sashes?.bottom;
|
||||
}
|
||||
}
|
||||
|
||||
flipOrientation(height: number, width: number): void {
|
||||
this.orientation = this.orientation === Orientation.VERTICAL ? Orientation.HORIZONTAL : Orientation.VERTICAL;
|
||||
const paneSizes = this.paneItems.map(pane => this.getPaneSize(pane.pane));
|
||||
|
||||
this.splitview.dispose();
|
||||
clearNode(this.element);
|
||||
|
||||
this.splitview = this._register(new SplitView(this.element, { orientation: this.orientation }));
|
||||
this.updateSplitviewOrthogonalSashes(this.boundarySashes);
|
||||
|
||||
const newOrthogonalSize = this.orientation === Orientation.VERTICAL ? width : height;
|
||||
const newSize = this.orientation === Orientation.HORIZONTAL ? width : height;
|
||||
|
||||
this.paneItems.forEach((pane, index) => {
|
||||
pane.pane.orthogonalSize = newOrthogonalSize;
|
||||
pane.pane.orientation = this.orientation;
|
||||
|
||||
const viewSize = this.size === 0 ? 0 : (newSize * paneSizes[index]) / this.size;
|
||||
this.splitview.addView(pane.pane, viewSize, index);
|
||||
});
|
||||
|
||||
this.size = newSize;
|
||||
this.orthogonalSize = newOrthogonalSize;
|
||||
|
||||
this.splitview.layout(this.size);
|
||||
}
|
||||
|
||||
private setupAnimation(): void {
|
||||
if (typeof this.animationTimer === 'number') {
|
||||
getWindow(this.element).clearTimeout(this.animationTimer);
|
||||
}
|
||||
|
||||
this.element.classList.add('animated');
|
||||
|
||||
this.animationTimer = getWindow(this.element).setTimeout(() => {
|
||||
this.animationTimer = undefined;
|
||||
this.element.classList.remove('animated');
|
||||
}, 200);
|
||||
}
|
||||
|
||||
private getPaneHeaderElements(): HTMLElement[] {
|
||||
return [...this.element.querySelectorAll('.pane-header')] as HTMLElement[];
|
||||
}
|
||||
|
||||
private focusPrevious(): void {
|
||||
const headers = this.getPaneHeaderElements();
|
||||
const index = headers.indexOf(this.element.ownerDocument.activeElement as HTMLElement);
|
||||
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
headers[Math.max(index - 1, 0)].focus();
|
||||
}
|
||||
|
||||
private focusNext(): void {
|
||||
const headers = this.getPaneHeaderElements();
|
||||
const index = headers.indexOf(this.element.ownerDocument.activeElement as HTMLElement);
|
||||
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
headers[Math.min(index + 1, headers.length - 1)].focus();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this.paneItems.forEach(i => i.disposable.dispose());
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-split-view2 {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-split-view2 > .sash-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.monaco-split-view2 > .sash-container > .monaco-sash {
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
.monaco-split-view2 > .monaco-scrollable-element {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-split-view2 > .monaco-scrollable-element > .split-view-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.monaco-split-view2 > .monaco-scrollable-element > .split-view-container > .split-view-view {
|
||||
white-space: initial;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.monaco-split-view2 > .monaco-scrollable-element > .split-view-container > .split-view-view:not(.visible) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-split-view2.vertical > .monaco-scrollable-element > .split-view-container > .split-view-view {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monaco-split-view2.horizontal > .monaco-scrollable-element > .split-view-container > .split-view-view {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-split-view2.separator-border > .monaco-scrollable-element > .split-view-container > .split-view-view:not(:first-child)::before {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
background-color: var(--separator-border);
|
||||
}
|
||||
|
||||
.monaco-split-view2.separator-border.horizontal > .monaco-scrollable-element > .split-view-container > .split-view-view:not(:first-child)::before {
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.monaco-split-view2.separator-border.vertical > .monaco-scrollable-element > .split-view-container > .split-view-view:not(:first-child)::before {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,271 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
export interface ITelemetryData {
|
||||
readonly from?: string;
|
||||
readonly target?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export type WorkbenchActionExecutedClassification = {
|
||||
id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the action that was run.' };
|
||||
from: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the component the action was run from.' };
|
||||
detail?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Optional details about how the action was run, e.g which keybinding was used.' };
|
||||
owner: 'bpasero';
|
||||
comment: 'Provides insight into actions that are executed within the workbench.';
|
||||
};
|
||||
|
||||
export type WorkbenchActionExecutedEvent = {
|
||||
id: string;
|
||||
from: string;
|
||||
detail?: string;
|
||||
};
|
||||
|
||||
export interface IAction {
|
||||
readonly id: string;
|
||||
label: string;
|
||||
tooltip: string;
|
||||
class: string | undefined;
|
||||
enabled: boolean;
|
||||
checked?: boolean;
|
||||
run(...args: unknown[]): unknown;
|
||||
}
|
||||
|
||||
export interface IActionRunner extends IDisposable {
|
||||
readonly onDidRun: Event<IRunEvent>;
|
||||
readonly onWillRun: Event<IRunEvent>;
|
||||
|
||||
run(action: IAction, context?: unknown): unknown;
|
||||
}
|
||||
|
||||
export interface IActionChangeEvent {
|
||||
readonly label?: string;
|
||||
readonly tooltip?: string;
|
||||
readonly class?: string;
|
||||
readonly enabled?: boolean;
|
||||
readonly checked?: boolean;
|
||||
}
|
||||
|
||||
export class Action extends Disposable implements IAction {
|
||||
|
||||
protected _onDidChange = this._register(new Emitter<IActionChangeEvent>());
|
||||
readonly onDidChange = this._onDidChange.event;
|
||||
|
||||
protected readonly _id: string;
|
||||
protected _label: string;
|
||||
protected _tooltip: string | undefined;
|
||||
protected _cssClass: string | undefined;
|
||||
protected _enabled: boolean = true;
|
||||
protected _checked?: boolean;
|
||||
protected readonly _actionCallback?: (event?: unknown) => unknown;
|
||||
|
||||
constructor(id: string, label: string = '', cssClass: string = '', enabled: boolean = true, actionCallback?: (event?: unknown) => unknown) {
|
||||
super();
|
||||
this._id = id;
|
||||
this._label = label;
|
||||
this._cssClass = cssClass;
|
||||
this._enabled = enabled;
|
||||
this._actionCallback = actionCallback;
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get label(): string {
|
||||
return this._label;
|
||||
}
|
||||
|
||||
set label(value: string) {
|
||||
this._setLabel(value);
|
||||
}
|
||||
|
||||
private _setLabel(value: string): void {
|
||||
if (this._label !== value) {
|
||||
this._label = value;
|
||||
this._onDidChange.fire({ label: value });
|
||||
}
|
||||
}
|
||||
|
||||
get tooltip(): string {
|
||||
return this._tooltip || '';
|
||||
}
|
||||
|
||||
set tooltip(value: string) {
|
||||
this._setTooltip(value);
|
||||
}
|
||||
|
||||
protected _setTooltip(value: string): void {
|
||||
if (this._tooltip !== value) {
|
||||
this._tooltip = value;
|
||||
this._onDidChange.fire({ tooltip: value });
|
||||
}
|
||||
}
|
||||
|
||||
get class(): string | undefined {
|
||||
return this._cssClass;
|
||||
}
|
||||
|
||||
set class(value: string | undefined) {
|
||||
this._setClass(value);
|
||||
}
|
||||
|
||||
protected _setClass(value: string | undefined): void {
|
||||
if (this._cssClass !== value) {
|
||||
this._cssClass = value;
|
||||
this._onDidChange.fire({ class: value });
|
||||
}
|
||||
}
|
||||
|
||||
get enabled(): boolean {
|
||||
return this._enabled;
|
||||
}
|
||||
|
||||
set enabled(value: boolean) {
|
||||
this._setEnabled(value);
|
||||
}
|
||||
|
||||
protected _setEnabled(value: boolean): void {
|
||||
if (this._enabled !== value) {
|
||||
this._enabled = value;
|
||||
this._onDidChange.fire({ enabled: value });
|
||||
}
|
||||
}
|
||||
|
||||
get checked(): boolean | undefined {
|
||||
return this._checked;
|
||||
}
|
||||
|
||||
set checked(value: boolean | undefined) {
|
||||
this._setChecked(value);
|
||||
}
|
||||
|
||||
protected _setChecked(value: boolean | undefined): void {
|
||||
if (this._checked !== value) {
|
||||
this._checked = value;
|
||||
this._onDidChange.fire({ checked: value });
|
||||
}
|
||||
}
|
||||
|
||||
async run(event?: unknown, data?: ITelemetryData): Promise<void> {
|
||||
if (this._actionCallback) {
|
||||
await this._actionCallback(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface IRunEvent {
|
||||
readonly action: IAction;
|
||||
readonly error?: Error;
|
||||
}
|
||||
|
||||
export class ActionRunner extends Disposable implements IActionRunner {
|
||||
|
||||
private readonly _onWillRun = this._register(new Emitter<IRunEvent>());
|
||||
readonly onWillRun = this._onWillRun.event;
|
||||
|
||||
private readonly _onDidRun = this._register(new Emitter<IRunEvent>());
|
||||
readonly onDidRun = this._onDidRun.event;
|
||||
|
||||
async run(action: IAction, context?: unknown): Promise<void> {
|
||||
if (!action.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._onWillRun.fire({ action });
|
||||
|
||||
let error: Error | undefined = undefined;
|
||||
try {
|
||||
await this.runAction(action, context);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
this._onDidRun.fire({ action, error });
|
||||
}
|
||||
|
||||
protected async runAction(action: IAction, context?: unknown): Promise<void> {
|
||||
await action.run(context);
|
||||
}
|
||||
}
|
||||
|
||||
export class Separator implements IAction {
|
||||
|
||||
/**
|
||||
* Joins all non-empty lists of actions with separators.
|
||||
*/
|
||||
public static join(...actionLists: readonly IAction[][]) {
|
||||
let out: IAction[] = [];
|
||||
for (const list of actionLists) {
|
||||
if (!list.length) {
|
||||
// skip
|
||||
} else if (out.length) {
|
||||
out = [...out, new Separator(), ...list];
|
||||
} else {
|
||||
out = list;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static readonly ID = 'vs.actions.separator';
|
||||
|
||||
readonly id: string = Separator.ID;
|
||||
|
||||
readonly label: string = '';
|
||||
readonly tooltip: string = '';
|
||||
readonly class: string = 'separator';
|
||||
readonly enabled: boolean = false;
|
||||
readonly checked: boolean = false;
|
||||
async run() { }
|
||||
}
|
||||
|
||||
export class SubmenuAction implements IAction {
|
||||
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly class: string | undefined;
|
||||
readonly tooltip: string = '';
|
||||
readonly enabled: boolean = true;
|
||||
readonly checked: undefined = undefined;
|
||||
|
||||
private readonly _actions: readonly IAction[];
|
||||
get actions(): readonly IAction[] { return this._actions; }
|
||||
|
||||
constructor(id: string, label: string, actions: readonly IAction[], cssClass?: string) {
|
||||
this.id = id;
|
||||
this.label = label;
|
||||
this.class = cssClass;
|
||||
this._actions = actions;
|
||||
}
|
||||
|
||||
async run(): Promise<void> { }
|
||||
}
|
||||
|
||||
export class EmptySubmenuAction extends Action {
|
||||
|
||||
static readonly ID = 'vs.actions.empty';
|
||||
|
||||
constructor() {
|
||||
super(EmptySubmenuAction.ID, nls.localize('submenu.empty', '(empty)'), undefined, false);
|
||||
}
|
||||
}
|
||||
|
||||
export function toAction(props: { id: string; label: string; tooltip?: string; enabled?: boolean; checked?: boolean; class?: string; run: Function }): IAction {
|
||||
return {
|
||||
id: props.id,
|
||||
label: props.label,
|
||||
tooltip: props.tooltip ?? props.label,
|
||||
class: props.class,
|
||||
enabled: props.enabled ?? true,
|
||||
checked: props.checked,
|
||||
run: async (...args: unknown[]) => props.run(...args),
|
||||
};
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// ESM-comment-begin
|
||||
export const isESM = false;
|
||||
// ESM-comment-end
|
||||
// ESM-uncomment-begin
|
||||
// export const isESM = true;
|
||||
// ESM-uncomment-end
|
||||
|
||||
export const enum LoaderEventType {
|
||||
LoaderAvailable = 1,
|
||||
|
||||
BeginLoadingScript = 10,
|
||||
EndLoadingScriptOK = 11,
|
||||
EndLoadingScriptError = 12,
|
||||
|
||||
BeginInvokeFactory = 21,
|
||||
EndInvokeFactory = 22,
|
||||
|
||||
NodeBeginEvaluatingScript = 31,
|
||||
NodeEndEvaluatingScript = 32,
|
||||
|
||||
NodeBeginNativeRequire = 33,
|
||||
NodeEndNativeRequire = 34,
|
||||
|
||||
CachedDataFound = 60,
|
||||
CachedDataMissed = 61,
|
||||
CachedDataRejected = 62,
|
||||
CachedDataCreated = 63,
|
||||
}
|
||||
|
||||
export abstract class LoaderStats {
|
||||
abstract get amdLoad(): [string, number][];
|
||||
abstract get amdInvoke(): [string, number][];
|
||||
abstract get nodeRequire(): [string, number][];
|
||||
abstract get nodeEval(): [string, number][];
|
||||
abstract get nodeRequireTotal(): number;
|
||||
|
||||
static get(): LoaderStats {
|
||||
const amdLoadScript = new Map<string, number>();
|
||||
const amdInvokeFactory = new Map<string, number>();
|
||||
const nodeRequire = new Map<string, number>();
|
||||
const nodeEval = new Map<string, number>();
|
||||
|
||||
function mark(map: Map<string, number>, stat: LoaderEvent) {
|
||||
if (map.has(stat.detail)) {
|
||||
// console.warn('BAD events, DOUBLE start', stat);
|
||||
// map.delete(stat.detail);
|
||||
return;
|
||||
}
|
||||
map.set(stat.detail, -stat.timestamp);
|
||||
}
|
||||
|
||||
function diff(map: Map<string, number>, stat: LoaderEvent) {
|
||||
const duration = map.get(stat.detail);
|
||||
if (!duration) {
|
||||
// console.warn('BAD events, end WITHOUT start', stat);
|
||||
// map.delete(stat.detail);
|
||||
return;
|
||||
}
|
||||
if (duration >= 0) {
|
||||
// console.warn('BAD events, DOUBLE end', stat);
|
||||
// map.delete(stat.detail);
|
||||
return;
|
||||
}
|
||||
map.set(stat.detail, duration + stat.timestamp);
|
||||
}
|
||||
|
||||
let stats: readonly LoaderEvent[] = [];
|
||||
if (typeof require === 'function' && typeof require.getStats === 'function') {
|
||||
stats = require.getStats().slice(0).sort((a, b) => a.timestamp - b.timestamp);
|
||||
}
|
||||
|
||||
for (const stat of stats) {
|
||||
switch (stat.type) {
|
||||
case LoaderEventType.BeginLoadingScript:
|
||||
mark(amdLoadScript, stat);
|
||||
break;
|
||||
case LoaderEventType.EndLoadingScriptOK:
|
||||
case LoaderEventType.EndLoadingScriptError:
|
||||
diff(amdLoadScript, stat);
|
||||
break;
|
||||
|
||||
case LoaderEventType.BeginInvokeFactory:
|
||||
mark(amdInvokeFactory, stat);
|
||||
break;
|
||||
case LoaderEventType.EndInvokeFactory:
|
||||
diff(amdInvokeFactory, stat);
|
||||
break;
|
||||
|
||||
case LoaderEventType.NodeBeginNativeRequire:
|
||||
mark(nodeRequire, stat);
|
||||
break;
|
||||
case LoaderEventType.NodeEndNativeRequire:
|
||||
diff(nodeRequire, stat);
|
||||
break;
|
||||
|
||||
case LoaderEventType.NodeBeginEvaluatingScript:
|
||||
mark(nodeEval, stat);
|
||||
break;
|
||||
case LoaderEventType.NodeEndEvaluatingScript:
|
||||
diff(nodeEval, stat);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let nodeRequireTotal = 0;
|
||||
nodeRequire.forEach(value => nodeRequireTotal += value);
|
||||
|
||||
function to2dArray(map: Map<string, number>): [string, number][] {
|
||||
const res: [string, number][] = [];
|
||||
map.forEach((value, index) => res.push([index, value]));
|
||||
return res;
|
||||
}
|
||||
|
||||
return {
|
||||
amdLoad: to2dArray(amdLoadScript),
|
||||
amdInvoke: to2dArray(amdInvokeFactory),
|
||||
nodeRequire: to2dArray(nodeRequire),
|
||||
nodeEval: to2dArray(nodeEval),
|
||||
nodeRequireTotal
|
||||
};
|
||||
}
|
||||
|
||||
static toMarkdownTable(header: string[], rows: Array<Array<{ toString(): string } | undefined>>): string {
|
||||
let result = '';
|
||||
|
||||
const lengths: number[] = [];
|
||||
header.forEach((cell, ci) => {
|
||||
lengths[ci] = cell.length;
|
||||
});
|
||||
rows.forEach(row => {
|
||||
row.forEach((cell, ci) => {
|
||||
if (typeof cell === 'undefined') {
|
||||
cell = row[ci] = '-';
|
||||
}
|
||||
const len = cell.toString().length;
|
||||
lengths[ci] = Math.max(len, lengths[ci]);
|
||||
});
|
||||
});
|
||||
|
||||
// header
|
||||
header.forEach((cell, ci) => { result += `| ${cell + ' '.repeat(lengths[ci] - cell.toString().length)} `; });
|
||||
result += '|\n';
|
||||
header.forEach((_cell, ci) => { result += `| ${'-'.repeat(lengths[ci])} `; });
|
||||
result += '|\n';
|
||||
|
||||
// cells
|
||||
rows.forEach(row => {
|
||||
row.forEach((cell, ci) => {
|
||||
if (typeof cell !== 'undefined') {
|
||||
result += `| ${cell + ' '.repeat(lengths[ci] - cell.toString().length)} `;
|
||||
}
|
||||
});
|
||||
result += '|\n';
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,441 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Lazy } from 'vs/base/common/lazy';
|
||||
import * as streams from 'vs/base/common/stream';
|
||||
|
||||
declare const Buffer: any;
|
||||
|
||||
const hasBuffer = (typeof Buffer !== 'undefined');
|
||||
const indexOfTable = new Lazy(() => new Uint8Array(256));
|
||||
|
||||
let textEncoder: TextEncoder | null;
|
||||
let textDecoder: TextDecoder | null;
|
||||
|
||||
export class VSBuffer {
|
||||
|
||||
/**
|
||||
* When running in a nodejs context, the backing store for the returned `VSBuffer` instance
|
||||
* might use a nodejs Buffer allocated from node's Buffer pool, which is not transferrable.
|
||||
*/
|
||||
static alloc(byteLength: number): VSBuffer {
|
||||
if (hasBuffer) {
|
||||
return new VSBuffer(Buffer.allocUnsafe(byteLength));
|
||||
} else {
|
||||
return new VSBuffer(new Uint8Array(byteLength));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When running in a nodejs context, if `actual` is not a nodejs Buffer, the backing store for
|
||||
* the returned `VSBuffer` instance might use a nodejs Buffer allocated from node's Buffer pool,
|
||||
* which is not transferrable.
|
||||
*/
|
||||
static wrap(actual: Uint8Array): VSBuffer {
|
||||
if (hasBuffer && !(Buffer.isBuffer(actual))) {
|
||||
// https://nodejs.org/dist/latest-v10.x/docs/api/buffer.html#buffer_class_method_buffer_from_arraybuffer_byteoffset_length
|
||||
// Create a zero-copy Buffer wrapper around the ArrayBuffer pointed to by the Uint8Array
|
||||
actual = Buffer.from(actual.buffer, actual.byteOffset, actual.byteLength);
|
||||
}
|
||||
return new VSBuffer(actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* When running in a nodejs context, the backing store for the returned `VSBuffer` instance
|
||||
* might use a nodejs Buffer allocated from node's Buffer pool, which is not transferrable.
|
||||
*/
|
||||
static fromString(source: string, options?: { dontUseNodeBuffer?: boolean }): VSBuffer {
|
||||
const dontUseNodeBuffer = options?.dontUseNodeBuffer || false;
|
||||
if (!dontUseNodeBuffer && hasBuffer) {
|
||||
return new VSBuffer(Buffer.from(source));
|
||||
} else {
|
||||
if (!textEncoder) {
|
||||
textEncoder = new TextEncoder();
|
||||
}
|
||||
return new VSBuffer(textEncoder.encode(source));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When running in a nodejs context, the backing store for the returned `VSBuffer` instance
|
||||
* might use a nodejs Buffer allocated from node's Buffer pool, which is not transferrable.
|
||||
*/
|
||||
static fromByteArray(source: number[]): VSBuffer {
|
||||
const result = VSBuffer.alloc(source.length);
|
||||
for (let i = 0, len = source.length; i < len; i++) {
|
||||
result.buffer[i] = source[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* When running in a nodejs context, the backing store for the returned `VSBuffer` instance
|
||||
* might use a nodejs Buffer allocated from node's Buffer pool, which is not transferrable.
|
||||
*/
|
||||
static concat(buffers: VSBuffer[], totalLength?: number): VSBuffer {
|
||||
if (typeof totalLength === 'undefined') {
|
||||
totalLength = 0;
|
||||
for (let i = 0, len = buffers.length; i < len; i++) {
|
||||
totalLength += buffers[i].byteLength;
|
||||
}
|
||||
}
|
||||
|
||||
const ret = VSBuffer.alloc(totalLength);
|
||||
let offset = 0;
|
||||
for (let i = 0, len = buffers.length; i < len; i++) {
|
||||
const element = buffers[i];
|
||||
ret.set(element, offset);
|
||||
offset += element.byteLength;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
readonly buffer: Uint8Array;
|
||||
readonly byteLength: number;
|
||||
|
||||
private constructor(buffer: Uint8Array) {
|
||||
this.buffer = buffer;
|
||||
this.byteLength = this.buffer.byteLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* When running in a nodejs context, the backing store for the returned `VSBuffer` instance
|
||||
* might use a nodejs Buffer allocated from node's Buffer pool, which is not transferrable.
|
||||
*/
|
||||
clone(): VSBuffer {
|
||||
const result = VSBuffer.alloc(this.byteLength);
|
||||
result.set(this);
|
||||
return result;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
if (hasBuffer) {
|
||||
return this.buffer.toString();
|
||||
} else {
|
||||
if (!textDecoder) {
|
||||
textDecoder = new TextDecoder();
|
||||
}
|
||||
return textDecoder.decode(this.buffer);
|
||||
}
|
||||
}
|
||||
|
||||
slice(start?: number, end?: number): VSBuffer {
|
||||
// IMPORTANT: use subarray instead of slice because TypedArray#slice
|
||||
// creates shallow copy and NodeBuffer#slice doesn't. The use of subarray
|
||||
// ensures the same, performance, behaviour.
|
||||
return new VSBuffer(this.buffer.subarray(start, end));
|
||||
}
|
||||
|
||||
set(array: VSBuffer, offset?: number): void;
|
||||
set(array: Uint8Array, offset?: number): void;
|
||||
set(array: ArrayBuffer, offset?: number): void;
|
||||
set(array: ArrayBufferView, offset?: number): void;
|
||||
set(array: VSBuffer | Uint8Array | ArrayBuffer | ArrayBufferView, offset?: number): void;
|
||||
set(array: VSBuffer | Uint8Array | ArrayBuffer | ArrayBufferView, offset?: number): void {
|
||||
if (array instanceof VSBuffer) {
|
||||
this.buffer.set(array.buffer, offset);
|
||||
} else if (array instanceof Uint8Array) {
|
||||
this.buffer.set(array, offset);
|
||||
} else if (array instanceof ArrayBuffer) {
|
||||
this.buffer.set(new Uint8Array(array), offset);
|
||||
} else if (ArrayBuffer.isView(array)) {
|
||||
this.buffer.set(new Uint8Array(array.buffer, array.byteOffset, array.byteLength), offset);
|
||||
} else {
|
||||
throw new Error(`Unknown argument 'array'`);
|
||||
}
|
||||
}
|
||||
|
||||
readUInt32BE(offset: number): number {
|
||||
return readUInt32BE(this.buffer, offset);
|
||||
}
|
||||
|
||||
writeUInt32BE(value: number, offset: number): void {
|
||||
writeUInt32BE(this.buffer, value, offset);
|
||||
}
|
||||
|
||||
readUInt32LE(offset: number): number {
|
||||
return readUInt32LE(this.buffer, offset);
|
||||
}
|
||||
|
||||
writeUInt32LE(value: number, offset: number): void {
|
||||
writeUInt32LE(this.buffer, value, offset);
|
||||
}
|
||||
|
||||
readUInt8(offset: number): number {
|
||||
return readUInt8(this.buffer, offset);
|
||||
}
|
||||
|
||||
writeUInt8(value: number, offset: number): void {
|
||||
writeUInt8(this.buffer, value, offset);
|
||||
}
|
||||
|
||||
indexOf(subarray: VSBuffer | Uint8Array, offset = 0) {
|
||||
return binaryIndexOf(this.buffer, subarray instanceof VSBuffer ? subarray.buffer : subarray, offset);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like String.indexOf, but works on Uint8Arrays.
|
||||
* Uses the boyer-moore-horspool algorithm to be reasonably speedy.
|
||||
*/
|
||||
export function binaryIndexOf(haystack: Uint8Array, needle: Uint8Array, offset = 0): number {
|
||||
const needleLen = needle.byteLength;
|
||||
const haystackLen = haystack.byteLength;
|
||||
|
||||
if (needleLen === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (needleLen === 1) {
|
||||
return haystack.indexOf(needle[0]);
|
||||
}
|
||||
|
||||
if (needleLen > haystackLen - offset) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// find index of the subarray using boyer-moore-horspool algorithm
|
||||
const table = indexOfTable.value;
|
||||
table.fill(needle.length);
|
||||
for (let i = 0; i < needle.length; i++) {
|
||||
table[needle[i]] = needle.length - i - 1;
|
||||
}
|
||||
|
||||
let i = offset + needle.length - 1;
|
||||
let j = i;
|
||||
let result = -1;
|
||||
while (i < haystackLen) {
|
||||
if (haystack[i] === needle[j]) {
|
||||
if (j === 0) {
|
||||
result = i;
|
||||
break;
|
||||
}
|
||||
|
||||
i--;
|
||||
j--;
|
||||
} else {
|
||||
i += Math.max(needle.length - j, table[haystack[i]]);
|
||||
j = needle.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function readUInt16LE(source: Uint8Array, offset: number): number {
|
||||
return (
|
||||
((source[offset + 0] << 0) >>> 0) |
|
||||
((source[offset + 1] << 8) >>> 0)
|
||||
);
|
||||
}
|
||||
|
||||
export function writeUInt16LE(destination: Uint8Array, value: number, offset: number): void {
|
||||
destination[offset + 0] = (value & 0b11111111);
|
||||
value = value >>> 8;
|
||||
destination[offset + 1] = (value & 0b11111111);
|
||||
}
|
||||
|
||||
export function readUInt32BE(source: Uint8Array, offset: number): number {
|
||||
return (
|
||||
source[offset] * 2 ** 24
|
||||
+ source[offset + 1] * 2 ** 16
|
||||
+ source[offset + 2] * 2 ** 8
|
||||
+ source[offset + 3]
|
||||
);
|
||||
}
|
||||
|
||||
export function writeUInt32BE(destination: Uint8Array, value: number, offset: number): void {
|
||||
destination[offset + 3] = value;
|
||||
value = value >>> 8;
|
||||
destination[offset + 2] = value;
|
||||
value = value >>> 8;
|
||||
destination[offset + 1] = value;
|
||||
value = value >>> 8;
|
||||
destination[offset] = value;
|
||||
}
|
||||
|
||||
export function readUInt32LE(source: Uint8Array, offset: number): number {
|
||||
return (
|
||||
((source[offset + 0] << 0) >>> 0) |
|
||||
((source[offset + 1] << 8) >>> 0) |
|
||||
((source[offset + 2] << 16) >>> 0) |
|
||||
((source[offset + 3] << 24) >>> 0)
|
||||
);
|
||||
}
|
||||
|
||||
export function writeUInt32LE(destination: Uint8Array, value: number, offset: number): void {
|
||||
destination[offset + 0] = (value & 0b11111111);
|
||||
value = value >>> 8;
|
||||
destination[offset + 1] = (value & 0b11111111);
|
||||
value = value >>> 8;
|
||||
destination[offset + 2] = (value & 0b11111111);
|
||||
value = value >>> 8;
|
||||
destination[offset + 3] = (value & 0b11111111);
|
||||
}
|
||||
|
||||
export function readUInt8(source: Uint8Array, offset: number): number {
|
||||
return source[offset];
|
||||
}
|
||||
|
||||
export function writeUInt8(destination: Uint8Array, value: number, offset: number): void {
|
||||
destination[offset] = value;
|
||||
}
|
||||
|
||||
export interface VSBufferReadable extends streams.Readable<VSBuffer> { }
|
||||
|
||||
export interface VSBufferReadableStream extends streams.ReadableStream<VSBuffer> { }
|
||||
|
||||
export interface VSBufferWriteableStream extends streams.WriteableStream<VSBuffer> { }
|
||||
|
||||
export interface VSBufferReadableBufferedStream extends streams.ReadableBufferedStream<VSBuffer> { }
|
||||
|
||||
export function readableToBuffer(readable: VSBufferReadable): VSBuffer {
|
||||
return streams.consumeReadable<VSBuffer>(readable, chunks => VSBuffer.concat(chunks));
|
||||
}
|
||||
|
||||
export function bufferToReadable(buffer: VSBuffer): VSBufferReadable {
|
||||
return streams.toReadable<VSBuffer>(buffer);
|
||||
}
|
||||
|
||||
export function streamToBuffer(stream: streams.ReadableStream<VSBuffer>): Promise<VSBuffer> {
|
||||
return streams.consumeStream<VSBuffer>(stream, chunks => VSBuffer.concat(chunks));
|
||||
}
|
||||
|
||||
export async function bufferedStreamToBuffer(bufferedStream: streams.ReadableBufferedStream<VSBuffer>): Promise<VSBuffer> {
|
||||
if (bufferedStream.ended) {
|
||||
return VSBuffer.concat(bufferedStream.buffer);
|
||||
}
|
||||
|
||||
return VSBuffer.concat([
|
||||
|
||||
// Include already read chunks...
|
||||
...bufferedStream.buffer,
|
||||
|
||||
// ...and all additional chunks
|
||||
await streamToBuffer(bufferedStream.stream)
|
||||
]);
|
||||
}
|
||||
|
||||
export function bufferToStream(buffer: VSBuffer): streams.ReadableStream<VSBuffer> {
|
||||
return streams.toStream<VSBuffer>(buffer, chunks => VSBuffer.concat(chunks));
|
||||
}
|
||||
|
||||
export function streamToBufferReadableStream(stream: streams.ReadableStreamEvents<Uint8Array | string>): streams.ReadableStream<VSBuffer> {
|
||||
return streams.transform<Uint8Array | string, VSBuffer>(stream, { data: data => typeof data === 'string' ? VSBuffer.fromString(data) : VSBuffer.wrap(data) }, chunks => VSBuffer.concat(chunks));
|
||||
}
|
||||
|
||||
export function newWriteableBufferStream(options?: streams.WriteableStreamOptions): streams.WriteableStream<VSBuffer> {
|
||||
return streams.newWriteableStream<VSBuffer>(chunks => VSBuffer.concat(chunks), options);
|
||||
}
|
||||
|
||||
export function prefixedBufferReadable(prefix: VSBuffer, readable: VSBufferReadable): VSBufferReadable {
|
||||
return streams.prefixedReadable(prefix, readable, chunks => VSBuffer.concat(chunks));
|
||||
}
|
||||
|
||||
export function prefixedBufferStream(prefix: VSBuffer, stream: VSBufferReadableStream): VSBufferReadableStream {
|
||||
return streams.prefixedStream(prefix, stream, chunks => VSBuffer.concat(chunks));
|
||||
}
|
||||
|
||||
/** Decodes base64 to a uint8 array. URL-encoded and unpadded base64 is allowed. */
|
||||
export function decodeBase64(encoded: string) {
|
||||
let building = 0;
|
||||
let remainder = 0;
|
||||
let bufi = 0;
|
||||
|
||||
// The simpler way to do this is `Uint8Array.from(atob(str), c => c.charCodeAt(0))`,
|
||||
// but that's about 10-20x slower than this function in current Chromium versions.
|
||||
|
||||
const buffer = new Uint8Array(Math.floor(encoded.length / 4 * 3));
|
||||
const append = (value: number) => {
|
||||
switch (remainder) {
|
||||
case 3:
|
||||
buffer[bufi++] = building | value;
|
||||
remainder = 0;
|
||||
break;
|
||||
case 2:
|
||||
buffer[bufi++] = building | (value >>> 2);
|
||||
building = value << 6;
|
||||
remainder = 3;
|
||||
break;
|
||||
case 1:
|
||||
buffer[bufi++] = building | (value >>> 4);
|
||||
building = value << 4;
|
||||
remainder = 2;
|
||||
break;
|
||||
default:
|
||||
building = value << 2;
|
||||
remainder = 1;
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < encoded.length; i++) {
|
||||
const code = encoded.charCodeAt(i);
|
||||
// See https://datatracker.ietf.org/doc/html/rfc4648#section-4
|
||||
// This branchy code is about 3x faster than an indexOf on a base64 char string.
|
||||
if (code >= 65 && code <= 90) {
|
||||
append(code - 65); // A-Z starts ranges from char code 65 to 90
|
||||
} else if (code >= 97 && code <= 122) {
|
||||
append(code - 97 + 26); // a-z starts ranges from char code 97 to 122, starting at byte 26
|
||||
} else if (code >= 48 && code <= 57) {
|
||||
append(code - 48 + 52); // 0-9 starts ranges from char code 48 to 58, starting at byte 52
|
||||
} else if (code === 43 || code === 45) {
|
||||
append(62); // "+" or "-" for URLS
|
||||
} else if (code === 47 || code === 95) {
|
||||
append(63); // "/" or "_" for URLS
|
||||
} else if (code === 61) {
|
||||
break; // "="
|
||||
} else {
|
||||
throw new SyntaxError(`Unexpected base64 character ${encoded[i]}`);
|
||||
}
|
||||
}
|
||||
|
||||
const unpadded = bufi;
|
||||
while (remainder > 0) {
|
||||
append(0);
|
||||
}
|
||||
|
||||
// slice is needed to account for overestimation due to padding
|
||||
return VSBuffer.wrap(buffer).slice(0, unpadded);
|
||||
}
|
||||
|
||||
const base64Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
const base64UrlSafeAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
||||
|
||||
/** Encodes a buffer to a base64 string. */
|
||||
export function encodeBase64({ buffer }: VSBuffer, padded = true, urlSafe = false) {
|
||||
const dictionary = urlSafe ? base64UrlSafeAlphabet : base64Alphabet;
|
||||
let output = '';
|
||||
|
||||
const remainder = buffer.byteLength % 3;
|
||||
|
||||
let i = 0;
|
||||
for (; i < buffer.byteLength - remainder; i += 3) {
|
||||
const a = buffer[i + 0];
|
||||
const b = buffer[i + 1];
|
||||
const c = buffer[i + 2];
|
||||
|
||||
output += dictionary[a >>> 2];
|
||||
output += dictionary[(a << 4 | b >>> 4) & 0b111111];
|
||||
output += dictionary[(b << 2 | c >>> 6) & 0b111111];
|
||||
output += dictionary[c & 0b111111];
|
||||
}
|
||||
|
||||
if (remainder === 1) {
|
||||
const a = buffer[i + 0];
|
||||
output += dictionary[a >>> 2];
|
||||
output += dictionary[(a << 4) & 0b111111];
|
||||
if (padded) { output += '=='; }
|
||||
} else if (remainder === 2) {
|
||||
const a = buffer[i + 0];
|
||||
const b = buffer[i + 1];
|
||||
output += dictionary[a >>> 2];
|
||||
output += dictionary[(a << 4 | b >>> 4) & 0b111111];
|
||||
output += dictionary[(b << 2) & 0b111111];
|
||||
if (padded) { output += '='; }
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export interface CacheResult<T> extends IDisposable {
|
||||
promise: Promise<T>;
|
||||
}
|
||||
|
||||
export class Cache<T> {
|
||||
|
||||
private result: CacheResult<T> | null = null;
|
||||
constructor(private task: (ct: CancellationToken) => Promise<T>) { }
|
||||
|
||||
get(): CacheResult<T> {
|
||||
if (this.result) {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
const cts = new CancellationTokenSource();
|
||||
const promise = this.task(cts.token);
|
||||
|
||||
this.result = {
|
||||
promise,
|
||||
dispose: () => {
|
||||
this.result = null;
|
||||
cts.cancel();
|
||||
cts.dispose();
|
||||
}
|
||||
};
|
||||
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
|
||||
export function identity<T>(t: T): T {
|
||||
return t;
|
||||
}
|
||||
|
||||
interface ICacheOptions<TArg> {
|
||||
/**
|
||||
* The cache key is used to identify the cache entry.
|
||||
* Strict equality is used to compare cache keys.
|
||||
*/
|
||||
getCacheKey: (arg: TArg) => unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a LRU cache to make a given parametrized function cached.
|
||||
* Caches just the last key/value.
|
||||
*/
|
||||
export class LRUCachedFunction<TArg, TComputed> {
|
||||
private lastCache: TComputed | undefined = undefined;
|
||||
private lastArgKey: unknown | undefined = undefined;
|
||||
|
||||
private readonly _fn: (arg: TArg) => TComputed;
|
||||
private readonly _computeKey: (arg: TArg) => unknown;
|
||||
|
||||
constructor(fn: (arg: TArg) => TComputed);
|
||||
constructor(options: ICacheOptions<TArg>, fn: (arg: TArg) => TComputed);
|
||||
constructor(arg1: ICacheOptions<TArg> | ((arg: TArg) => TComputed), arg2?: (arg: TArg) => TComputed) {
|
||||
if (typeof arg1 === 'function') {
|
||||
this._fn = arg1;
|
||||
this._computeKey = identity;
|
||||
} else {
|
||||
this._fn = arg2!;
|
||||
this._computeKey = arg1.getCacheKey;
|
||||
}
|
||||
}
|
||||
|
||||
public get(arg: TArg): TComputed {
|
||||
const key = this._computeKey(arg);
|
||||
if (this.lastArgKey !== key) {
|
||||
this.lastArgKey = key;
|
||||
this.lastCache = this._fn(arg);
|
||||
}
|
||||
return this.lastCache!;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses an unbounded cache to memoize the results of the given function.
|
||||
*/
|
||||
export class CachedFunction<TArg, TComputed> {
|
||||
private readonly _map = new Map<TArg, TComputed>();
|
||||
private readonly _map2 = new Map<unknown, TComputed>();
|
||||
public get cachedValues(): ReadonlyMap<TArg, TComputed> {
|
||||
return this._map;
|
||||
}
|
||||
|
||||
private readonly _fn: (arg: TArg) => TComputed;
|
||||
private readonly _computeKey: (arg: TArg) => unknown;
|
||||
|
||||
constructor(fn: (arg: TArg) => TComputed);
|
||||
constructor(options: ICacheOptions<TArg>, fn: (arg: TArg) => TComputed);
|
||||
constructor(arg1: ICacheOptions<TArg> | ((arg: TArg) => TComputed), arg2?: (arg: TArg) => TComputed) {
|
||||
if (typeof arg1 === 'function') {
|
||||
this._fn = arg1;
|
||||
this._computeKey = identity;
|
||||
} else {
|
||||
this._fn = arg2!;
|
||||
this._computeKey = arg1.getCacheKey;
|
||||
}
|
||||
}
|
||||
|
||||
public get(arg: TArg): TComputed {
|
||||
const key = this._computeKey(arg);
|
||||
if (this._map2.has(key)) {
|
||||
return this._map2.get(key)!;
|
||||
}
|
||||
|
||||
const value = this._fn(arg);
|
||||
this._map.set(arg, value);
|
||||
this._map2.set(key, value);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { ThemeIcon } from 'vs/base/common/themables';
|
||||
import { isString } from 'vs/base/common/types';
|
||||
|
||||
|
||||
const _codiconFontCharacters: { [id: string]: number } = Object.create(null);
|
||||
|
||||
export function register(id: string, fontCharacter: number | string): ThemeIcon {
|
||||
if (isString(fontCharacter)) {
|
||||
const val = _codiconFontCharacters[fontCharacter];
|
||||
if (val === undefined) {
|
||||
throw new Error(`${id} references an unknown codicon: ${fontCharacter}`);
|
||||
}
|
||||
fontCharacter = val;
|
||||
}
|
||||
_codiconFontCharacters[id] = fontCharacter;
|
||||
return { id };
|
||||
}
|
||||
|
||||
/**
|
||||
* Only to be used by the iconRegistry.
|
||||
*/
|
||||
export function getCodiconFontCharacters(): { [id: string]: number } {
|
||||
return _codiconFontCharacters;
|
||||
}
|
||||
@@ -1,633 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
|
||||
function roundFloat(number: number, decimalPoints: number): number {
|
||||
const decimal = Math.pow(10, decimalPoints);
|
||||
return Math.round(number * decimal) / decimal;
|
||||
}
|
||||
|
||||
export class RGBA {
|
||||
_rgbaBrand: void = undefined;
|
||||
|
||||
/**
|
||||
* Red: integer in [0-255]
|
||||
*/
|
||||
readonly r: number;
|
||||
|
||||
/**
|
||||
* Green: integer in [0-255]
|
||||
*/
|
||||
readonly g: number;
|
||||
|
||||
/**
|
||||
* Blue: integer in [0-255]
|
||||
*/
|
||||
readonly b: number;
|
||||
|
||||
/**
|
||||
* Alpha: float in [0-1]
|
||||
*/
|
||||
readonly a: number;
|
||||
|
||||
constructor(r: number, g: number, b: number, a: number = 1) {
|
||||
this.r = Math.min(255, Math.max(0, r)) | 0;
|
||||
this.g = Math.min(255, Math.max(0, g)) | 0;
|
||||
this.b = Math.min(255, Math.max(0, b)) | 0;
|
||||
this.a = roundFloat(Math.max(Math.min(1, a), 0), 3);
|
||||
}
|
||||
|
||||
static equals(a: RGBA, b: RGBA): boolean {
|
||||
return a.r === b.r && a.g === b.g && a.b === b.b && a.a === b.a;
|
||||
}
|
||||
}
|
||||
|
||||
export class HSLA {
|
||||
|
||||
_hslaBrand: void = undefined;
|
||||
|
||||
/**
|
||||
* Hue: integer in [0, 360]
|
||||
*/
|
||||
readonly h: number;
|
||||
|
||||
/**
|
||||
* Saturation: float in [0, 1]
|
||||
*/
|
||||
readonly s: number;
|
||||
|
||||
/**
|
||||
* Luminosity: float in [0, 1]
|
||||
*/
|
||||
readonly l: number;
|
||||
|
||||
/**
|
||||
* Alpha: float in [0, 1]
|
||||
*/
|
||||
readonly a: number;
|
||||
|
||||
constructor(h: number, s: number, l: number, a: number) {
|
||||
this.h = Math.max(Math.min(360, h), 0) | 0;
|
||||
this.s = roundFloat(Math.max(Math.min(1, s), 0), 3);
|
||||
this.l = roundFloat(Math.max(Math.min(1, l), 0), 3);
|
||||
this.a = roundFloat(Math.max(Math.min(1, a), 0), 3);
|
||||
}
|
||||
|
||||
static equals(a: HSLA, b: HSLA): boolean {
|
||||
return a.h === b.h && a.s === b.s && a.l === b.l && a.a === b.a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an RGB color value to HSL. Conversion formula
|
||||
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
|
||||
* Assumes r, g, and b are contained in the set [0, 255] and
|
||||
* returns h in the set [0, 360], s, and l in the set [0, 1].
|
||||
*/
|
||||
static fromRGBA(rgba: RGBA): HSLA {
|
||||
const r = rgba.r / 255;
|
||||
const g = rgba.g / 255;
|
||||
const b = rgba.b / 255;
|
||||
const a = rgba.a;
|
||||
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
let h = 0;
|
||||
let s = 0;
|
||||
const l = (min + max) / 2;
|
||||
const chroma = max - min;
|
||||
|
||||
if (chroma > 0) {
|
||||
s = Math.min((l <= 0.5 ? chroma / (2 * l) : chroma / (2 - (2 * l))), 1);
|
||||
|
||||
switch (max) {
|
||||
case r: h = (g - b) / chroma + (g < b ? 6 : 0); break;
|
||||
case g: h = (b - r) / chroma + 2; break;
|
||||
case b: h = (r - g) / chroma + 4; break;
|
||||
}
|
||||
|
||||
h *= 60;
|
||||
h = Math.round(h);
|
||||
}
|
||||
return new HSLA(h, s, l, a);
|
||||
}
|
||||
|
||||
private static _hue2rgb(p: number, q: number, t: number): number {
|
||||
if (t < 0) {
|
||||
t += 1;
|
||||
}
|
||||
if (t > 1) {
|
||||
t -= 1;
|
||||
}
|
||||
if (t < 1 / 6) {
|
||||
return p + (q - p) * 6 * t;
|
||||
}
|
||||
if (t < 1 / 2) {
|
||||
return q;
|
||||
}
|
||||
if (t < 2 / 3) {
|
||||
return p + (q - p) * (2 / 3 - t) * 6;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an HSL color value to RGB. Conversion formula
|
||||
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
|
||||
* Assumes h in the set [0, 360] s, and l are contained in the set [0, 1] and
|
||||
* returns r, g, and b in the set [0, 255].
|
||||
*/
|
||||
static toRGBA(hsla: HSLA): RGBA {
|
||||
const h = hsla.h / 360;
|
||||
const { s, l, a } = hsla;
|
||||
let r: number, g: number, b: number;
|
||||
|
||||
if (s === 0) {
|
||||
r = g = b = l; // achromatic
|
||||
} else {
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
const p = 2 * l - q;
|
||||
r = HSLA._hue2rgb(p, q, h + 1 / 3);
|
||||
g = HSLA._hue2rgb(p, q, h);
|
||||
b = HSLA._hue2rgb(p, q, h - 1 / 3);
|
||||
}
|
||||
|
||||
return new RGBA(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255), a);
|
||||
}
|
||||
}
|
||||
|
||||
export class HSVA {
|
||||
|
||||
_hsvaBrand: void = undefined;
|
||||
|
||||
/**
|
||||
* Hue: integer in [0, 360]
|
||||
*/
|
||||
readonly h: number;
|
||||
|
||||
/**
|
||||
* Saturation: float in [0, 1]
|
||||
*/
|
||||
readonly s: number;
|
||||
|
||||
/**
|
||||
* Value: float in [0, 1]
|
||||
*/
|
||||
readonly v: number;
|
||||
|
||||
/**
|
||||
* Alpha: float in [0, 1]
|
||||
*/
|
||||
readonly a: number;
|
||||
|
||||
constructor(h: number, s: number, v: number, a: number) {
|
||||
this.h = Math.max(Math.min(360, h), 0) | 0;
|
||||
this.s = roundFloat(Math.max(Math.min(1, s), 0), 3);
|
||||
this.v = roundFloat(Math.max(Math.min(1, v), 0), 3);
|
||||
this.a = roundFloat(Math.max(Math.min(1, a), 0), 3);
|
||||
}
|
||||
|
||||
static equals(a: HSVA, b: HSVA): boolean {
|
||||
return a.h === b.h && a.s === b.s && a.v === b.v && a.a === b.a;
|
||||
}
|
||||
|
||||
// from http://www.rapidtables.com/convert/color/rgb-to-hsv.htm
|
||||
static fromRGBA(rgba: RGBA): HSVA {
|
||||
const r = rgba.r / 255;
|
||||
const g = rgba.g / 255;
|
||||
const b = rgba.b / 255;
|
||||
const cmax = Math.max(r, g, b);
|
||||
const cmin = Math.min(r, g, b);
|
||||
const delta = cmax - cmin;
|
||||
const s = cmax === 0 ? 0 : (delta / cmax);
|
||||
let m: number;
|
||||
|
||||
if (delta === 0) {
|
||||
m = 0;
|
||||
} else if (cmax === r) {
|
||||
m = ((((g - b) / delta) % 6) + 6) % 6;
|
||||
} else if (cmax === g) {
|
||||
m = ((b - r) / delta) + 2;
|
||||
} else {
|
||||
m = ((r - g) / delta) + 4;
|
||||
}
|
||||
|
||||
return new HSVA(Math.round(m * 60), s, cmax, rgba.a);
|
||||
}
|
||||
|
||||
// from http://www.rapidtables.com/convert/color/hsv-to-rgb.htm
|
||||
static toRGBA(hsva: HSVA): RGBA {
|
||||
const { h, s, v, a } = hsva;
|
||||
const c = v * s;
|
||||
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
|
||||
const m = v - c;
|
||||
let [r, g, b] = [0, 0, 0];
|
||||
|
||||
if (h < 60) {
|
||||
r = c;
|
||||
g = x;
|
||||
} else if (h < 120) {
|
||||
r = x;
|
||||
g = c;
|
||||
} else if (h < 180) {
|
||||
g = c;
|
||||
b = x;
|
||||
} else if (h < 240) {
|
||||
g = x;
|
||||
b = c;
|
||||
} else if (h < 300) {
|
||||
r = x;
|
||||
b = c;
|
||||
} else if (h <= 360) {
|
||||
r = c;
|
||||
b = x;
|
||||
}
|
||||
|
||||
r = Math.round((r + m) * 255);
|
||||
g = Math.round((g + m) * 255);
|
||||
b = Math.round((b + m) * 255);
|
||||
|
||||
return new RGBA(r, g, b, a);
|
||||
}
|
||||
}
|
||||
|
||||
export class Color {
|
||||
|
||||
static fromHex(hex: string): Color {
|
||||
return Color.Format.CSS.parseHex(hex) || Color.red;
|
||||
}
|
||||
|
||||
static equals(a: Color | null, b: Color | null): boolean {
|
||||
if (!a && !b) {
|
||||
return true;
|
||||
}
|
||||
if (!a || !b) {
|
||||
return false;
|
||||
}
|
||||
return a.equals(b);
|
||||
}
|
||||
|
||||
readonly rgba: RGBA;
|
||||
private _hsla?: HSLA;
|
||||
get hsla(): HSLA {
|
||||
if (this._hsla) {
|
||||
return this._hsla;
|
||||
} else {
|
||||
return HSLA.fromRGBA(this.rgba);
|
||||
}
|
||||
}
|
||||
|
||||
private _hsva?: HSVA;
|
||||
get hsva(): HSVA {
|
||||
if (this._hsva) {
|
||||
return this._hsva;
|
||||
}
|
||||
return HSVA.fromRGBA(this.rgba);
|
||||
}
|
||||
|
||||
constructor(arg: RGBA | HSLA | HSVA) {
|
||||
if (!arg) {
|
||||
throw new Error('Color needs a value');
|
||||
} else if (arg instanceof RGBA) {
|
||||
this.rgba = arg;
|
||||
} else if (arg instanceof HSLA) {
|
||||
this._hsla = arg;
|
||||
this.rgba = HSLA.toRGBA(arg);
|
||||
} else if (arg instanceof HSVA) {
|
||||
this._hsva = arg;
|
||||
this.rgba = HSVA.toRGBA(arg);
|
||||
} else {
|
||||
throw new Error('Invalid color ctor argument');
|
||||
}
|
||||
}
|
||||
|
||||
equals(other: Color | null): boolean {
|
||||
return !!other && RGBA.equals(this.rgba, other.rgba) && HSLA.equals(this.hsla, other.hsla) && HSVA.equals(this.hsva, other.hsva);
|
||||
}
|
||||
|
||||
/**
|
||||
* http://www.w3.org/TR/WCAG20/#relativeluminancedef
|
||||
* Returns the number in the set [0, 1]. O => Darkest Black. 1 => Lightest white.
|
||||
*/
|
||||
getRelativeLuminance(): number {
|
||||
const R = Color._relativeLuminanceForComponent(this.rgba.r);
|
||||
const G = Color._relativeLuminanceForComponent(this.rgba.g);
|
||||
const B = Color._relativeLuminanceForComponent(this.rgba.b);
|
||||
const luminance = 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
||||
|
||||
return roundFloat(luminance, 4);
|
||||
}
|
||||
|
||||
private static _relativeLuminanceForComponent(color: number): number {
|
||||
const c = color / 255;
|
||||
return (c <= 0.03928) ? c / 12.92 : Math.pow(((c + 0.055) / 1.055), 2.4);
|
||||
}
|
||||
|
||||
/**
|
||||
* http://www.w3.org/TR/WCAG20/#contrast-ratiodef
|
||||
* Returns the contrast ration number in the set [1, 21].
|
||||
*/
|
||||
getContrastRatio(another: Color): number {
|
||||
const lum1 = this.getRelativeLuminance();
|
||||
const lum2 = another.getRelativeLuminance();
|
||||
return lum1 > lum2 ? (lum1 + 0.05) / (lum2 + 0.05) : (lum2 + 0.05) / (lum1 + 0.05);
|
||||
}
|
||||
|
||||
/**
|
||||
* http://24ways.org/2010/calculating-color-contrast
|
||||
* Return 'true' if darker color otherwise 'false'
|
||||
*/
|
||||
isDarker(): boolean {
|
||||
const yiq = (this.rgba.r * 299 + this.rgba.g * 587 + this.rgba.b * 114) / 1000;
|
||||
return yiq < 128;
|
||||
}
|
||||
|
||||
/**
|
||||
* http://24ways.org/2010/calculating-color-contrast
|
||||
* Return 'true' if lighter color otherwise 'false'
|
||||
*/
|
||||
isLighter(): boolean {
|
||||
const yiq = (this.rgba.r * 299 + this.rgba.g * 587 + this.rgba.b * 114) / 1000;
|
||||
return yiq >= 128;
|
||||
}
|
||||
|
||||
isLighterThan(another: Color): boolean {
|
||||
const lum1 = this.getRelativeLuminance();
|
||||
const lum2 = another.getRelativeLuminance();
|
||||
return lum1 > lum2;
|
||||
}
|
||||
|
||||
isDarkerThan(another: Color): boolean {
|
||||
const lum1 = this.getRelativeLuminance();
|
||||
const lum2 = another.getRelativeLuminance();
|
||||
return lum1 < lum2;
|
||||
}
|
||||
|
||||
lighten(factor: number): Color {
|
||||
return new Color(new HSLA(this.hsla.h, this.hsla.s, this.hsla.l + this.hsla.l * factor, this.hsla.a));
|
||||
}
|
||||
|
||||
darken(factor: number): Color {
|
||||
return new Color(new HSLA(this.hsla.h, this.hsla.s, this.hsla.l - this.hsla.l * factor, this.hsla.a));
|
||||
}
|
||||
|
||||
transparent(factor: number): Color {
|
||||
const { r, g, b, a } = this.rgba;
|
||||
return new Color(new RGBA(r, g, b, a * factor));
|
||||
}
|
||||
|
||||
isTransparent(): boolean {
|
||||
return this.rgba.a === 0;
|
||||
}
|
||||
|
||||
isOpaque(): boolean {
|
||||
return this.rgba.a === 1;
|
||||
}
|
||||
|
||||
opposite(): Color {
|
||||
return new Color(new RGBA(255 - this.rgba.r, 255 - this.rgba.g, 255 - this.rgba.b, this.rgba.a));
|
||||
}
|
||||
|
||||
blend(c: Color): Color {
|
||||
const rgba = c.rgba;
|
||||
|
||||
// Convert to 0..1 opacity
|
||||
const thisA = this.rgba.a;
|
||||
const colorA = rgba.a;
|
||||
|
||||
const a = thisA + colorA * (1 - thisA);
|
||||
if (a < 1e-6) {
|
||||
return Color.transparent;
|
||||
}
|
||||
|
||||
const r = this.rgba.r * thisA / a + rgba.r * colorA * (1 - thisA) / a;
|
||||
const g = this.rgba.g * thisA / a + rgba.g * colorA * (1 - thisA) / a;
|
||||
const b = this.rgba.b * thisA / a + rgba.b * colorA * (1 - thisA) / a;
|
||||
|
||||
return new Color(new RGBA(r, g, b, a));
|
||||
}
|
||||
|
||||
makeOpaque(opaqueBackground: Color): Color {
|
||||
if (this.isOpaque() || opaqueBackground.rgba.a !== 1) {
|
||||
// only allow to blend onto a non-opaque color onto a opaque color
|
||||
return this;
|
||||
}
|
||||
|
||||
const { r, g, b, a } = this.rgba;
|
||||
|
||||
// https://stackoverflow.com/questions/12228548/finding-equivalent-color-with-opacity
|
||||
return new Color(new RGBA(
|
||||
opaqueBackground.rgba.r - a * (opaqueBackground.rgba.r - r),
|
||||
opaqueBackground.rgba.g - a * (opaqueBackground.rgba.g - g),
|
||||
opaqueBackground.rgba.b - a * (opaqueBackground.rgba.b - b),
|
||||
1
|
||||
));
|
||||
}
|
||||
|
||||
flatten(...backgrounds: Color[]): Color {
|
||||
const background = backgrounds.reduceRight((accumulator, color) => {
|
||||
return Color._flatten(color, accumulator);
|
||||
});
|
||||
return Color._flatten(this, background);
|
||||
}
|
||||
|
||||
private static _flatten(foreground: Color, background: Color) {
|
||||
const backgroundAlpha = 1 - foreground.rgba.a;
|
||||
return new Color(new RGBA(
|
||||
backgroundAlpha * background.rgba.r + foreground.rgba.a * foreground.rgba.r,
|
||||
backgroundAlpha * background.rgba.g + foreground.rgba.a * foreground.rgba.g,
|
||||
backgroundAlpha * background.rgba.b + foreground.rgba.a * foreground.rgba.b
|
||||
));
|
||||
}
|
||||
|
||||
private _toString?: string;
|
||||
toString(): string {
|
||||
if (!this._toString) {
|
||||
this._toString = Color.Format.CSS.format(this);
|
||||
}
|
||||
return this._toString;
|
||||
}
|
||||
|
||||
static getLighterColor(of: Color, relative: Color, factor?: number): Color {
|
||||
if (of.isLighterThan(relative)) {
|
||||
return of;
|
||||
}
|
||||
factor = factor ? factor : 0.5;
|
||||
const lum1 = of.getRelativeLuminance();
|
||||
const lum2 = relative.getRelativeLuminance();
|
||||
factor = factor * (lum2 - lum1) / lum2;
|
||||
return of.lighten(factor);
|
||||
}
|
||||
|
||||
static getDarkerColor(of: Color, relative: Color, factor?: number): Color {
|
||||
if (of.isDarkerThan(relative)) {
|
||||
return of;
|
||||
}
|
||||
factor = factor ? factor : 0.5;
|
||||
const lum1 = of.getRelativeLuminance();
|
||||
const lum2 = relative.getRelativeLuminance();
|
||||
factor = factor * (lum1 - lum2) / lum1;
|
||||
return of.darken(factor);
|
||||
}
|
||||
|
||||
static readonly white = new Color(new RGBA(255, 255, 255, 1));
|
||||
static readonly black = new Color(new RGBA(0, 0, 0, 1));
|
||||
static readonly red = new Color(new RGBA(255, 0, 0, 1));
|
||||
static readonly blue = new Color(new RGBA(0, 0, 255, 1));
|
||||
static readonly green = new Color(new RGBA(0, 255, 0, 1));
|
||||
static readonly cyan = new Color(new RGBA(0, 255, 255, 1));
|
||||
static readonly lightgrey = new Color(new RGBA(211, 211, 211, 1));
|
||||
static readonly transparent = new Color(new RGBA(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
export namespace Color {
|
||||
export namespace Format {
|
||||
export namespace CSS {
|
||||
|
||||
export function formatRGB(color: Color): string {
|
||||
if (color.rgba.a === 1) {
|
||||
return `rgb(${color.rgba.r}, ${color.rgba.g}, ${color.rgba.b})`;
|
||||
}
|
||||
|
||||
return Color.Format.CSS.formatRGBA(color);
|
||||
}
|
||||
|
||||
export function formatRGBA(color: Color): string {
|
||||
return `rgba(${color.rgba.r}, ${color.rgba.g}, ${color.rgba.b}, ${+(color.rgba.a).toFixed(2)})`;
|
||||
}
|
||||
|
||||
export function formatHSL(color: Color): string {
|
||||
if (color.hsla.a === 1) {
|
||||
return `hsl(${color.hsla.h}, ${(color.hsla.s * 100).toFixed(2)}%, ${(color.hsla.l * 100).toFixed(2)}%)`;
|
||||
}
|
||||
|
||||
return Color.Format.CSS.formatHSLA(color);
|
||||
}
|
||||
|
||||
export function formatHSLA(color: Color): string {
|
||||
return `hsla(${color.hsla.h}, ${(color.hsla.s * 100).toFixed(2)}%, ${(color.hsla.l * 100).toFixed(2)}%, ${color.hsla.a.toFixed(2)})`;
|
||||
}
|
||||
|
||||
function _toTwoDigitHex(n: number): string {
|
||||
const r = n.toString(16);
|
||||
return r.length !== 2 ? '0' + r : r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the color as #RRGGBB
|
||||
*/
|
||||
export function formatHex(color: Color): string {
|
||||
return `#${_toTwoDigitHex(color.rgba.r)}${_toTwoDigitHex(color.rgba.g)}${_toTwoDigitHex(color.rgba.b)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the color as #RRGGBBAA
|
||||
* If 'compact' is set, colors without transparancy will be printed as #RRGGBB
|
||||
*/
|
||||
export function formatHexA(color: Color, compact = false): string {
|
||||
if (compact && color.rgba.a === 1) {
|
||||
return Color.Format.CSS.formatHex(color);
|
||||
}
|
||||
|
||||
return `#${_toTwoDigitHex(color.rgba.r)}${_toTwoDigitHex(color.rgba.g)}${_toTwoDigitHex(color.rgba.b)}${_toTwoDigitHex(Math.round(color.rgba.a * 255))}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default format will use HEX if opaque and RGBA otherwise.
|
||||
*/
|
||||
export function format(color: Color): string {
|
||||
if (color.isOpaque()) {
|
||||
return Color.Format.CSS.formatHex(color);
|
||||
}
|
||||
|
||||
return Color.Format.CSS.formatRGBA(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an Hex color value to a Color.
|
||||
* returns r, g, and b are contained in the set [0, 255]
|
||||
* @param hex string (#RGB, #RGBA, #RRGGBB or #RRGGBBAA).
|
||||
*/
|
||||
export function parseHex(hex: string): Color | null {
|
||||
const length = hex.length;
|
||||
|
||||
if (length === 0) {
|
||||
// Invalid color
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hex.charCodeAt(0) !== CharCode.Hash) {
|
||||
// Does not begin with a #
|
||||
return null;
|
||||
}
|
||||
|
||||
if (length === 7) {
|
||||
// #RRGGBB format
|
||||
const r = 16 * _parseHexDigit(hex.charCodeAt(1)) + _parseHexDigit(hex.charCodeAt(2));
|
||||
const g = 16 * _parseHexDigit(hex.charCodeAt(3)) + _parseHexDigit(hex.charCodeAt(4));
|
||||
const b = 16 * _parseHexDigit(hex.charCodeAt(5)) + _parseHexDigit(hex.charCodeAt(6));
|
||||
return new Color(new RGBA(r, g, b, 1));
|
||||
}
|
||||
|
||||
if (length === 9) {
|
||||
// #RRGGBBAA format
|
||||
const r = 16 * _parseHexDigit(hex.charCodeAt(1)) + _parseHexDigit(hex.charCodeAt(2));
|
||||
const g = 16 * _parseHexDigit(hex.charCodeAt(3)) + _parseHexDigit(hex.charCodeAt(4));
|
||||
const b = 16 * _parseHexDigit(hex.charCodeAt(5)) + _parseHexDigit(hex.charCodeAt(6));
|
||||
const a = 16 * _parseHexDigit(hex.charCodeAt(7)) + _parseHexDigit(hex.charCodeAt(8));
|
||||
return new Color(new RGBA(r, g, b, a / 255));
|
||||
}
|
||||
|
||||
if (length === 4) {
|
||||
// #RGB format
|
||||
const r = _parseHexDigit(hex.charCodeAt(1));
|
||||
const g = _parseHexDigit(hex.charCodeAt(2));
|
||||
const b = _parseHexDigit(hex.charCodeAt(3));
|
||||
return new Color(new RGBA(16 * r + r, 16 * g + g, 16 * b + b));
|
||||
}
|
||||
|
||||
if (length === 5) {
|
||||
// #RGBA format
|
||||
const r = _parseHexDigit(hex.charCodeAt(1));
|
||||
const g = _parseHexDigit(hex.charCodeAt(2));
|
||||
const b = _parseHexDigit(hex.charCodeAt(3));
|
||||
const a = _parseHexDigit(hex.charCodeAt(4));
|
||||
return new Color(new RGBA(16 * r + r, 16 * g + g, 16 * b + b, (16 * a + a) / 255));
|
||||
}
|
||||
|
||||
// Invalid color
|
||||
return null;
|
||||
}
|
||||
|
||||
function _parseHexDigit(charCode: CharCode): number {
|
||||
switch (charCode) {
|
||||
case CharCode.Digit0: return 0;
|
||||
case CharCode.Digit1: return 1;
|
||||
case CharCode.Digit2: return 2;
|
||||
case CharCode.Digit3: return 3;
|
||||
case CharCode.Digit4: return 4;
|
||||
case CharCode.Digit5: return 5;
|
||||
case CharCode.Digit6: return 6;
|
||||
case CharCode.Digit7: return 7;
|
||||
case CharCode.Digit8: return 8;
|
||||
case CharCode.Digit9: return 9;
|
||||
case CharCode.a: return 10;
|
||||
case CharCode.A: return 10;
|
||||
case CharCode.b: return 11;
|
||||
case CharCode.B: return 11;
|
||||
case CharCode.c: return 12;
|
||||
case CharCode.C: return 12;
|
||||
case CharCode.d: return 13;
|
||||
case CharCode.D: return 13;
|
||||
case CharCode.e: return 14;
|
||||
case CharCode.E: return 14;
|
||||
case CharCode.f: return 15;
|
||||
case CharCode.F: return 15;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,355 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Lazy } from 'vs/base/common/lazy';
|
||||
import { sep } from 'vs/base/common/path';
|
||||
|
||||
// When comparing large numbers of strings it's better for performance to create an
|
||||
// Intl.Collator object and use the function provided by its compare property
|
||||
// than it is to use String.prototype.localeCompare()
|
||||
|
||||
// A collator with numeric sorting enabled, and no sensitivity to case, accents or diacritics.
|
||||
const intlFileNameCollatorBaseNumeric: Lazy<{ collator: Intl.Collator; collatorIsNumeric: boolean }> = new Lazy(() => {
|
||||
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
|
||||
return {
|
||||
collator,
|
||||
collatorIsNumeric: collator.resolvedOptions().numeric
|
||||
};
|
||||
});
|
||||
|
||||
// A collator with numeric sorting enabled.
|
||||
const intlFileNameCollatorNumeric: Lazy<{ collator: Intl.Collator }> = new Lazy(() => {
|
||||
const collator = new Intl.Collator(undefined, { numeric: true });
|
||||
return {
|
||||
collator
|
||||
};
|
||||
});
|
||||
|
||||
// A collator with numeric sorting enabled, and sensitivity to accents and diacritics but not case.
|
||||
const intlFileNameCollatorNumericCaseInsensitive: Lazy<{ collator: Intl.Collator }> = new Lazy(() => {
|
||||
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'accent' });
|
||||
return {
|
||||
collator
|
||||
};
|
||||
});
|
||||
|
||||
/** Compares filenames without distinguishing the name from the extension. Disambiguates by unicode comparison. */
|
||||
export function compareFileNames(one: string | null, other: string | null, caseSensitive = false): number {
|
||||
const a = one || '';
|
||||
const b = other || '';
|
||||
const result = intlFileNameCollatorBaseNumeric.value.collator.compare(a, b);
|
||||
|
||||
// Using the numeric option will make compare(`foo1`, `foo01`) === 0. Disambiguate.
|
||||
if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && result === 0 && a !== b) {
|
||||
return a < b ? -1 : 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Compares full filenames without grouping by case. */
|
||||
export function compareFileNamesDefault(one: string | null, other: string | null): number {
|
||||
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
|
||||
one = one || '';
|
||||
other = other || '';
|
||||
|
||||
return compareAndDisambiguateByLength(collatorNumeric, one, other);
|
||||
}
|
||||
|
||||
/** Compares full filenames grouping uppercase names before lowercase. */
|
||||
export function compareFileNamesUpper(one: string | null, other: string | null) {
|
||||
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
|
||||
one = one || '';
|
||||
other = other || '';
|
||||
|
||||
return compareCaseUpperFirst(one, other) || compareAndDisambiguateByLength(collatorNumeric, one, other);
|
||||
}
|
||||
|
||||
/** Compares full filenames grouping lowercase names before uppercase. */
|
||||
export function compareFileNamesLower(one: string | null, other: string | null) {
|
||||
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
|
||||
one = one || '';
|
||||
other = other || '';
|
||||
|
||||
return compareCaseLowerFirst(one, other) || compareAndDisambiguateByLength(collatorNumeric, one, other);
|
||||
}
|
||||
|
||||
/** Compares full filenames by unicode value. */
|
||||
export function compareFileNamesUnicode(one: string | null, other: string | null) {
|
||||
one = one || '';
|
||||
other = other || '';
|
||||
|
||||
if (one === other) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return one < other ? -1 : 1;
|
||||
}
|
||||
|
||||
/** Compares filenames by extension, then by name. Disambiguates by unicode comparison. */
|
||||
export function compareFileExtensions(one: string | null, other: string | null): number {
|
||||
const [oneName, oneExtension] = extractNameAndExtension(one);
|
||||
const [otherName, otherExtension] = extractNameAndExtension(other);
|
||||
|
||||
let result = intlFileNameCollatorBaseNumeric.value.collator.compare(oneExtension, otherExtension);
|
||||
|
||||
if (result === 0) {
|
||||
// Using the numeric option will make compare(`foo1`, `foo01`) === 0. Disambiguate.
|
||||
if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && oneExtension !== otherExtension) {
|
||||
return oneExtension < otherExtension ? -1 : 1;
|
||||
}
|
||||
|
||||
// Extensions are equal, compare filenames
|
||||
result = intlFileNameCollatorBaseNumeric.value.collator.compare(oneName, otherName);
|
||||
|
||||
if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && result === 0 && oneName !== otherName) {
|
||||
return oneName < otherName ? -1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Compares filenames by extension, then by full filename. Mixes uppercase and lowercase names together. */
|
||||
export function compareFileExtensionsDefault(one: string | null, other: string | null): number {
|
||||
one = one || '';
|
||||
other = other || '';
|
||||
const oneExtension = extractExtension(one);
|
||||
const otherExtension = extractExtension(other);
|
||||
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
|
||||
const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsensitive.value.collator;
|
||||
|
||||
return compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension) ||
|
||||
compareAndDisambiguateByLength(collatorNumeric, one, other);
|
||||
}
|
||||
|
||||
/** Compares filenames by extension, then case, then full filename. Groups uppercase names before lowercase. */
|
||||
export function compareFileExtensionsUpper(one: string | null, other: string | null): number {
|
||||
one = one || '';
|
||||
other = other || '';
|
||||
const oneExtension = extractExtension(one);
|
||||
const otherExtension = extractExtension(other);
|
||||
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
|
||||
const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsensitive.value.collator;
|
||||
|
||||
return compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension) ||
|
||||
compareCaseUpperFirst(one, other) ||
|
||||
compareAndDisambiguateByLength(collatorNumeric, one, other);
|
||||
}
|
||||
|
||||
/** Compares filenames by extension, then case, then full filename. Groups lowercase names before uppercase. */
|
||||
export function compareFileExtensionsLower(one: string | null, other: string | null): number {
|
||||
one = one || '';
|
||||
other = other || '';
|
||||
const oneExtension = extractExtension(one);
|
||||
const otherExtension = extractExtension(other);
|
||||
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
|
||||
const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsensitive.value.collator;
|
||||
|
||||
return compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension) ||
|
||||
compareCaseLowerFirst(one, other) ||
|
||||
compareAndDisambiguateByLength(collatorNumeric, one, other);
|
||||
}
|
||||
|
||||
/** Compares filenames by case-insensitive extension unicode value, then by full filename unicode value. */
|
||||
export function compareFileExtensionsUnicode(one: string | null, other: string | null) {
|
||||
one = one || '';
|
||||
other = other || '';
|
||||
const oneExtension = extractExtension(one).toLowerCase();
|
||||
const otherExtension = extractExtension(other).toLowerCase();
|
||||
|
||||
// Check for extension differences
|
||||
if (oneExtension !== otherExtension) {
|
||||
return oneExtension < otherExtension ? -1 : 1;
|
||||
}
|
||||
|
||||
// Check for full filename differences.
|
||||
if (one !== other) {
|
||||
return one < other ? -1 : 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const FileNameMatch = /^(.*?)(\.([^.]*))?$/;
|
||||
|
||||
/** Extracts the name and extension from a full filename, with optional special handling for dotfiles */
|
||||
function extractNameAndExtension(str?: string | null, dotfilesAsNames = false): [string, string] {
|
||||
const match = str ? FileNameMatch.exec(str) as Array<string> : ([] as Array<string>);
|
||||
|
||||
let result: [string, string] = [(match && match[1]) || '', (match && match[3]) || ''];
|
||||
|
||||
// if the dotfilesAsNames option is selected, treat an empty filename with an extension
|
||||
// or a filename that starts with a dot, as a dotfile name
|
||||
if (dotfilesAsNames && (!result[0] && result[1] || result[0] && result[0].charAt(0) === '.')) {
|
||||
result = [result[0] + '.' + result[1], ''];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Extracts the extension from a full filename. Treats dotfiles as names, not extensions. */
|
||||
function extractExtension(str?: string | null): string {
|
||||
const match = str ? FileNameMatch.exec(str) as Array<string> : ([] as Array<string>);
|
||||
|
||||
return (match && match[1] && match[1].charAt(0) !== '.' && match[3]) || '';
|
||||
}
|
||||
|
||||
function compareAndDisambiguateByLength(collator: Intl.Collator, one: string, other: string) {
|
||||
// Check for differences
|
||||
const result = collator.compare(one, other);
|
||||
if (result !== 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// In a numeric comparison, `foo1` and `foo01` will compare as equivalent.
|
||||
// Disambiguate by sorting the shorter string first.
|
||||
if (one.length !== other.length) {
|
||||
return one.length < other.length ? -1 : 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** @returns `true` if the string is starts with a lowercase letter. Otherwise, `false`. */
|
||||
function startsWithLower(string: string) {
|
||||
const character = string.charAt(0);
|
||||
|
||||
return (character.toLocaleUpperCase() !== character) ? true : false;
|
||||
}
|
||||
|
||||
/** @returns `true` if the string starts with an uppercase letter. Otherwise, `false`. */
|
||||
function startsWithUpper(string: string) {
|
||||
const character = string.charAt(0);
|
||||
|
||||
return (character.toLocaleLowerCase() !== character) ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the case of the provided strings - lowercase before uppercase
|
||||
*
|
||||
* @returns
|
||||
* ```text
|
||||
* -1 if one is lowercase and other is uppercase
|
||||
* 1 if one is uppercase and other is lowercase
|
||||
* 0 otherwise
|
||||
* ```
|
||||
*/
|
||||
function compareCaseLowerFirst(one: string, other: string): number {
|
||||
if (startsWithLower(one) && startsWithUpper(other)) {
|
||||
return -1;
|
||||
}
|
||||
return (startsWithUpper(one) && startsWithLower(other)) ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the case of the provided strings - uppercase before lowercase
|
||||
*
|
||||
* @returns
|
||||
* ```text
|
||||
* -1 if one is uppercase and other is lowercase
|
||||
* 1 if one is lowercase and other is uppercase
|
||||
* 0 otherwise
|
||||
* ```
|
||||
*/
|
||||
function compareCaseUpperFirst(one: string, other: string): number {
|
||||
if (startsWithUpper(one) && startsWithLower(other)) {
|
||||
return -1;
|
||||
}
|
||||
return (startsWithLower(one) && startsWithUpper(other)) ? 1 : 0;
|
||||
}
|
||||
|
||||
function comparePathComponents(one: string, other: string, caseSensitive = false): number {
|
||||
if (!caseSensitive) {
|
||||
one = one && one.toLowerCase();
|
||||
other = other && other.toLowerCase();
|
||||
}
|
||||
|
||||
if (one === other) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return one < other ? -1 : 1;
|
||||
}
|
||||
|
||||
export function comparePaths(one: string, other: string, caseSensitive = false): number {
|
||||
const oneParts = one.split(sep);
|
||||
const otherParts = other.split(sep);
|
||||
|
||||
const lastOne = oneParts.length - 1;
|
||||
const lastOther = otherParts.length - 1;
|
||||
let endOne: boolean, endOther: boolean;
|
||||
|
||||
for (let i = 0; ; i++) {
|
||||
endOne = lastOne === i;
|
||||
endOther = lastOther === i;
|
||||
|
||||
if (endOne && endOther) {
|
||||
return compareFileNames(oneParts[i], otherParts[i], caseSensitive);
|
||||
} else if (endOne) {
|
||||
return -1;
|
||||
} else if (endOther) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const result = comparePathComponents(oneParts[i], otherParts[i], caseSensitive);
|
||||
|
||||
if (result !== 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function compareAnything(one: string, other: string, lookFor: string): number {
|
||||
const elementAName = one.toLowerCase();
|
||||
const elementBName = other.toLowerCase();
|
||||
|
||||
// Sort prefix matches over non prefix matches
|
||||
const prefixCompare = compareByPrefix(one, other, lookFor);
|
||||
if (prefixCompare) {
|
||||
return prefixCompare;
|
||||
}
|
||||
|
||||
// Sort suffix matches over non suffix matches
|
||||
const elementASuffixMatch = elementAName.endsWith(lookFor);
|
||||
const elementBSuffixMatch = elementBName.endsWith(lookFor);
|
||||
if (elementASuffixMatch !== elementBSuffixMatch) {
|
||||
return elementASuffixMatch ? -1 : 1;
|
||||
}
|
||||
|
||||
// Understand file names
|
||||
const r = compareFileNames(elementAName, elementBName);
|
||||
if (r !== 0) {
|
||||
return r;
|
||||
}
|
||||
|
||||
// Compare by name
|
||||
return elementAName.localeCompare(elementBName);
|
||||
}
|
||||
|
||||
export function compareByPrefix(one: string, other: string, lookFor: string): number {
|
||||
const elementAName = one.toLowerCase();
|
||||
const elementBName = other.toLowerCase();
|
||||
|
||||
// Sort prefix matches over non prefix matches
|
||||
const elementAPrefixMatch = elementAName.startsWith(lookFor);
|
||||
const elementBPrefixMatch = elementBName.startsWith(lookFor);
|
||||
if (elementAPrefixMatch !== elementBPrefixMatch) {
|
||||
return elementAPrefixMatch ? -1 : 1;
|
||||
}
|
||||
|
||||
// Same prefix: Sort shorter matches to the top to have those on top that match more precisely
|
||||
else if (elementAPrefixMatch && elementBPrefixMatch) {
|
||||
if (elementAName.length < elementBName.length) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (elementAName.length > elementBName.length) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { BugIndicatingError } from 'vs/base/common/errors';
|
||||
|
||||
/*
|
||||
* This file contains helper classes to manage control flow.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Prevents code from being re-entrant.
|
||||
*/
|
||||
export class ReentrancyBarrier {
|
||||
private _isOccupied = false;
|
||||
|
||||
/**
|
||||
* Calls `runner` if the barrier is not occupied.
|
||||
* During the call, the barrier becomes occupied.
|
||||
*/
|
||||
public runExclusivelyOrSkip(runner: () => void): void {
|
||||
if (this._isOccupied) {
|
||||
return;
|
||||
}
|
||||
this._isOccupied = true;
|
||||
try {
|
||||
runner();
|
||||
} finally {
|
||||
this._isOccupied = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls `runner`. If the barrier is occupied, throws an error.
|
||||
* During the call, the barrier becomes active.
|
||||
*/
|
||||
public runExclusivelyOrThrow(runner: () => void): void {
|
||||
if (this._isOccupied) {
|
||||
throw new BugIndicatingError(`ReentrancyBarrier: reentrant call detected!`);
|
||||
}
|
||||
this._isOccupied = true;
|
||||
try {
|
||||
runner();
|
||||
} finally {
|
||||
this._isOccupied = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if some runner occupies this barrier.
|
||||
*/
|
||||
public get isOccupied() {
|
||||
return this._isOccupied;
|
||||
}
|
||||
|
||||
public makeExclusiveOrSkip<TFunction extends Function>(fn: TFunction): TFunction {
|
||||
return ((...args: any[]) => {
|
||||
if (this._isOccupied) {
|
||||
return;
|
||||
}
|
||||
this._isOccupied = true;
|
||||
try {
|
||||
return fn(...args);
|
||||
} finally {
|
||||
this._isOccupied = false;
|
||||
}
|
||||
}) as any;
|
||||
}
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
const minute = 60;
|
||||
const hour = minute * 60;
|
||||
const day = hour * 24;
|
||||
const week = day * 7;
|
||||
const month = day * 30;
|
||||
const year = day * 365;
|
||||
|
||||
/**
|
||||
* Create a localized difference of the time between now and the specified date.
|
||||
* @param date The date to generate the difference from.
|
||||
* @param appendAgoLabel Whether to append the " ago" to the end.
|
||||
* @param useFullTimeWords Whether to use full words (eg. seconds) instead of
|
||||
* shortened (eg. secs).
|
||||
* @param disallowNow Whether to disallow the string "now" when the difference
|
||||
* is less than 30 seconds.
|
||||
*/
|
||||
export function fromNow(date: number | Date, appendAgoLabel?: boolean, useFullTimeWords?: boolean, disallowNow?: boolean): string {
|
||||
if (typeof date !== 'number') {
|
||||
date = date.getTime();
|
||||
}
|
||||
|
||||
const seconds = Math.round((new Date().getTime() - date) / 1000);
|
||||
if (seconds < -30) {
|
||||
return localize('date.fromNow.in', 'in {0}', fromNow(new Date().getTime() + seconds * 1000, false));
|
||||
}
|
||||
|
||||
if (!disallowNow && seconds < 30) {
|
||||
return localize('date.fromNow.now', 'now');
|
||||
}
|
||||
|
||||
let value: number;
|
||||
if (seconds < minute) {
|
||||
value = seconds;
|
||||
|
||||
if (appendAgoLabel) {
|
||||
if (value === 1) {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.seconds.singular.ago.fullWord', '{0} second ago', value)
|
||||
: localize('date.fromNow.seconds.singular.ago', '{0} sec ago', value);
|
||||
} else {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.seconds.plural.ago.fullWord', '{0} seconds ago', value)
|
||||
: localize('date.fromNow.seconds.plural.ago', '{0} secs ago', value);
|
||||
}
|
||||
} else {
|
||||
if (value === 1) {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.seconds.singular.fullWord', '{0} second', value)
|
||||
: localize('date.fromNow.seconds.singular', '{0} sec', value);
|
||||
} else {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.seconds.plural.fullWord', '{0} seconds', value)
|
||||
: localize('date.fromNow.seconds.plural', '{0} secs', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (seconds < hour) {
|
||||
value = Math.floor(seconds / minute);
|
||||
if (appendAgoLabel) {
|
||||
if (value === 1) {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.minutes.singular.ago.fullWord', '{0} minute ago', value)
|
||||
: localize('date.fromNow.minutes.singular.ago', '{0} min ago', value);
|
||||
} else {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.minutes.plural.ago.fullWord', '{0} minutes ago', value)
|
||||
: localize('date.fromNow.minutes.plural.ago', '{0} mins ago', value);
|
||||
}
|
||||
} else {
|
||||
if (value === 1) {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.minutes.singular.fullWord', '{0} minute', value)
|
||||
: localize('date.fromNow.minutes.singular', '{0} min', value);
|
||||
} else {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.minutes.plural.fullWord', '{0} minutes', value)
|
||||
: localize('date.fromNow.minutes.plural', '{0} mins', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (seconds < day) {
|
||||
value = Math.floor(seconds / hour);
|
||||
if (appendAgoLabel) {
|
||||
if (value === 1) {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.hours.singular.ago.fullWord', '{0} hour ago', value)
|
||||
: localize('date.fromNow.hours.singular.ago', '{0} hr ago', value);
|
||||
} else {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.hours.plural.ago.fullWord', '{0} hours ago', value)
|
||||
: localize('date.fromNow.hours.plural.ago', '{0} hrs ago', value);
|
||||
}
|
||||
} else {
|
||||
if (value === 1) {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.hours.singular.fullWord', '{0} hour', value)
|
||||
: localize('date.fromNow.hours.singular', '{0} hr', value);
|
||||
} else {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.hours.plural.fullWord', '{0} hours', value)
|
||||
: localize('date.fromNow.hours.plural', '{0} hrs', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (seconds < week) {
|
||||
value = Math.floor(seconds / day);
|
||||
if (appendAgoLabel) {
|
||||
return value === 1
|
||||
? localize('date.fromNow.days.singular.ago', '{0} day ago', value)
|
||||
: localize('date.fromNow.days.plural.ago', '{0} days ago', value);
|
||||
} else {
|
||||
return value === 1
|
||||
? localize('date.fromNow.days.singular', '{0} day', value)
|
||||
: localize('date.fromNow.days.plural', '{0} days', value);
|
||||
}
|
||||
}
|
||||
|
||||
if (seconds < month) {
|
||||
value = Math.floor(seconds / week);
|
||||
if (appendAgoLabel) {
|
||||
if (value === 1) {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.weeks.singular.ago.fullWord', '{0} week ago', value)
|
||||
: localize('date.fromNow.weeks.singular.ago', '{0} wk ago', value);
|
||||
} else {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.weeks.plural.ago.fullWord', '{0} weeks ago', value)
|
||||
: localize('date.fromNow.weeks.plural.ago', '{0} wks ago', value);
|
||||
}
|
||||
} else {
|
||||
if (value === 1) {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.weeks.singular.fullWord', '{0} week', value)
|
||||
: localize('date.fromNow.weeks.singular', '{0} wk', value);
|
||||
} else {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.weeks.plural.fullWord', '{0} weeks', value)
|
||||
: localize('date.fromNow.weeks.plural', '{0} wks', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (seconds < year) {
|
||||
value = Math.floor(seconds / month);
|
||||
if (appendAgoLabel) {
|
||||
if (value === 1) {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.months.singular.ago.fullWord', '{0} month ago', value)
|
||||
: localize('date.fromNow.months.singular.ago', '{0} mo ago', value);
|
||||
} else {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.months.plural.ago.fullWord', '{0} months ago', value)
|
||||
: localize('date.fromNow.months.plural.ago', '{0} mos ago', value);
|
||||
}
|
||||
} else {
|
||||
if (value === 1) {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.months.singular.fullWord', '{0} month', value)
|
||||
: localize('date.fromNow.months.singular', '{0} mo', value);
|
||||
} else {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.months.plural.fullWord', '{0} months', value)
|
||||
: localize('date.fromNow.months.plural', '{0} mos', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
value = Math.floor(seconds / year);
|
||||
if (appendAgoLabel) {
|
||||
if (value === 1) {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.years.singular.ago.fullWord', '{0} year ago', value)
|
||||
: localize('date.fromNow.years.singular.ago', '{0} yr ago', value);
|
||||
} else {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.years.plural.ago.fullWord', '{0} years ago', value)
|
||||
: localize('date.fromNow.years.plural.ago', '{0} yrs ago', value);
|
||||
}
|
||||
} else {
|
||||
if (value === 1) {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.years.singular.fullWord', '{0} year', value)
|
||||
: localize('date.fromNow.years.singular', '{0} yr', value);
|
||||
} else {
|
||||
return useFullTimeWords
|
||||
? localize('date.fromNow.years.plural.fullWord', '{0} years', value)
|
||||
: localize('date.fromNow.years.plural', '{0} yrs', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a readable duration with intelligent/lossy precision. For example "40ms" or "3.040s")
|
||||
* @param ms The duration to get in milliseconds.
|
||||
* @param useFullTimeWords Whether to use full words (eg. seconds) instead of
|
||||
* shortened (eg. secs).
|
||||
*/
|
||||
export function getDurationString(ms: number, useFullTimeWords?: boolean) {
|
||||
const seconds = Math.abs(ms / 1000);
|
||||
if (seconds < 1) {
|
||||
return useFullTimeWords
|
||||
? localize('duration.ms.full', '{0} milliseconds', ms)
|
||||
: localize('duration.ms', '{0}ms', ms);
|
||||
}
|
||||
if (seconds < minute) {
|
||||
return useFullTimeWords
|
||||
? localize('duration.s.full', '{0} seconds', Math.round(ms) / 1000)
|
||||
: localize('duration.s', '{0}s', Math.round(ms) / 1000);
|
||||
}
|
||||
if (seconds < hour) {
|
||||
return useFullTimeWords
|
||||
? localize('duration.m.full', '{0} minutes', Math.round(ms / (1000 * minute)))
|
||||
: localize('duration.m', '{0} mins', Math.round(ms / (1000 * minute)));
|
||||
}
|
||||
if (seconds < day) {
|
||||
return useFullTimeWords
|
||||
? localize('duration.h.full', '{0} hours', Math.round(ms / (1000 * hour)))
|
||||
: localize('duration.h', '{0} hrs', Math.round(ms / (1000 * hour)));
|
||||
}
|
||||
return localize('duration.d', '{0} days', Math.round(ms / (1000 * day)));
|
||||
}
|
||||
|
||||
export function toLocalISOString(date: Date): string {
|
||||
return date.getFullYear() +
|
||||
'-' + String(date.getMonth() + 1).padStart(2, '0') +
|
||||
'-' + String(date.getDate()).padStart(2, '0') +
|
||||
'T' + String(date.getHours()).padStart(2, '0') +
|
||||
':' + String(date.getMinutes()).padStart(2, '0') +
|
||||
':' + String(date.getSeconds()).padStart(2, '0') +
|
||||
'.' + (date.getMilliseconds() / 1000).toFixed(3).slice(2, 5) +
|
||||
'Z';
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { env } from 'vs/base/common/process';
|
||||
|
||||
// Define the enumeration for Desktop Environments
|
||||
enum DesktopEnvironment {
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
CINNAMON = 'CINNAMON',
|
||||
DEEPIN = 'DEEPIN',
|
||||
GNOME = 'GNOME',
|
||||
KDE3 = 'KDE3',
|
||||
KDE4 = 'KDE4',
|
||||
KDE5 = 'KDE5',
|
||||
KDE6 = 'KDE6',
|
||||
PANTHEON = 'PANTHEON',
|
||||
UNITY = 'UNITY',
|
||||
XFCE = 'XFCE',
|
||||
UKUI = 'UKUI',
|
||||
LXQT = 'LXQT',
|
||||
}
|
||||
|
||||
const kXdgCurrentDesktopEnvVar = 'XDG_CURRENT_DESKTOP';
|
||||
const kKDESessionEnvVar = 'KDE_SESSION_VERSION';
|
||||
|
||||
export function getDesktopEnvironment(): DesktopEnvironment {
|
||||
const xdgCurrentDesktop = env[kXdgCurrentDesktopEnvVar];
|
||||
if (xdgCurrentDesktop) {
|
||||
const values = xdgCurrentDesktop.split(':').map(value => value.trim()).filter(value => value.length > 0);
|
||||
for (const value of values) {
|
||||
switch (value) {
|
||||
case 'Unity': {
|
||||
const desktopSessionUnity = env['DESKTOP_SESSION'];
|
||||
if (desktopSessionUnity && desktopSessionUnity.includes('gnome-fallback')) {
|
||||
return DesktopEnvironment.GNOME;
|
||||
}
|
||||
|
||||
return DesktopEnvironment.UNITY;
|
||||
}
|
||||
case 'Deepin':
|
||||
return DesktopEnvironment.DEEPIN;
|
||||
case 'GNOME':
|
||||
return DesktopEnvironment.GNOME;
|
||||
case 'X-Cinnamon':
|
||||
return DesktopEnvironment.CINNAMON;
|
||||
case 'KDE': {
|
||||
const kdeSession = env[kKDESessionEnvVar];
|
||||
if (kdeSession === '5') { return DesktopEnvironment.KDE5; }
|
||||
if (kdeSession === '6') { return DesktopEnvironment.KDE6; }
|
||||
return DesktopEnvironment.KDE4;
|
||||
}
|
||||
case 'Pantheon':
|
||||
return DesktopEnvironment.PANTHEON;
|
||||
case 'XFCE':
|
||||
return DesktopEnvironment.XFCE;
|
||||
case 'UKUI':
|
||||
return DesktopEnvironment.UKUI;
|
||||
case 'LXQt':
|
||||
return DesktopEnvironment.LXQT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const desktopSession = env['DESKTOP_SESSION'];
|
||||
if (desktopSession) {
|
||||
switch (desktopSession) {
|
||||
case 'deepin':
|
||||
return DesktopEnvironment.DEEPIN;
|
||||
case 'gnome':
|
||||
case 'mate':
|
||||
return DesktopEnvironment.GNOME;
|
||||
case 'kde4':
|
||||
case 'kde-plasma':
|
||||
return DesktopEnvironment.KDE4;
|
||||
case 'kde':
|
||||
if (kKDESessionEnvVar in env) {
|
||||
return DesktopEnvironment.KDE4;
|
||||
}
|
||||
return DesktopEnvironment.KDE3;
|
||||
case 'xfce':
|
||||
case 'xubuntu':
|
||||
return DesktopEnvironment.XFCE;
|
||||
case 'ukui':
|
||||
return DesktopEnvironment.UKUI;
|
||||
}
|
||||
}
|
||||
|
||||
if ('GNOME_DESKTOP_SESSION_ID' in env) {
|
||||
return DesktopEnvironment.GNOME;
|
||||
}
|
||||
if ('KDE_FULL_SESSION' in env) {
|
||||
if (kKDESessionEnvVar in env) {
|
||||
return DesktopEnvironment.KDE4;
|
||||
}
|
||||
return DesktopEnvironment.KDE3;
|
||||
}
|
||||
|
||||
return DesktopEnvironment.UNKNOWN;
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as arrays from 'vs/base/common/arrays';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import * as nls from 'vs/nls';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
|
||||
function exceptionToErrorMessage(exception: any, verbose: boolean): string {
|
||||
if (verbose && (exception.stack || exception.stacktrace)) {
|
||||
return nls.localize('stackTrace.format', "{0}: {1}", detectSystemErrorMessage(exception), stackToString(exception.stack) || stackToString(exception.stacktrace));
|
||||
}
|
||||
|
||||
return detectSystemErrorMessage(exception);
|
||||
}
|
||||
|
||||
function stackToString(stack: string[] | string | undefined): string | undefined {
|
||||
if (Array.isArray(stack)) {
|
||||
return stack.join('\n');
|
||||
}
|
||||
|
||||
return stack;
|
||||
}
|
||||
|
||||
function detectSystemErrorMessage(exception: any): string {
|
||||
|
||||
// Custom node.js error from us
|
||||
if (exception.code === 'ERR_UNC_HOST_NOT_ALLOWED') {
|
||||
return `${exception.message}. Please update the 'security.allowedUNCHosts' setting if you want to allow this host.`;
|
||||
}
|
||||
|
||||
// See https://nodejs.org/api/errors.html#errors_class_system_error
|
||||
if (typeof exception.code === 'string' && typeof exception.errno === 'number' && typeof exception.syscall === 'string') {
|
||||
return nls.localize('nodeExceptionMessage', "A system error occurred ({0})", exception.message);
|
||||
}
|
||||
|
||||
return exception.message || nls.localize('error.defaultMessage', "An unknown error occurred. Please consult the log for more details.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to generate a human readable error message out of the error. If the verbose parameter
|
||||
* is set to true, the error message will include stacktrace details if provided.
|
||||
*
|
||||
* @returns A string containing the error message.
|
||||
*/
|
||||
export function toErrorMessage(error: any = null, verbose: boolean = false): string {
|
||||
if (!error) {
|
||||
return nls.localize('error.defaultMessage', "An unknown error occurred. Please consult the log for more details.");
|
||||
}
|
||||
|
||||
if (Array.isArray(error)) {
|
||||
const errors: any[] = arrays.coalesce(error);
|
||||
const msg = toErrorMessage(errors[0], verbose);
|
||||
|
||||
if (errors.length > 1) {
|
||||
return nls.localize('error.moreErrors', "{0} ({1} errors in total)", msg, errors.length);
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
if (types.isString(error)) {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error.detail) {
|
||||
const detail = error.detail;
|
||||
|
||||
if (detail.error) {
|
||||
return exceptionToErrorMessage(detail.error, verbose);
|
||||
}
|
||||
|
||||
if (detail.exception) {
|
||||
return exceptionToErrorMessage(detail.exception, verbose);
|
||||
}
|
||||
}
|
||||
|
||||
if (error.stack) {
|
||||
return exceptionToErrorMessage(error, verbose);
|
||||
}
|
||||
|
||||
if (error.message) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return nls.localize('error.defaultMessage', "An unknown error occurred. Please consult the log for more details.");
|
||||
}
|
||||
|
||||
|
||||
export interface IErrorWithActions extends Error {
|
||||
actions: IAction[];
|
||||
}
|
||||
|
||||
export function isErrorWithActions(obj: unknown): obj is IErrorWithActions {
|
||||
const candidate = obj as IErrorWithActions | undefined;
|
||||
|
||||
return candidate instanceof Error && Array.isArray(candidate.actions);
|
||||
}
|
||||
|
||||
export function createErrorWithActions(messageOrError: string | Error, actions: IAction[]): IErrorWithActions {
|
||||
let error: IErrorWithActions;
|
||||
if (typeof messageOrError === 'string') {
|
||||
error = new Error(messageOrError) as IErrorWithActions;
|
||||
} else {
|
||||
error = messageOrError as IErrorWithActions;
|
||||
}
|
||||
|
||||
error.actions = actions;
|
||||
|
||||
return error;
|
||||
}
|
||||
@@ -1,423 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
import { isAbsolute, join, normalize, posix, sep } from 'vs/base/common/path';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { equalsIgnoreCase, rtrim, startsWithIgnoreCase } from 'vs/base/common/strings';
|
||||
import { isNumber } from 'vs/base/common/types';
|
||||
|
||||
export function isPathSeparator(code: number) {
|
||||
return code === CharCode.Slash || code === CharCode.Backslash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a Windows OS path and changes backward slashes to forward slashes.
|
||||
* This should only be done for OS paths from Windows (or user provided paths potentially from Windows).
|
||||
* Using it on a Linux or MaxOS path might change it.
|
||||
*/
|
||||
export function toSlashes(osPath: string) {
|
||||
return osPath.replace(/[\\/]/g, posix.sep);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a Windows OS path (using backward or forward slashes) and turns it into a posix path:
|
||||
* - turns backward slashes into forward slashes
|
||||
* - makes it absolute if it starts with a drive letter
|
||||
* This should only be done for OS paths from Windows (or user provided paths potentially from Windows).
|
||||
* Using it on a Linux or MaxOS path might change it.
|
||||
*/
|
||||
export function toPosixPath(osPath: string) {
|
||||
if (osPath.indexOf('/') === -1) {
|
||||
osPath = toSlashes(osPath);
|
||||
}
|
||||
if (/^[a-zA-Z]:(\/|$)/.test(osPath)) { // starts with a drive letter
|
||||
osPath = '/' + osPath;
|
||||
}
|
||||
return osPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the _root_ this path, like `getRoot('c:\files') === c:\`,
|
||||
* `getRoot('files:///files/path') === files:///`,
|
||||
* or `getRoot('\\server\shares\path') === \\server\shares\`
|
||||
*/
|
||||
export function getRoot(path: string, sep: string = posix.sep): string {
|
||||
if (!path) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const len = path.length;
|
||||
const firstLetter = path.charCodeAt(0);
|
||||
if (isPathSeparator(firstLetter)) {
|
||||
if (isPathSeparator(path.charCodeAt(1))) {
|
||||
// UNC candidate \\localhost\shares\ddd
|
||||
// ^^^^^^^^^^^^^^^^^^^
|
||||
if (!isPathSeparator(path.charCodeAt(2))) {
|
||||
let pos = 3;
|
||||
const start = pos;
|
||||
for (; pos < len; pos++) {
|
||||
if (isPathSeparator(path.charCodeAt(pos))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (start !== pos && !isPathSeparator(path.charCodeAt(pos + 1))) {
|
||||
pos += 1;
|
||||
for (; pos < len; pos++) {
|
||||
if (isPathSeparator(path.charCodeAt(pos))) {
|
||||
return path.slice(0, pos + 1) // consume this separator
|
||||
.replace(/[\\/]/g, sep);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// /user/far
|
||||
// ^
|
||||
return sep;
|
||||
|
||||
} else if (isWindowsDriveLetter(firstLetter)) {
|
||||
// check for windows drive letter c:\ or c:
|
||||
|
||||
if (path.charCodeAt(1) === CharCode.Colon) {
|
||||
if (isPathSeparator(path.charCodeAt(2))) {
|
||||
// C:\fff
|
||||
// ^^^
|
||||
return path.slice(0, 2) + sep;
|
||||
} else {
|
||||
// C:
|
||||
// ^^
|
||||
return path.slice(0, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check for URI
|
||||
// scheme://authority/path
|
||||
// ^^^^^^^^^^^^^^^^^^^
|
||||
let pos = path.indexOf('://');
|
||||
if (pos !== -1) {
|
||||
pos += 3; // 3 -> "://".length
|
||||
for (; pos < len; pos++) {
|
||||
if (isPathSeparator(path.charCodeAt(pos))) {
|
||||
return path.slice(0, pos + 1); // consume this separator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the path follows this pattern: `\\hostname\sharename`.
|
||||
*
|
||||
* @see https://msdn.microsoft.com/en-us/library/gg465305.aspx
|
||||
* @return A boolean indication if the path is a UNC path, on none-windows
|
||||
* always false.
|
||||
*/
|
||||
export function isUNC(path: string): boolean {
|
||||
if (!isWindows) {
|
||||
// UNC is a windows concept
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!path || path.length < 5) {
|
||||
// at least \\a\b
|
||||
return false;
|
||||
}
|
||||
|
||||
let code = path.charCodeAt(0);
|
||||
if (code !== CharCode.Backslash) {
|
||||
return false;
|
||||
}
|
||||
|
||||
code = path.charCodeAt(1);
|
||||
|
||||
if (code !== CharCode.Backslash) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let pos = 2;
|
||||
const start = pos;
|
||||
for (; pos < path.length; pos++) {
|
||||
code = path.charCodeAt(pos);
|
||||
if (code === CharCode.Backslash) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (start === pos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
code = path.charCodeAt(pos + 1);
|
||||
|
||||
if (isNaN(code) || code === CharCode.Backslash) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reference: https://en.wikipedia.org/wiki/Filename
|
||||
const WINDOWS_INVALID_FILE_CHARS = /[\\/:\*\?"<>\|]/g;
|
||||
const UNIX_INVALID_FILE_CHARS = /[/]/g;
|
||||
const WINDOWS_FORBIDDEN_NAMES = /^(con|prn|aux|clock\$|nul|lpt[0-9]|com[0-9])(\.(.*?))?$/i;
|
||||
export function isValidBasename(name: string | null | undefined, isWindowsOS: boolean = isWindows): boolean {
|
||||
const invalidFileChars = isWindowsOS ? WINDOWS_INVALID_FILE_CHARS : UNIX_INVALID_FILE_CHARS;
|
||||
|
||||
if (!name || name.length === 0 || /^\s+$/.test(name)) {
|
||||
return false; // require a name that is not just whitespace
|
||||
}
|
||||
|
||||
invalidFileChars.lastIndex = 0; // the holy grail of software development
|
||||
if (invalidFileChars.test(name)) {
|
||||
return false; // check for certain invalid file characters
|
||||
}
|
||||
|
||||
if (isWindowsOS && WINDOWS_FORBIDDEN_NAMES.test(name)) {
|
||||
return false; // check for certain invalid file names
|
||||
}
|
||||
|
||||
if (name === '.' || name === '..') {
|
||||
return false; // check for reserved values
|
||||
}
|
||||
|
||||
if (isWindowsOS && name[name.length - 1] === '.') {
|
||||
return false; // Windows: file cannot end with a "."
|
||||
}
|
||||
|
||||
if (isWindowsOS && name.length !== name.trim().length) {
|
||||
return false; // Windows: file cannot end with a whitespace
|
||||
}
|
||||
|
||||
if (name.length > 255) {
|
||||
return false; // most file systems do not allow files > 255 length
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated please use `IUriIdentityService.extUri.isEqual` instead. If you are
|
||||
* in a context without services, consider to pass down the `extUri` from the outside
|
||||
* or use `extUriBiasedIgnorePathCase` if you know what you are doing.
|
||||
*/
|
||||
export function isEqual(pathA: string, pathB: string, ignoreCase?: boolean): boolean {
|
||||
const identityEquals = (pathA === pathB);
|
||||
if (!ignoreCase || identityEquals) {
|
||||
return identityEquals;
|
||||
}
|
||||
|
||||
if (!pathA || !pathB) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return equalsIgnoreCase(pathA, pathB);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated please use `IUriIdentityService.extUri.isEqualOrParent` instead. If
|
||||
* you are in a context without services, consider to pass down the `extUri` from the
|
||||
* outside, or use `extUriBiasedIgnorePathCase` if you know what you are doing.
|
||||
*/
|
||||
export function isEqualOrParent(base: string, parentCandidate: string, ignoreCase?: boolean, separator = sep): boolean {
|
||||
if (base === parentCandidate) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!base || !parentCandidate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parentCandidate.length > base.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ignoreCase) {
|
||||
const beginsWith = startsWithIgnoreCase(base, parentCandidate);
|
||||
if (!beginsWith) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parentCandidate.length === base.length) {
|
||||
return true; // same path, different casing
|
||||
}
|
||||
|
||||
let sepOffset = parentCandidate.length;
|
||||
if (parentCandidate.charAt(parentCandidate.length - 1) === separator) {
|
||||
sepOffset--; // adjust the expected sep offset in case our candidate already ends in separator character
|
||||
}
|
||||
|
||||
return base.charAt(sepOffset) === separator;
|
||||
}
|
||||
|
||||
if (parentCandidate.charAt(parentCandidate.length - 1) !== separator) {
|
||||
parentCandidate += separator;
|
||||
}
|
||||
|
||||
return base.indexOf(parentCandidate) === 0;
|
||||
}
|
||||
|
||||
export function isWindowsDriveLetter(char0: number): boolean {
|
||||
return char0 >= CharCode.A && char0 <= CharCode.Z || char0 >= CharCode.a && char0 <= CharCode.z;
|
||||
}
|
||||
|
||||
export function sanitizeFilePath(candidate: string, cwd: string): string {
|
||||
|
||||
// Special case: allow to open a drive letter without trailing backslash
|
||||
if (isWindows && candidate.endsWith(':')) {
|
||||
candidate += sep;
|
||||
}
|
||||
|
||||
// Ensure absolute
|
||||
if (!isAbsolute(candidate)) {
|
||||
candidate = join(cwd, candidate);
|
||||
}
|
||||
|
||||
// Ensure normalized
|
||||
candidate = normalize(candidate);
|
||||
|
||||
// Ensure no trailing slash/backslash
|
||||
return removeTrailingPathSeparator(candidate);
|
||||
}
|
||||
|
||||
export function removeTrailingPathSeparator(candidate: string): string {
|
||||
if (isWindows) {
|
||||
candidate = rtrim(candidate, sep);
|
||||
|
||||
// Special case: allow to open drive root ('C:\')
|
||||
if (candidate.endsWith(':')) {
|
||||
candidate += sep;
|
||||
}
|
||||
|
||||
} else {
|
||||
candidate = rtrim(candidate, sep);
|
||||
|
||||
// Special case: allow to open root ('/')
|
||||
if (!candidate) {
|
||||
candidate = sep;
|
||||
}
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
export function isRootOrDriveLetter(path: string): boolean {
|
||||
const pathNormalized = normalize(path);
|
||||
|
||||
if (isWindows) {
|
||||
if (path.length > 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hasDriveLetter(pathNormalized) &&
|
||||
(path.length === 2 || pathNormalized.charCodeAt(2) === CharCode.Backslash);
|
||||
}
|
||||
|
||||
return pathNormalized === posix.sep;
|
||||
}
|
||||
|
||||
export function hasDriveLetter(path: string, isWindowsOS: boolean = isWindows): boolean {
|
||||
if (isWindowsOS) {
|
||||
return isWindowsDriveLetter(path.charCodeAt(0)) && path.charCodeAt(1) === CharCode.Colon;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getDriveLetter(path: string, isWindowsOS: boolean = isWindows): string | undefined {
|
||||
return hasDriveLetter(path, isWindowsOS) ? path[0] : undefined;
|
||||
}
|
||||
|
||||
export function indexOfPath(path: string, candidate: string, ignoreCase?: boolean): number {
|
||||
if (candidate.length > path.length) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (path === candidate) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ignoreCase) {
|
||||
path = path.toLowerCase();
|
||||
candidate = candidate.toLowerCase();
|
||||
}
|
||||
|
||||
return path.indexOf(candidate);
|
||||
}
|
||||
|
||||
export interface IPathWithLineAndColumn {
|
||||
path: string;
|
||||
line?: number;
|
||||
column?: number;
|
||||
}
|
||||
|
||||
export function parseLineAndColumnAware(rawPath: string): IPathWithLineAndColumn {
|
||||
const segments = rawPath.split(':'); // C:\file.txt:<line>:<column>
|
||||
|
||||
let path: string | undefined = undefined;
|
||||
let line: number | undefined = undefined;
|
||||
let column: number | undefined = undefined;
|
||||
|
||||
for (const segment of segments) {
|
||||
const segmentAsNumber = Number(segment);
|
||||
if (!isNumber(segmentAsNumber)) {
|
||||
path = !!path ? [path, segment].join(':') : segment; // a colon can well be part of a path (e.g. C:\...)
|
||||
} else if (line === undefined) {
|
||||
line = segmentAsNumber;
|
||||
} else if (column === undefined) {
|
||||
column = segmentAsNumber;
|
||||
}
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
throw new Error('Format for `--goto` should be: `FILE:LINE(:COLUMN)`');
|
||||
}
|
||||
|
||||
return {
|
||||
path,
|
||||
line: line !== undefined ? line : undefined,
|
||||
column: column !== undefined ? column : line !== undefined ? 1 : undefined // if we have a line, make sure column is also set
|
||||
};
|
||||
}
|
||||
|
||||
const pathChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const windowsSafePathFirstChars = 'BDEFGHIJKMOQRSTUVWXYZbdefghijkmoqrstuvwxyz0123456789';
|
||||
|
||||
export function randomPath(parent?: string, prefix?: string, randomLength = 8): string {
|
||||
let suffix = '';
|
||||
for (let i = 0; i < randomLength; i++) {
|
||||
let pathCharsTouse: string;
|
||||
if (i === 0 && isWindows && !prefix && (randomLength === 3 || randomLength === 4)) {
|
||||
|
||||
// Windows has certain reserved file names that cannot be used, such
|
||||
// as AUX, CON, PRN, etc. We want to avoid generating a random name
|
||||
// that matches that pattern, so we use a different set of characters
|
||||
// for the first character of the name that does not include any of
|
||||
// the reserved names first characters.
|
||||
|
||||
pathCharsTouse = windowsSafePathFirstChars;
|
||||
} else {
|
||||
pathCharsTouse = pathChars;
|
||||
}
|
||||
|
||||
suffix += pathCharsTouse.charAt(Math.floor(Math.random() * pathCharsTouse.length));
|
||||
}
|
||||
|
||||
let randomFileName: string;
|
||||
if (prefix) {
|
||||
randomFileName = `${prefix}-${suffix}`;
|
||||
} else {
|
||||
randomFileName = suffix;
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
return join(parent, randomFileName);
|
||||
}
|
||||
|
||||
return randomFileName;
|
||||
}
|
||||
@@ -1,737 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { equals } from 'vs/base/common/arrays';
|
||||
import { isThenable } from 'vs/base/common/async';
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
import { isEqualOrParent } from 'vs/base/common/extpath';
|
||||
import { basename, extname, posix, sep } from 'vs/base/common/path';
|
||||
import { isLinux } from 'vs/base/common/platform';
|
||||
import { escapeRegExpCharacters, ltrim } from 'vs/base/common/strings';
|
||||
|
||||
export interface IRelativePattern {
|
||||
|
||||
/**
|
||||
* A base file path to which this pattern will be matched against relatively.
|
||||
*/
|
||||
readonly base: string;
|
||||
|
||||
/**
|
||||
* A file glob pattern like `*.{ts,js}` that will be matched on file paths
|
||||
* relative to the base path.
|
||||
*
|
||||
* Example: Given a base of `/home/work/folder` and a file path of `/home/work/folder/index.js`,
|
||||
* the file glob pattern will match on `index.js`.
|
||||
*/
|
||||
readonly pattern: string;
|
||||
}
|
||||
|
||||
export interface IExpression {
|
||||
[pattern: string]: boolean | SiblingClause;
|
||||
}
|
||||
|
||||
export function getEmptyExpression(): IExpression {
|
||||
return Object.create(null);
|
||||
}
|
||||
|
||||
interface SiblingClause {
|
||||
when: string;
|
||||
}
|
||||
|
||||
export const GLOBSTAR = '**';
|
||||
export const GLOB_SPLIT = '/';
|
||||
|
||||
const PATH_REGEX = '[/\\\\]'; // any slash or backslash
|
||||
const NO_PATH_REGEX = '[^/\\\\]'; // any non-slash and non-backslash
|
||||
const ALL_FORWARD_SLASHES = /\//g;
|
||||
|
||||
function starsToRegExp(starCount: number, isLastPattern?: boolean): string {
|
||||
switch (starCount) {
|
||||
case 0:
|
||||
return '';
|
||||
case 1:
|
||||
return `${NO_PATH_REGEX}*?`; // 1 star matches any number of characters except path separator (/ and \) - non greedy (?)
|
||||
default:
|
||||
// Matches: (Path Sep OR Path Val followed by Path Sep) 0-many times except when it's the last pattern
|
||||
// in which case also matches (Path Sep followed by Path Val)
|
||||
// Group is non capturing because we don't need to capture at all (?:...)
|
||||
// Overall we use non-greedy matching because it could be that we match too much
|
||||
return `(?:${PATH_REGEX}|${NO_PATH_REGEX}+${PATH_REGEX}${isLastPattern ? `|${PATH_REGEX}${NO_PATH_REGEX}+` : ''})*?`;
|
||||
}
|
||||
}
|
||||
|
||||
export function splitGlobAware(pattern: string, splitChar: string): string[] {
|
||||
if (!pattern) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const segments: string[] = [];
|
||||
|
||||
let inBraces = false;
|
||||
let inBrackets = false;
|
||||
|
||||
let curVal = '';
|
||||
for (const char of pattern) {
|
||||
switch (char) {
|
||||
case splitChar:
|
||||
if (!inBraces && !inBrackets) {
|
||||
segments.push(curVal);
|
||||
curVal = '';
|
||||
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case '{':
|
||||
inBraces = true;
|
||||
break;
|
||||
case '}':
|
||||
inBraces = false;
|
||||
break;
|
||||
case '[':
|
||||
inBrackets = true;
|
||||
break;
|
||||
case ']':
|
||||
inBrackets = false;
|
||||
break;
|
||||
}
|
||||
|
||||
curVal += char;
|
||||
}
|
||||
|
||||
// Tail
|
||||
if (curVal) {
|
||||
segments.push(curVal);
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
function parseRegExp(pattern: string): string {
|
||||
if (!pattern) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let regEx = '';
|
||||
|
||||
// Split up into segments for each slash found
|
||||
const segments = splitGlobAware(pattern, GLOB_SPLIT);
|
||||
|
||||
// Special case where we only have globstars
|
||||
if (segments.every(segment => segment === GLOBSTAR)) {
|
||||
regEx = '.*';
|
||||
}
|
||||
|
||||
// Build regex over segments
|
||||
else {
|
||||
let previousSegmentWasGlobStar = false;
|
||||
segments.forEach((segment, index) => {
|
||||
|
||||
// Treat globstar specially
|
||||
if (segment === GLOBSTAR) {
|
||||
|
||||
// if we have more than one globstar after another, just ignore it
|
||||
if (previousSegmentWasGlobStar) {
|
||||
return;
|
||||
}
|
||||
|
||||
regEx += starsToRegExp(2, index === segments.length - 1);
|
||||
}
|
||||
|
||||
// Anything else, not globstar
|
||||
else {
|
||||
|
||||
// States
|
||||
let inBraces = false;
|
||||
let braceVal = '';
|
||||
|
||||
let inBrackets = false;
|
||||
let bracketVal = '';
|
||||
|
||||
for (const char of segment) {
|
||||
|
||||
// Support brace expansion
|
||||
if (char !== '}' && inBraces) {
|
||||
braceVal += char;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Support brackets
|
||||
if (inBrackets && (char !== ']' || !bracketVal) /* ] is literally only allowed as first character in brackets to match it */) {
|
||||
let res: string;
|
||||
|
||||
// range operator
|
||||
if (char === '-') {
|
||||
res = char;
|
||||
}
|
||||
|
||||
// negation operator (only valid on first index in bracket)
|
||||
else if ((char === '^' || char === '!') && !bracketVal) {
|
||||
res = '^';
|
||||
}
|
||||
|
||||
// glob split matching is not allowed within character ranges
|
||||
// see http://man7.org/linux/man-pages/man7/glob.7.html
|
||||
else if (char === GLOB_SPLIT) {
|
||||
res = '';
|
||||
}
|
||||
|
||||
// anything else gets escaped
|
||||
else {
|
||||
res = escapeRegExpCharacters(char);
|
||||
}
|
||||
|
||||
bracketVal += res;
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (char) {
|
||||
case '{':
|
||||
inBraces = true;
|
||||
continue;
|
||||
|
||||
case '[':
|
||||
inBrackets = true;
|
||||
continue;
|
||||
|
||||
case '}': {
|
||||
const choices = splitGlobAware(braceVal, ',');
|
||||
|
||||
// Converts {foo,bar} => [foo|bar]
|
||||
const braceRegExp = `(?:${choices.map(choice => parseRegExp(choice)).join('|')})`;
|
||||
|
||||
regEx += braceRegExp;
|
||||
|
||||
inBraces = false;
|
||||
braceVal = '';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case ']': {
|
||||
regEx += ('[' + bracketVal + ']');
|
||||
|
||||
inBrackets = false;
|
||||
bracketVal = '';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case '?':
|
||||
regEx += NO_PATH_REGEX; // 1 ? matches any single character except path separator (/ and \)
|
||||
continue;
|
||||
|
||||
case '*':
|
||||
regEx += starsToRegExp(1);
|
||||
continue;
|
||||
|
||||
default:
|
||||
regEx += escapeRegExpCharacters(char);
|
||||
}
|
||||
}
|
||||
|
||||
// Tail: Add the slash we had split on if there is more to
|
||||
// come and the remaining pattern is not a globstar
|
||||
// For example if pattern: some/**/*.js we want the "/" after
|
||||
// some to be included in the RegEx to prevent a folder called
|
||||
// "something" to match as well.
|
||||
if (
|
||||
index < segments.length - 1 && // more segments to come after this
|
||||
(
|
||||
segments[index + 1] !== GLOBSTAR || // next segment is not **, or...
|
||||
index + 2 < segments.length // ...next segment is ** but there is more segments after that
|
||||
)
|
||||
) {
|
||||
regEx += PATH_REGEX;
|
||||
}
|
||||
}
|
||||
|
||||
// update globstar state
|
||||
previousSegmentWasGlobStar = (segment === GLOBSTAR);
|
||||
});
|
||||
}
|
||||
|
||||
return regEx;
|
||||
}
|
||||
|
||||
// regexes to check for trivial glob patterns that just check for String#endsWith
|
||||
const T1 = /^\*\*\/\*\.[\w\.-]+$/; // **/*.something
|
||||
const T2 = /^\*\*\/([\w\.-]+)\/?$/; // **/something
|
||||
const T3 = /^{\*\*\/\*?[\w\.-]+\/?(,\*\*\/\*?[\w\.-]+\/?)*}$/; // {**/*.something,**/*.else} or {**/package.json,**/project.json}
|
||||
const T3_2 = /^{\*\*\/\*?[\w\.-]+(\/(\*\*)?)?(,\*\*\/\*?[\w\.-]+(\/(\*\*)?)?)*}$/; // Like T3, with optional trailing /**
|
||||
const T4 = /^\*\*((\/[\w\.-]+)+)\/?$/; // **/something/else
|
||||
const T5 = /^([\w\.-]+(\/[\w\.-]+)*)\/?$/; // something/else
|
||||
|
||||
export type ParsedPattern = (path: string, basename?: string) => boolean;
|
||||
|
||||
// The `ParsedExpression` returns a `Promise`
|
||||
// iff `hasSibling` returns a `Promise`.
|
||||
export type ParsedExpression = (path: string, basename?: string, hasSibling?: (name: string) => boolean | Promise<boolean>) => string | null | Promise<string | null> /* the matching pattern */;
|
||||
|
||||
interface IGlobOptions {
|
||||
|
||||
/**
|
||||
* Simplify patterns for use as exclusion filters during
|
||||
* tree traversal to skip entire subtrees. Cannot be used
|
||||
* outside of a tree traversal.
|
||||
*/
|
||||
trimForExclusions?: boolean;
|
||||
}
|
||||
|
||||
interface ParsedStringPattern {
|
||||
(path: string, basename?: string): string | null | Promise<string | null> /* the matching pattern */;
|
||||
basenames?: string[];
|
||||
patterns?: string[];
|
||||
allBasenames?: string[];
|
||||
allPaths?: string[];
|
||||
}
|
||||
|
||||
interface ParsedExpressionPattern {
|
||||
(path: string, basename?: string, name?: string, hasSibling?: (name: string) => boolean | Promise<boolean>): string | null | Promise<string | null> /* the matching pattern */;
|
||||
requiresSiblings?: boolean;
|
||||
allBasenames?: string[];
|
||||
allPaths?: string[];
|
||||
}
|
||||
|
||||
const FALSE = function () {
|
||||
return false;
|
||||
};
|
||||
|
||||
const NULL = function (): string | null {
|
||||
return null;
|
||||
};
|
||||
|
||||
function trimForExclusions(pattern: string, options: IGlobOptions): string {
|
||||
return options.trimForExclusions && pattern.endsWith('/**') ? pattern.substr(0, pattern.length - 2) : pattern; // dropping **, tailing / is dropped later
|
||||
}
|
||||
|
||||
// common pattern: **/*.txt just need endsWith check
|
||||
function trivia1(base: string, pattern: string): ParsedStringPattern {
|
||||
return function (path: string, basename?: string) {
|
||||
return typeof path === 'string' && path.endsWith(base) ? pattern : null;
|
||||
};
|
||||
}
|
||||
|
||||
// common pattern: **/some.txt just need basename check
|
||||
function trivia2(base: string, pattern: string): ParsedStringPattern {
|
||||
const slashBase = `/${base}`;
|
||||
const backslashBase = `\\${base}`;
|
||||
|
||||
const parsedPattern: ParsedStringPattern = function (path: string, basename?: string) {
|
||||
if (typeof path !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (basename) {
|
||||
return basename === base ? pattern : null;
|
||||
}
|
||||
|
||||
return path === base || path.endsWith(slashBase) || path.endsWith(backslashBase) ? pattern : null;
|
||||
};
|
||||
|
||||
const basenames = [base];
|
||||
parsedPattern.basenames = basenames;
|
||||
parsedPattern.patterns = [pattern];
|
||||
parsedPattern.allBasenames = basenames;
|
||||
|
||||
return parsedPattern;
|
||||
}
|
||||
|
||||
// repetition of common patterns (see above) {**/*.txt,**/*.png}
|
||||
function trivia3(pattern: string, options: IGlobOptions): ParsedStringPattern {
|
||||
const parsedPatterns = aggregateBasenameMatches(pattern.slice(1, -1)
|
||||
.split(',')
|
||||
.map(pattern => parsePattern(pattern, options))
|
||||
.filter(pattern => pattern !== NULL), pattern);
|
||||
|
||||
const patternsLength = parsedPatterns.length;
|
||||
if (!patternsLength) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (patternsLength === 1) {
|
||||
return parsedPatterns[0];
|
||||
}
|
||||
|
||||
const parsedPattern: ParsedStringPattern = function (path: string, basename?: string) {
|
||||
for (let i = 0, n = parsedPatterns.length; i < n; i++) {
|
||||
if (parsedPatterns[i](path, basename)) {
|
||||
return pattern;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const withBasenames = parsedPatterns.find(pattern => !!pattern.allBasenames);
|
||||
if (withBasenames) {
|
||||
parsedPattern.allBasenames = withBasenames.allBasenames;
|
||||
}
|
||||
|
||||
const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, [] as string[]);
|
||||
if (allPaths.length) {
|
||||
parsedPattern.allPaths = allPaths;
|
||||
}
|
||||
|
||||
return parsedPattern;
|
||||
}
|
||||
|
||||
// common patterns: **/something/else just need endsWith check, something/else just needs and equals check
|
||||
function trivia4and5(targetPath: string, pattern: string, matchPathEnds: boolean): ParsedStringPattern {
|
||||
const usingPosixSep = sep === posix.sep;
|
||||
const nativePath = usingPosixSep ? targetPath : targetPath.replace(ALL_FORWARD_SLASHES, sep);
|
||||
const nativePathEnd = sep + nativePath;
|
||||
const targetPathEnd = posix.sep + targetPath;
|
||||
|
||||
let parsedPattern: ParsedStringPattern;
|
||||
if (matchPathEnds) {
|
||||
parsedPattern = function (path: string, basename?: string) {
|
||||
return typeof path === 'string' && ((path === nativePath || path.endsWith(nativePathEnd)) || !usingPosixSep && (path === targetPath || path.endsWith(targetPathEnd))) ? pattern : null;
|
||||
};
|
||||
} else {
|
||||
parsedPattern = function (path: string, basename?: string) {
|
||||
return typeof path === 'string' && (path === nativePath || (!usingPosixSep && path === targetPath)) ? pattern : null;
|
||||
};
|
||||
}
|
||||
|
||||
parsedPattern.allPaths = [(matchPathEnds ? '*/' : './') + targetPath];
|
||||
|
||||
return parsedPattern;
|
||||
}
|
||||
|
||||
function toRegExp(pattern: string): ParsedStringPattern {
|
||||
try {
|
||||
const regExp = new RegExp(`^${parseRegExp(pattern)}$`);
|
||||
return function (path: string) {
|
||||
regExp.lastIndex = 0; // reset RegExp to its initial state to reuse it!
|
||||
|
||||
return typeof path === 'string' && regExp.test(path) ? pattern : null;
|
||||
};
|
||||
} catch (error) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified glob matching. Supports a subset of glob patterns:
|
||||
* * `*` to match zero or more characters in a path segment
|
||||
* * `?` to match on one character in a path segment
|
||||
* * `**` to match any number of path segments, including none
|
||||
* * `{}` to group conditions (e.g. *.{ts,js} matches all TypeScript and JavaScript files)
|
||||
* * `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
|
||||
* * `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
|
||||
*/
|
||||
export function match(pattern: string | IRelativePattern, path: string): boolean;
|
||||
export function match(expression: IExpression, path: string, hasSibling?: (name: string) => boolean): string /* the matching pattern */;
|
||||
export function match(arg1: string | IExpression | IRelativePattern, path: string, hasSibling?: (name: string) => boolean): boolean | string | null | Promise<string | null> {
|
||||
if (!arg1 || typeof path !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parse(arg1)(path, undefined, hasSibling);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified glob matching. Supports a subset of glob patterns:
|
||||
* * `*` to match zero or more characters in a path segment
|
||||
* * `?` to match on one character in a path segment
|
||||
* * `**` to match any number of path segments, including none
|
||||
* * `{}` to group conditions (e.g. *.{ts,js} matches all TypeScript and JavaScript files)
|
||||
* * `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
|
||||
* * `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
|
||||
*/
|
||||
export function parse(pattern: string | IRelativePattern, options?: IGlobOptions): ParsedPattern;
|
||||
export function parse(expression: IExpression, options?: IGlobOptions): ParsedExpression;
|
||||
export function parse(arg1: string | IExpression | IRelativePattern, options?: IGlobOptions): ParsedPattern | ParsedExpression;
|
||||
export function parse(arg1: string | IExpression | IRelativePattern, options: IGlobOptions = {}): ParsedPattern | ParsedExpression {
|
||||
if (!arg1) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Glob with String
|
||||
if (typeof arg1 === 'string' || isRelativePattern(arg1)) {
|
||||
const parsedPattern = parsePattern(arg1, options);
|
||||
if (parsedPattern === NULL) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
const resultPattern: ParsedPattern & { allBasenames?: string[]; allPaths?: string[] } = function (path: string, basename?: string) {
|
||||
return !!parsedPattern(path, basename);
|
||||
};
|
||||
|
||||
if (parsedPattern.allBasenames) {
|
||||
resultPattern.allBasenames = parsedPattern.allBasenames;
|
||||
}
|
||||
|
||||
if (parsedPattern.allPaths) {
|
||||
resultPattern.allPaths = parsedPattern.allPaths;
|
||||
}
|
||||
|
||||
return resultPattern;
|
||||
}
|
||||
|
||||
// Glob with Expression
|
||||
return parsedExpression(<IExpression>arg1, options);
|
||||
}
|
||||
|
||||
export function isRelativePattern(obj: unknown): obj is IRelativePattern {
|
||||
const rp = obj as IRelativePattern | undefined | null;
|
||||
if (!rp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return typeof rp.base === 'string' && typeof rp.pattern === 'string';
|
||||
}
|
||||
|
||||
export function getBasenameTerms(patternOrExpression: ParsedPattern | ParsedExpression): string[] {
|
||||
return (<ParsedStringPattern>patternOrExpression).allBasenames || [];
|
||||
}
|
||||
|
||||
export function getPathTerms(patternOrExpression: ParsedPattern | ParsedExpression): string[] {
|
||||
return (<ParsedStringPattern>patternOrExpression).allPaths || [];
|
||||
}
|
||||
|
||||
function parsedExpression(expression: IExpression, options: IGlobOptions): ParsedExpression {
|
||||
const parsedPatterns = aggregateBasenameMatches(Object.getOwnPropertyNames(expression)
|
||||
.map(pattern => parseExpressionPattern(pattern, expression[pattern], options))
|
||||
.filter(pattern => pattern !== NULL));
|
||||
|
||||
const patternsLength = parsedPatterns.length;
|
||||
if (!patternsLength) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!parsedPatterns.some(parsedPattern => !!(<ParsedExpressionPattern>parsedPattern).requiresSiblings)) {
|
||||
if (patternsLength === 1) {
|
||||
return parsedPatterns[0] as ParsedStringPattern;
|
||||
}
|
||||
|
||||
const resultExpression: ParsedStringPattern = function (path: string, basename?: string) {
|
||||
let resultPromises: Promise<string | null>[] | undefined = undefined;
|
||||
|
||||
for (let i = 0, n = parsedPatterns.length; i < n; i++) {
|
||||
const result = parsedPatterns[i](path, basename);
|
||||
if (typeof result === 'string') {
|
||||
return result; // immediately return as soon as the first expression matches
|
||||
}
|
||||
|
||||
// If the result is a promise, we have to keep it for
|
||||
// later processing and await the result properly.
|
||||
if (isThenable(result)) {
|
||||
if (!resultPromises) {
|
||||
resultPromises = [];
|
||||
}
|
||||
|
||||
resultPromises.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
// With result promises, we have to loop over each and
|
||||
// await the result before we can return any result.
|
||||
if (resultPromises) {
|
||||
return (async () => {
|
||||
for (const resultPromise of resultPromises) {
|
||||
const result = await resultPromise;
|
||||
if (typeof result === 'string') {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
})();
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const withBasenames = parsedPatterns.find(pattern => !!pattern.allBasenames);
|
||||
if (withBasenames) {
|
||||
resultExpression.allBasenames = withBasenames.allBasenames;
|
||||
}
|
||||
|
||||
const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, [] as string[]);
|
||||
if (allPaths.length) {
|
||||
resultExpression.allPaths = allPaths;
|
||||
}
|
||||
|
||||
return resultExpression;
|
||||
}
|
||||
|
||||
const resultExpression: ParsedStringPattern = function (path: string, base?: string, hasSibling?: (name: string) => boolean | Promise<boolean>) {
|
||||
let name: string | undefined = undefined;
|
||||
let resultPromises: Promise<string | null>[] | undefined = undefined;
|
||||
|
||||
for (let i = 0, n = parsedPatterns.length; i < n; i++) {
|
||||
|
||||
// Pattern matches path
|
||||
const parsedPattern = (<ParsedExpressionPattern>parsedPatterns[i]);
|
||||
if (parsedPattern.requiresSiblings && hasSibling) {
|
||||
if (!base) {
|
||||
base = basename(path);
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
name = base.substr(0, base.length - extname(path).length);
|
||||
}
|
||||
}
|
||||
|
||||
const result = parsedPattern(path, base, name, hasSibling);
|
||||
if (typeof result === 'string') {
|
||||
return result; // immediately return as soon as the first expression matches
|
||||
}
|
||||
|
||||
// If the result is a promise, we have to keep it for
|
||||
// later processing and await the result properly.
|
||||
if (isThenable(result)) {
|
||||
if (!resultPromises) {
|
||||
resultPromises = [];
|
||||
}
|
||||
|
||||
resultPromises.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
// With result promises, we have to loop over each and
|
||||
// await the result before we can return any result.
|
||||
if (resultPromises) {
|
||||
return (async () => {
|
||||
for (const resultPromise of resultPromises) {
|
||||
const result = await resultPromise;
|
||||
if (typeof result === 'string') {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
})();
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const withBasenames = parsedPatterns.find(pattern => !!pattern.allBasenames);
|
||||
if (withBasenames) {
|
||||
resultExpression.allBasenames = withBasenames.allBasenames;
|
||||
}
|
||||
|
||||
const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, [] as string[]);
|
||||
if (allPaths.length) {
|
||||
resultExpression.allPaths = allPaths;
|
||||
}
|
||||
|
||||
return resultExpression;
|
||||
}
|
||||
|
||||
function parseExpressionPattern(pattern: string, value: boolean | SiblingClause, options: IGlobOptions): (ParsedStringPattern | ParsedExpressionPattern) {
|
||||
if (value === false) {
|
||||
return NULL; // pattern is disabled
|
||||
}
|
||||
|
||||
const parsedPattern = parsePattern(pattern, options);
|
||||
if (parsedPattern === NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Expression Pattern is <boolean>
|
||||
if (typeof value === 'boolean') {
|
||||
return parsedPattern;
|
||||
}
|
||||
|
||||
// Expression Pattern is <SiblingClause>
|
||||
if (value) {
|
||||
const when = value.when;
|
||||
if (typeof when === 'string') {
|
||||
const result: ParsedExpressionPattern = (path: string, basename?: string, name?: string, hasSibling?: (name: string) => boolean | Promise<boolean>) => {
|
||||
if (!hasSibling || !parsedPattern(path, basename)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const clausePattern = when.replace('$(basename)', () => name!);
|
||||
const matched = hasSibling(clausePattern);
|
||||
return isThenable(matched) ?
|
||||
matched.then(match => match ? pattern : null) :
|
||||
matched ? pattern : null;
|
||||
};
|
||||
|
||||
result.requiresSiblings = true;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Expression is anything
|
||||
return parsedPattern;
|
||||
}
|
||||
|
||||
function aggregateBasenameMatches(parsedPatterns: Array<ParsedStringPattern | ParsedExpressionPattern>, result?: string): Array<ParsedStringPattern | ParsedExpressionPattern> {
|
||||
const basenamePatterns = parsedPatterns.filter(parsedPattern => !!(<ParsedStringPattern>parsedPattern).basenames);
|
||||
if (basenamePatterns.length < 2) {
|
||||
return parsedPatterns;
|
||||
}
|
||||
|
||||
const basenames = basenamePatterns.reduce<string[]>((all, current) => {
|
||||
const basenames = (<ParsedStringPattern>current).basenames;
|
||||
|
||||
return basenames ? all.concat(basenames) : all;
|
||||
}, [] as string[]);
|
||||
|
||||
let patterns: string[];
|
||||
if (result) {
|
||||
patterns = [];
|
||||
|
||||
for (let i = 0, n = basenames.length; i < n; i++) {
|
||||
patterns.push(result);
|
||||
}
|
||||
} else {
|
||||
patterns = basenamePatterns.reduce((all, current) => {
|
||||
const patterns = (<ParsedStringPattern>current).patterns;
|
||||
|
||||
return patterns ? all.concat(patterns) : all;
|
||||
}, [] as string[]);
|
||||
}
|
||||
|
||||
const aggregate: ParsedStringPattern = function (path: string, basename?: string) {
|
||||
if (typeof path !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!basename) {
|
||||
let i: number;
|
||||
for (i = path.length; i > 0; i--) {
|
||||
const ch = path.charCodeAt(i - 1);
|
||||
if (ch === CharCode.Slash || ch === CharCode.Backslash) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
basename = path.substr(i);
|
||||
}
|
||||
|
||||
const index = basenames.indexOf(basename);
|
||||
return index !== -1 ? patterns[index] : null;
|
||||
};
|
||||
|
||||
aggregate.basenames = basenames;
|
||||
aggregate.patterns = patterns;
|
||||
aggregate.allBasenames = basenames;
|
||||
|
||||
const aggregatedPatterns = parsedPatterns.filter(parsedPattern => !(<ParsedStringPattern>parsedPattern).basenames);
|
||||
aggregatedPatterns.push(aggregate);
|
||||
|
||||
return aggregatedPatterns;
|
||||
}
|
||||
|
||||
export function patternsEquals(patternsA: Array<string | IRelativePattern> | undefined, patternsB: Array<string | IRelativePattern> | undefined): boolean {
|
||||
return equals(patternsA, patternsB, (a, b) => {
|
||||
if (typeof a === 'string' && typeof b === 'string') {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
if (typeof a !== 'string' && typeof b !== 'string') {
|
||||
return a.base === b.base && a.pattern === b.pattern;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export class HierarchicalKind {
|
||||
public static readonly sep = '.';
|
||||
|
||||
public static readonly None = new HierarchicalKind('@@none@@'); // Special kind that matches nothing
|
||||
public static readonly Empty = new HierarchicalKind('');
|
||||
|
||||
constructor(
|
||||
public readonly value: string
|
||||
) { }
|
||||
|
||||
public equals(other: HierarchicalKind): boolean {
|
||||
return this.value === other.value;
|
||||
}
|
||||
|
||||
public contains(other: HierarchicalKind): boolean {
|
||||
return this.equals(other) || this.value === '' || other.value.startsWith(this.value + HierarchicalKind.sep);
|
||||
}
|
||||
|
||||
public intersects(other: HierarchicalKind): boolean {
|
||||
return this.contains(other) || other.contains(this);
|
||||
}
|
||||
|
||||
public append(...parts: string[]): HierarchicalKind {
|
||||
return new HierarchicalKind((this.value ? [this.value, ...parts] : parts).join(HierarchicalKind.sep));
|
||||
}
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SetWithKey } from 'vs/base/common/collections';
|
||||
import { ArrayNavigator, INavigator } from 'vs/base/common/navigator';
|
||||
|
||||
export class HistoryNavigator<T> implements INavigator<T> {
|
||||
|
||||
private _history!: Set<T>;
|
||||
private _limit: number;
|
||||
private _navigator!: ArrayNavigator<T>;
|
||||
|
||||
constructor(history: readonly T[] = [], limit: number = 10) {
|
||||
this._initialize(history);
|
||||
this._limit = limit;
|
||||
this._onChange();
|
||||
}
|
||||
|
||||
public getHistory(): T[] {
|
||||
return this._elements;
|
||||
}
|
||||
|
||||
public add(t: T) {
|
||||
this._history.delete(t);
|
||||
this._history.add(t);
|
||||
this._onChange();
|
||||
}
|
||||
|
||||
public next(): T | null {
|
||||
// This will navigate past the end of the last element, and in that case the input should be cleared
|
||||
return this._navigator.next();
|
||||
}
|
||||
|
||||
public previous(): T | null {
|
||||
if (this._currentPosition() !== 0) {
|
||||
return this._navigator.previous();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public current(): T | null {
|
||||
return this._navigator.current();
|
||||
}
|
||||
|
||||
public first(): T | null {
|
||||
return this._navigator.first();
|
||||
}
|
||||
|
||||
public last(): T | null {
|
||||
return this._navigator.last();
|
||||
}
|
||||
|
||||
public isFirst(): boolean {
|
||||
return this._currentPosition() === 0;
|
||||
}
|
||||
|
||||
public isLast(): boolean {
|
||||
return this._currentPosition() >= this._elements.length - 1;
|
||||
}
|
||||
|
||||
public isNowhere(): boolean {
|
||||
return this._navigator.current() === null;
|
||||
}
|
||||
|
||||
public has(t: T): boolean {
|
||||
return this._history.has(t);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._initialize([]);
|
||||
this._onChange();
|
||||
}
|
||||
|
||||
private _onChange() {
|
||||
this._reduceToLimit();
|
||||
const elements = this._elements;
|
||||
this._navigator = new ArrayNavigator(elements, 0, elements.length, elements.length);
|
||||
}
|
||||
|
||||
private _reduceToLimit() {
|
||||
const data = this._elements;
|
||||
if (data.length > this._limit) {
|
||||
this._initialize(data.slice(data.length - this._limit));
|
||||
}
|
||||
}
|
||||
|
||||
private _currentPosition(): number {
|
||||
const currentElement = this._navigator.current();
|
||||
if (!currentElement) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return this._elements.indexOf(currentElement);
|
||||
}
|
||||
|
||||
private _initialize(history: readonly T[]): void {
|
||||
this._history = new Set();
|
||||
for (const entry of history) {
|
||||
this._history.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private get _elements(): T[] {
|
||||
const elements: T[] = [];
|
||||
this._history.forEach(e => elements.push(e));
|
||||
return elements;
|
||||
}
|
||||
}
|
||||
|
||||
interface HistoryNode<T> {
|
||||
value: T;
|
||||
previous: HistoryNode<T> | undefined;
|
||||
next: HistoryNode<T> | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The right way to use HistoryNavigator2 is for the last item in the list to be the user's uncommitted current text. eg empty string, or whatever has been typed. Then
|
||||
* the user can navigate away from the last item through the list, and back to it. When updating the last item, call replaceLast.
|
||||
*/
|
||||
export class HistoryNavigator2<T> {
|
||||
|
||||
private valueSet: Set<T>;
|
||||
private head: HistoryNode<T>;
|
||||
private tail: HistoryNode<T>;
|
||||
private cursor: HistoryNode<T>;
|
||||
private _size: number;
|
||||
get size(): number { return this._size; }
|
||||
|
||||
constructor(history: readonly T[], private capacity: number = 10, private identityFn: (t: T) => any = t => t) {
|
||||
if (history.length < 1) {
|
||||
throw new Error('not supported');
|
||||
}
|
||||
|
||||
this._size = 1;
|
||||
this.head = this.tail = this.cursor = {
|
||||
value: history[0],
|
||||
previous: undefined,
|
||||
next: undefined
|
||||
};
|
||||
|
||||
this.valueSet = new SetWithKey<T>([history[0]], identityFn);
|
||||
for (let i = 1; i < history.length; i++) {
|
||||
this.add(history[i]);
|
||||
}
|
||||
}
|
||||
|
||||
add(value: T): void {
|
||||
const node: HistoryNode<T> = {
|
||||
value,
|
||||
previous: this.tail,
|
||||
next: undefined
|
||||
};
|
||||
|
||||
this.tail.next = node;
|
||||
this.tail = node;
|
||||
this.cursor = this.tail;
|
||||
this._size++;
|
||||
|
||||
if (this.valueSet.has(value)) {
|
||||
this._deleteFromList(value);
|
||||
} else {
|
||||
this.valueSet.add(value);
|
||||
}
|
||||
|
||||
while (this._size > this.capacity) {
|
||||
this.valueSet.delete(this.head.value);
|
||||
|
||||
this.head = this.head.next!;
|
||||
this.head.previous = undefined;
|
||||
this._size--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns old last value
|
||||
*/
|
||||
replaceLast(value: T): T {
|
||||
if (this.identityFn(this.tail.value) === this.identityFn(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const oldValue = this.tail.value;
|
||||
this.valueSet.delete(oldValue);
|
||||
this.tail.value = value;
|
||||
|
||||
if (this.valueSet.has(value)) {
|
||||
this._deleteFromList(value);
|
||||
} else {
|
||||
this.valueSet.add(value);
|
||||
}
|
||||
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
prepend(value: T): void {
|
||||
if (this._size === this.capacity || this.valueSet.has(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const node: HistoryNode<T> = {
|
||||
value,
|
||||
previous: undefined,
|
||||
next: this.head
|
||||
};
|
||||
|
||||
this.head.previous = node;
|
||||
this.head = node;
|
||||
this._size++;
|
||||
|
||||
this.valueSet.add(value);
|
||||
}
|
||||
|
||||
isAtEnd(): boolean {
|
||||
return this.cursor === this.tail;
|
||||
}
|
||||
|
||||
current(): T {
|
||||
return this.cursor.value;
|
||||
}
|
||||
|
||||
previous(): T {
|
||||
if (this.cursor.previous) {
|
||||
this.cursor = this.cursor.previous;
|
||||
}
|
||||
|
||||
return this.cursor.value;
|
||||
}
|
||||
|
||||
next(): T {
|
||||
if (this.cursor.next) {
|
||||
this.cursor = this.cursor.next;
|
||||
}
|
||||
|
||||
return this.cursor.value;
|
||||
}
|
||||
|
||||
has(t: T): boolean {
|
||||
return this.valueSet.has(t);
|
||||
}
|
||||
|
||||
resetCursor(): T {
|
||||
this.cursor = this.tail;
|
||||
return this.cursor.value;
|
||||
}
|
||||
|
||||
*[Symbol.iterator](): Iterator<T> {
|
||||
let node: HistoryNode<T> | undefined = this.head;
|
||||
|
||||
while (node) {
|
||||
yield node.value;
|
||||
node = node.next;
|
||||
}
|
||||
}
|
||||
|
||||
private _deleteFromList(value: T): void {
|
||||
let temp = this.head;
|
||||
|
||||
const valueKey = this.identityFn(value);
|
||||
while (temp !== this.tail) {
|
||||
if (this.identityFn(temp.value) === valueKey) {
|
||||
if (temp === this.head) {
|
||||
this.head = this.head.next!;
|
||||
this.head.previous = undefined;
|
||||
} else {
|
||||
temp.previous!.next = temp.next;
|
||||
temp.next!.previous = temp.previous;
|
||||
}
|
||||
|
||||
this._size--;
|
||||
}
|
||||
|
||||
temp = temp.next!;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { env } from 'vs/base/common/process';
|
||||
|
||||
export function isHotReloadEnabled(): boolean {
|
||||
return env && !!env['VSCODE_DEV'];
|
||||
}
|
||||
export function registerHotReloadHandler(handler: HotReloadHandler): IDisposable {
|
||||
if (!isHotReloadEnabled()) {
|
||||
return { dispose() { } };
|
||||
} else {
|
||||
const handlers = registerGlobalHotReloadHandler();
|
||||
handlers.add(handler);
|
||||
return {
|
||||
dispose() { handlers.delete(handler); }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the old exports of the module to reload and returns a function to apply the new exports.
|
||||
* If `undefined` is returned, this handler is not able to handle the module.
|
||||
*
|
||||
* If no handler can apply the new exports, the module will not be reloaded.
|
||||
*/
|
||||
export type HotReloadHandler = (args: { oldExports: Record<string, unknown>; newSrc: string; config: IHotReloadConfig }) => AcceptNewExportsHandler | undefined;
|
||||
export type AcceptNewExportsHandler = (newExports: Record<string, unknown>) => boolean;
|
||||
export type IHotReloadConfig = HotReloadConfig;
|
||||
|
||||
function registerGlobalHotReloadHandler() {
|
||||
if (!hotReloadHandlers) {
|
||||
hotReloadHandlers = new Set();
|
||||
}
|
||||
|
||||
const g = globalThis as unknown as GlobalThisAddition;
|
||||
if (!g.$hotReload_applyNewExports) {
|
||||
g.$hotReload_applyNewExports = args => {
|
||||
const args2 = { config: { mode: undefined }, ...args };
|
||||
|
||||
const results: AcceptNewExportsHandler[] = [];
|
||||
for (const h of hotReloadHandlers!) {
|
||||
const result = h(args2);
|
||||
if (result) {
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
if (results.length > 0) {
|
||||
return newExports => {
|
||||
let result = false;
|
||||
for (const r of results) {
|
||||
if (r(newExports)) {
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
return hotReloadHandlers;
|
||||
}
|
||||
|
||||
let hotReloadHandlers: Set<(args: { oldExports: Record<string, unknown>; newSrc: string; config: HotReloadConfig }) => AcceptNewExportsFn | undefined> | undefined = undefined;
|
||||
|
||||
interface HotReloadConfig {
|
||||
mode?: 'patch-prototype' | undefined;
|
||||
}
|
||||
|
||||
interface GlobalThisAddition {
|
||||
$hotReload_applyNewExports?(args: { oldExports: Record<string, unknown>; newSrc: string; config?: HotReloadConfig }): AcceptNewExportsFn | undefined;
|
||||
}
|
||||
|
||||
type AcceptNewExportsFn = (newExports: Record<string, unknown>) => boolean;
|
||||
|
||||
if (isHotReloadEnabled()) {
|
||||
// This code does not run in production.
|
||||
registerHotReloadHandler(({ oldExports, newSrc, config }) => {
|
||||
if (config.mode !== 'patch-prototype') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return newExports => {
|
||||
for (const key in newExports) {
|
||||
const exportedItem = newExports[key];
|
||||
console.log(`[hot-reload] Patching prototype methods of '${key}'`, { exportedItem });
|
||||
if (typeof exportedItem === 'function' && exportedItem.prototype) {
|
||||
const oldExportedItem = oldExports[key];
|
||||
if (oldExportedItem) {
|
||||
for (const prop of Object.getOwnPropertyNames(exportedItem.prototype)) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(exportedItem.prototype, prop)!;
|
||||
const oldDescriptor = Object.getOwnPropertyDescriptor((oldExportedItem as any).prototype, prop);
|
||||
|
||||
if (descriptor?.value?.toString() !== oldDescriptor?.value?.toString()) {
|
||||
console.log(`[hot-reload] Patching prototype method '${key}.${prop}'`);
|
||||
}
|
||||
|
||||
Object.defineProperty((oldExportedItem as any).prototype, prop, descriptor);
|
||||
}
|
||||
newExports[key] = oldExportedItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isHotReloadEnabled, registerHotReloadHandler } from 'vs/base/common/hotReload';
|
||||
import { IReader, observableSignalFromEvent } from 'vs/base/common/observable';
|
||||
|
||||
export function readHotReloadableExport<T>(value: T, reader: IReader | undefined): T {
|
||||
observeHotReloadableExports([value], reader);
|
||||
return value;
|
||||
}
|
||||
|
||||
export function observeHotReloadableExports(values: any[], reader: IReader | undefined): void {
|
||||
if (isHotReloadEnabled()) {
|
||||
const o = observableSignalFromEvent(
|
||||
'reload',
|
||||
event => registerHotReloadHandler(({ oldExports }) => {
|
||||
if (![...Object.values(oldExports)].some(v => values.includes(v))) {
|
||||
return undefined;
|
||||
}
|
||||
return (_newExports) => {
|
||||
event(undefined);
|
||||
return true;
|
||||
};
|
||||
})
|
||||
);
|
||||
o.read(reader);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export class IdGenerator {
|
||||
|
||||
private _prefix: string;
|
||||
private _lastId: number;
|
||||
|
||||
constructor(prefix: string) {
|
||||
this._prefix = prefix;
|
||||
this._lastId = 0;
|
||||
}
|
||||
|
||||
public nextId(): string {
|
||||
return this._prefix + (++this._lastId);
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultGenerator = new IdGenerator('id#');
|
||||
@@ -1,36 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
|
||||
export class IMEImpl {
|
||||
|
||||
private readonly _onDidChange = new Emitter<void>();
|
||||
public readonly onDidChange = this._onDidChange.event;
|
||||
|
||||
private _enabled = true;
|
||||
|
||||
public get enabled() {
|
||||
return this._enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable IME
|
||||
*/
|
||||
public enable(): void {
|
||||
this._enabled = true;
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable IME
|
||||
*/
|
||||
public disable(): void {
|
||||
this._enabled = false;
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
}
|
||||
|
||||
export const IME = new IMEImpl();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,176 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { findNodeAtLocation, JSONPath, Node, ParseError, parseTree, Segment } from './json';
|
||||
import { Edit, format, FormattingOptions, isEOL } from './jsonFormatter';
|
||||
|
||||
|
||||
export function removeProperty(text: string, path: JSONPath, formattingOptions: FormattingOptions): Edit[] {
|
||||
return setProperty(text, path, undefined, formattingOptions);
|
||||
}
|
||||
|
||||
export function setProperty(text: string, originalPath: JSONPath, value: any, formattingOptions: FormattingOptions, getInsertionIndex?: (properties: string[]) => number): Edit[] {
|
||||
const path = originalPath.slice();
|
||||
const errors: ParseError[] = [];
|
||||
const root = parseTree(text, errors);
|
||||
let parent: Node | undefined = undefined;
|
||||
|
||||
let lastSegment: Segment | undefined = undefined;
|
||||
while (path.length > 0) {
|
||||
lastSegment = path.pop();
|
||||
parent = findNodeAtLocation(root, path);
|
||||
if (parent === undefined && value !== undefined) {
|
||||
if (typeof lastSegment === 'string') {
|
||||
value = { [lastSegment]: value };
|
||||
} else {
|
||||
value = [value];
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!parent) {
|
||||
// empty document
|
||||
if (value === undefined) { // delete
|
||||
return []; // property does not exist, nothing to do
|
||||
}
|
||||
return withFormatting(text, { offset: root ? root.offset : 0, length: root ? root.length : 0, content: JSON.stringify(value) }, formattingOptions);
|
||||
} else if (parent.type === 'object' && typeof lastSegment === 'string' && Array.isArray(parent.children)) {
|
||||
const existing = findNodeAtLocation(parent, [lastSegment]);
|
||||
if (existing !== undefined) {
|
||||
if (value === undefined) { // delete
|
||||
if (!existing.parent) {
|
||||
throw new Error('Malformed AST');
|
||||
}
|
||||
const propertyIndex = parent.children.indexOf(existing.parent);
|
||||
let removeBegin: number;
|
||||
let removeEnd = existing.parent.offset + existing.parent.length;
|
||||
if (propertyIndex > 0) {
|
||||
// remove the comma of the previous node
|
||||
const previous = parent.children[propertyIndex - 1];
|
||||
removeBegin = previous.offset + previous.length;
|
||||
} else {
|
||||
removeBegin = parent.offset + 1;
|
||||
if (parent.children.length > 1) {
|
||||
// remove the comma of the next node
|
||||
const next = parent.children[1];
|
||||
removeEnd = next.offset;
|
||||
}
|
||||
}
|
||||
return withFormatting(text, { offset: removeBegin, length: removeEnd - removeBegin, content: '' }, formattingOptions);
|
||||
} else {
|
||||
// set value of existing property
|
||||
return withFormatting(text, { offset: existing.offset, length: existing.length, content: JSON.stringify(value) }, formattingOptions);
|
||||
}
|
||||
} else {
|
||||
if (value === undefined) { // delete
|
||||
return []; // property does not exist, nothing to do
|
||||
}
|
||||
const newProperty = `${JSON.stringify(lastSegment)}: ${JSON.stringify(value)}`;
|
||||
const index = getInsertionIndex ? getInsertionIndex(parent.children.map(p => p.children![0].value)) : parent.children.length;
|
||||
let edit: Edit;
|
||||
if (index > 0) {
|
||||
const previous = parent.children[index - 1];
|
||||
edit = { offset: previous.offset + previous.length, length: 0, content: ',' + newProperty };
|
||||
} else if (parent.children.length === 0) {
|
||||
edit = { offset: parent.offset + 1, length: 0, content: newProperty };
|
||||
} else {
|
||||
edit = { offset: parent.offset + 1, length: 0, content: newProperty + ',' };
|
||||
}
|
||||
return withFormatting(text, edit, formattingOptions);
|
||||
}
|
||||
} else if (parent.type === 'array' && typeof lastSegment === 'number' && Array.isArray(parent.children)) {
|
||||
if (value !== undefined) {
|
||||
// Insert
|
||||
const newProperty = `${JSON.stringify(value)}`;
|
||||
let edit: Edit;
|
||||
if (parent.children.length === 0 || lastSegment === 0) {
|
||||
edit = { offset: parent.offset + 1, length: 0, content: parent.children.length === 0 ? newProperty : newProperty + ',' };
|
||||
} else {
|
||||
const index = lastSegment === -1 || lastSegment > parent.children.length ? parent.children.length : lastSegment;
|
||||
const previous = parent.children[index - 1];
|
||||
edit = { offset: previous.offset + previous.length, length: 0, content: ',' + newProperty };
|
||||
}
|
||||
return withFormatting(text, edit, formattingOptions);
|
||||
} else {
|
||||
//Removal
|
||||
const removalIndex = lastSegment;
|
||||
const toRemove = parent.children[removalIndex];
|
||||
let edit: Edit;
|
||||
if (parent.children.length === 1) {
|
||||
// only item
|
||||
edit = { offset: parent.offset + 1, length: parent.length - 2, content: '' };
|
||||
} else if (parent.children.length - 1 === removalIndex) {
|
||||
// last item
|
||||
const previous = parent.children[removalIndex - 1];
|
||||
const offset = previous.offset + previous.length;
|
||||
const parentEndOffset = parent.offset + parent.length;
|
||||
edit = { offset, length: parentEndOffset - 2 - offset, content: '' };
|
||||
} else {
|
||||
edit = { offset: toRemove.offset, length: parent.children[removalIndex + 1].offset - toRemove.offset, content: '' };
|
||||
}
|
||||
return withFormatting(text, edit, formattingOptions);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Can not add ${typeof lastSegment !== 'number' ? 'index' : 'property'} to parent of type ${parent.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function withFormatting(text: string, edit: Edit, formattingOptions: FormattingOptions): Edit[] {
|
||||
// apply the edit
|
||||
let newText = applyEdit(text, edit);
|
||||
|
||||
// format the new text
|
||||
let begin = edit.offset;
|
||||
let end = edit.offset + edit.content.length;
|
||||
if (edit.length === 0 || edit.content.length === 0) { // insert or remove
|
||||
while (begin > 0 && !isEOL(newText, begin - 1)) {
|
||||
begin--;
|
||||
}
|
||||
while (end < newText.length && !isEOL(newText, end)) {
|
||||
end++;
|
||||
}
|
||||
}
|
||||
|
||||
const edits = format(newText, { offset: begin, length: end - begin }, formattingOptions);
|
||||
|
||||
// apply the formatting edits and track the begin and end offsets of the changes
|
||||
for (let i = edits.length - 1; i >= 0; i--) {
|
||||
const curr = edits[i];
|
||||
newText = applyEdit(newText, curr);
|
||||
begin = Math.min(begin, curr.offset);
|
||||
end = Math.max(end, curr.offset + curr.length);
|
||||
end += curr.content.length - curr.length;
|
||||
}
|
||||
// create a single edit with all changes
|
||||
const editLength = text.length - (newText.length - end) - begin;
|
||||
return [{ offset: begin, length: editLength, content: newText.substring(begin, end) }];
|
||||
}
|
||||
|
||||
export function applyEdit(text: string, edit: Edit): string {
|
||||
return text.substring(0, edit.offset) + edit.content + text.substring(edit.offset + edit.length);
|
||||
}
|
||||
|
||||
export function applyEdits(text: string, edits: Edit[]): string {
|
||||
const sortedEdits = edits.slice(0).sort((a, b) => {
|
||||
const diff = a.offset - b.offset;
|
||||
if (diff === 0) {
|
||||
return a.length - b.length;
|
||||
}
|
||||
return diff;
|
||||
});
|
||||
let lastModifiedOffset = text.length;
|
||||
for (let i = sortedEdits.length - 1; i >= 0; i--) {
|
||||
const e = sortedEdits[i];
|
||||
if (e.offset + e.length <= lastModifiedOffset) {
|
||||
text = applyEdit(text, e);
|
||||
} else {
|
||||
throw new Error('Overlapping edit');
|
||||
}
|
||||
lastModifiedOffset = e.offset;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Extracted from json.ts to keep json nls free.
|
||||
*/
|
||||
import { localize } from 'vs/nls';
|
||||
import { ParseErrorCode } from './json';
|
||||
|
||||
export function getParseErrorMessage(errorCode: ParseErrorCode): string {
|
||||
switch (errorCode) {
|
||||
case ParseErrorCode.InvalidSymbol: return localize('error.invalidSymbol', 'Invalid symbol');
|
||||
case ParseErrorCode.InvalidNumberFormat: return localize('error.invalidNumberFormat', 'Invalid number format');
|
||||
case ParseErrorCode.PropertyNameExpected: return localize('error.propertyNameExpected', 'Property name expected');
|
||||
case ParseErrorCode.ValueExpected: return localize('error.valueExpected', 'Value expected');
|
||||
case ParseErrorCode.ColonExpected: return localize('error.colonExpected', 'Colon expected');
|
||||
case ParseErrorCode.CommaExpected: return localize('error.commaExpected', 'Comma expected');
|
||||
case ParseErrorCode.CloseBraceExpected: return localize('error.closeBraceExpected', 'Closing brace expected');
|
||||
case ParseErrorCode.CloseBracketExpected: return localize('error.closeBracketExpected', 'Closing bracket expected');
|
||||
case ParseErrorCode.EndOfFileExpected: return localize('error.endOfFileExpected', 'End of file expected');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createScanner, ScanError, SyntaxKind } from './json';
|
||||
|
||||
export interface FormattingOptions {
|
||||
/**
|
||||
* If indentation is based on spaces (`insertSpaces` = true), then what is the number of spaces that make an indent?
|
||||
*/
|
||||
tabSize?: number;
|
||||
/**
|
||||
* Is indentation based on spaces?
|
||||
*/
|
||||
insertSpaces?: boolean;
|
||||
/**
|
||||
* The default 'end of line' character. If not set, '\n' is used as default.
|
||||
*/
|
||||
eol?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a text modification
|
||||
*/
|
||||
export interface Edit {
|
||||
/**
|
||||
* The start offset of the modification.
|
||||
*/
|
||||
offset: number;
|
||||
/**
|
||||
* The length of the modification. Must not be negative. Empty length represents an *insert*.
|
||||
*/
|
||||
length: number;
|
||||
/**
|
||||
* The new content. Empty content represents a *remove*.
|
||||
*/
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A text range in the document
|
||||
*/
|
||||
export interface Range {
|
||||
/**
|
||||
* The start offset of the range.
|
||||
*/
|
||||
offset: number;
|
||||
/**
|
||||
* The length of the range. Must not be negative.
|
||||
*/
|
||||
length: number;
|
||||
}
|
||||
|
||||
|
||||
export function format(documentText: string, range: Range | undefined, options: FormattingOptions): Edit[] {
|
||||
let initialIndentLevel: number;
|
||||
let formatText: string;
|
||||
let formatTextStart: number;
|
||||
let rangeStart: number;
|
||||
let rangeEnd: number;
|
||||
if (range) {
|
||||
rangeStart = range.offset;
|
||||
rangeEnd = rangeStart + range.length;
|
||||
|
||||
formatTextStart = rangeStart;
|
||||
while (formatTextStart > 0 && !isEOL(documentText, formatTextStart - 1)) {
|
||||
formatTextStart--;
|
||||
}
|
||||
let endOffset = rangeEnd;
|
||||
while (endOffset < documentText.length && !isEOL(documentText, endOffset)) {
|
||||
endOffset++;
|
||||
}
|
||||
formatText = documentText.substring(formatTextStart, endOffset);
|
||||
initialIndentLevel = computeIndentLevel(formatText, options);
|
||||
} else {
|
||||
formatText = documentText;
|
||||
initialIndentLevel = 0;
|
||||
formatTextStart = 0;
|
||||
rangeStart = 0;
|
||||
rangeEnd = documentText.length;
|
||||
}
|
||||
const eol = getEOL(options, documentText);
|
||||
|
||||
let lineBreak = false;
|
||||
let indentLevel = 0;
|
||||
let indentValue: string;
|
||||
if (options.insertSpaces) {
|
||||
indentValue = repeat(' ', options.tabSize || 4);
|
||||
} else {
|
||||
indentValue = '\t';
|
||||
}
|
||||
|
||||
const scanner = createScanner(formatText, false);
|
||||
let hasError = false;
|
||||
|
||||
function newLineAndIndent(): string {
|
||||
return eol + repeat(indentValue, initialIndentLevel + indentLevel);
|
||||
}
|
||||
function scanNext(): SyntaxKind {
|
||||
let token = scanner.scan();
|
||||
lineBreak = false;
|
||||
while (token === SyntaxKind.Trivia || token === SyntaxKind.LineBreakTrivia) {
|
||||
lineBreak = lineBreak || (token === SyntaxKind.LineBreakTrivia);
|
||||
token = scanner.scan();
|
||||
}
|
||||
hasError = token === SyntaxKind.Unknown || scanner.getTokenError() !== ScanError.None;
|
||||
return token;
|
||||
}
|
||||
const editOperations: Edit[] = [];
|
||||
function addEdit(text: string, startOffset: number, endOffset: number) {
|
||||
if (!hasError && startOffset < rangeEnd && endOffset > rangeStart && documentText.substring(startOffset, endOffset) !== text) {
|
||||
editOperations.push({ offset: startOffset, length: endOffset - startOffset, content: text });
|
||||
}
|
||||
}
|
||||
|
||||
let firstToken = scanNext();
|
||||
|
||||
if (firstToken !== SyntaxKind.EOF) {
|
||||
const firstTokenStart = scanner.getTokenOffset() + formatTextStart;
|
||||
const initialIndent = repeat(indentValue, initialIndentLevel);
|
||||
addEdit(initialIndent, formatTextStart, firstTokenStart);
|
||||
}
|
||||
|
||||
while (firstToken !== SyntaxKind.EOF) {
|
||||
let firstTokenEnd = scanner.getTokenOffset() + scanner.getTokenLength() + formatTextStart;
|
||||
let secondToken = scanNext();
|
||||
|
||||
let replaceContent = '';
|
||||
while (!lineBreak && (secondToken === SyntaxKind.LineCommentTrivia || secondToken === SyntaxKind.BlockCommentTrivia)) {
|
||||
// comments on the same line: keep them on the same line, but ignore them otherwise
|
||||
const commentTokenStart = scanner.getTokenOffset() + formatTextStart;
|
||||
addEdit(' ', firstTokenEnd, commentTokenStart);
|
||||
firstTokenEnd = scanner.getTokenOffset() + scanner.getTokenLength() + formatTextStart;
|
||||
replaceContent = secondToken === SyntaxKind.LineCommentTrivia ? newLineAndIndent() : '';
|
||||
secondToken = scanNext();
|
||||
}
|
||||
|
||||
if (secondToken === SyntaxKind.CloseBraceToken) {
|
||||
if (firstToken !== SyntaxKind.OpenBraceToken) {
|
||||
indentLevel--;
|
||||
replaceContent = newLineAndIndent();
|
||||
}
|
||||
} else if (secondToken === SyntaxKind.CloseBracketToken) {
|
||||
if (firstToken !== SyntaxKind.OpenBracketToken) {
|
||||
indentLevel--;
|
||||
replaceContent = newLineAndIndent();
|
||||
}
|
||||
} else {
|
||||
switch (firstToken) {
|
||||
case SyntaxKind.OpenBracketToken:
|
||||
case SyntaxKind.OpenBraceToken:
|
||||
indentLevel++;
|
||||
replaceContent = newLineAndIndent();
|
||||
break;
|
||||
case SyntaxKind.CommaToken:
|
||||
case SyntaxKind.LineCommentTrivia:
|
||||
replaceContent = newLineAndIndent();
|
||||
break;
|
||||
case SyntaxKind.BlockCommentTrivia:
|
||||
if (lineBreak) {
|
||||
replaceContent = newLineAndIndent();
|
||||
} else {
|
||||
// symbol following comment on the same line: keep on same line, separate with ' '
|
||||
replaceContent = ' ';
|
||||
}
|
||||
break;
|
||||
case SyntaxKind.ColonToken:
|
||||
replaceContent = ' ';
|
||||
break;
|
||||
case SyntaxKind.StringLiteral:
|
||||
if (secondToken === SyntaxKind.ColonToken) {
|
||||
replaceContent = '';
|
||||
break;
|
||||
}
|
||||
// fall through
|
||||
case SyntaxKind.NullKeyword:
|
||||
case SyntaxKind.TrueKeyword:
|
||||
case SyntaxKind.FalseKeyword:
|
||||
case SyntaxKind.NumericLiteral:
|
||||
case SyntaxKind.CloseBraceToken:
|
||||
case SyntaxKind.CloseBracketToken:
|
||||
if (secondToken === SyntaxKind.LineCommentTrivia || secondToken === SyntaxKind.BlockCommentTrivia) {
|
||||
replaceContent = ' ';
|
||||
} else if (secondToken !== SyntaxKind.CommaToken && secondToken !== SyntaxKind.EOF) {
|
||||
hasError = true;
|
||||
}
|
||||
break;
|
||||
case SyntaxKind.Unknown:
|
||||
hasError = true;
|
||||
break;
|
||||
}
|
||||
if (lineBreak && (secondToken === SyntaxKind.LineCommentTrivia || secondToken === SyntaxKind.BlockCommentTrivia)) {
|
||||
replaceContent = newLineAndIndent();
|
||||
}
|
||||
|
||||
}
|
||||
const secondTokenStart = scanner.getTokenOffset() + formatTextStart;
|
||||
addEdit(replaceContent, firstTokenEnd, secondTokenStart);
|
||||
firstToken = secondToken;
|
||||
}
|
||||
return editOperations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string out of the object passed as argument, using the given formatting options
|
||||
* @param any The object to stringify and format
|
||||
* @param options The formatting options to use
|
||||
*/
|
||||
export function toFormattedString(obj: any, options: FormattingOptions) {
|
||||
const content = JSON.stringify(obj, undefined, options.insertSpaces ? options.tabSize || 4 : '\t');
|
||||
if (options.eol !== undefined) {
|
||||
return content.replace(/\r\n|\r|\n/g, options.eol);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
function repeat(s: string, count: number): string {
|
||||
let result = '';
|
||||
for (let i = 0; i < count; i++) {
|
||||
result += s;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function computeIndentLevel(content: string, options: FormattingOptions): number {
|
||||
let i = 0;
|
||||
let nChars = 0;
|
||||
const tabSize = options.tabSize || 4;
|
||||
while (i < content.length) {
|
||||
const ch = content.charAt(i);
|
||||
if (ch === ' ') {
|
||||
nChars++;
|
||||
} else if (ch === '\t') {
|
||||
nChars += tabSize;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return Math.floor(nChars / tabSize);
|
||||
}
|
||||
|
||||
export function getEOL(options: FormattingOptions, text: string): string {
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text.charAt(i);
|
||||
if (ch === '\r') {
|
||||
if (i + 1 < text.length && text.charAt(i + 1) === '\n') {
|
||||
return '\r\n';
|
||||
}
|
||||
return '\r';
|
||||
} else if (ch === '\n') {
|
||||
return '\n';
|
||||
}
|
||||
}
|
||||
return (options && options.eol) || '\n';
|
||||
}
|
||||
|
||||
export function isEOL(text: string, offset: number) {
|
||||
return '\r\n'.indexOf(text.charAt(offset)) !== -1;
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export type JSONSchemaType = 'string' | 'number' | 'integer' | 'boolean' | 'null' | 'array' | 'object';
|
||||
|
||||
export interface IJSONSchema {
|
||||
id?: string;
|
||||
$id?: string;
|
||||
$schema?: string;
|
||||
type?: JSONSchemaType | JSONSchemaType[];
|
||||
title?: string;
|
||||
default?: any;
|
||||
definitions?: IJSONSchemaMap;
|
||||
description?: string;
|
||||
properties?: IJSONSchemaMap;
|
||||
patternProperties?: IJSONSchemaMap;
|
||||
additionalProperties?: boolean | IJSONSchema;
|
||||
minProperties?: number;
|
||||
maxProperties?: number;
|
||||
dependencies?: IJSONSchemaMap | { [prop: string]: string[] };
|
||||
items?: IJSONSchema | IJSONSchema[];
|
||||
minItems?: number;
|
||||
maxItems?: number;
|
||||
uniqueItems?: boolean;
|
||||
additionalItems?: boolean | IJSONSchema;
|
||||
pattern?: string;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
exclusiveMinimum?: boolean | number;
|
||||
exclusiveMaximum?: boolean | number;
|
||||
multipleOf?: number;
|
||||
required?: string[];
|
||||
$ref?: string;
|
||||
anyOf?: IJSONSchema[];
|
||||
allOf?: IJSONSchema[];
|
||||
oneOf?: IJSONSchema[];
|
||||
not?: IJSONSchema;
|
||||
enum?: any[];
|
||||
format?: string;
|
||||
|
||||
// schema draft 06
|
||||
const?: any;
|
||||
contains?: IJSONSchema;
|
||||
propertyNames?: IJSONSchema;
|
||||
examples?: any[];
|
||||
|
||||
// schema draft 07
|
||||
$comment?: string;
|
||||
if?: IJSONSchema;
|
||||
then?: IJSONSchema;
|
||||
else?: IJSONSchema;
|
||||
|
||||
// schema 2019-09
|
||||
unevaluatedProperties?: boolean | IJSONSchema;
|
||||
unevaluatedItems?: boolean | IJSONSchema;
|
||||
minContains?: number;
|
||||
maxContains?: number;
|
||||
deprecated?: boolean;
|
||||
dependentRequired?: { [prop: string]: string[] };
|
||||
dependentSchemas?: IJSONSchemaMap;
|
||||
$defs?: { [name: string]: IJSONSchema };
|
||||
$anchor?: string;
|
||||
$recursiveRef?: string;
|
||||
$recursiveAnchor?: string;
|
||||
$vocabulary?: any;
|
||||
|
||||
// schema 2020-12
|
||||
prefixItems?: IJSONSchema[];
|
||||
$dynamicRef?: string;
|
||||
$dynamicAnchor?: string;
|
||||
|
||||
// VSCode extensions
|
||||
|
||||
defaultSnippets?: IJSONSchemaSnippet[];
|
||||
errorMessage?: string;
|
||||
patternErrorMessage?: string;
|
||||
deprecationMessage?: string;
|
||||
markdownDeprecationMessage?: string;
|
||||
enumDescriptions?: string[];
|
||||
markdownEnumDescriptions?: string[];
|
||||
markdownDescription?: string;
|
||||
doNotSuggest?: boolean;
|
||||
suggestSortText?: string;
|
||||
allowComments?: boolean;
|
||||
allowTrailingCommas?: boolean;
|
||||
}
|
||||
|
||||
export interface IJSONSchemaMap {
|
||||
[name: string]: IJSONSchema;
|
||||
}
|
||||
|
||||
export interface IJSONSchemaSnippet {
|
||||
label?: string;
|
||||
description?: string;
|
||||
body?: any; // a object that will be JSON stringified
|
||||
bodyText?: string; // an already stringified JSON object that can contain new lines (\n) and tabs (\t)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a basic JSON schema to a TypeScript type.
|
||||
*
|
||||
* TODO: only supports basic schemas. Doesn't support all JSON schema features.
|
||||
*/
|
||||
export type SchemaToType<T> = T extends { type: 'string' }
|
||||
? string
|
||||
: T extends { type: 'number' }
|
||||
? number
|
||||
: T extends { type: 'boolean' }
|
||||
? boolean
|
||||
: T extends { type: 'null' }
|
||||
? null
|
||||
: T extends { type: 'object'; properties: infer P }
|
||||
? { [K in keyof P]: SchemaToType<P[K]> }
|
||||
: T extends { type: 'array'; items: infer I }
|
||||
? Array<SchemaToType<I>>
|
||||
: never;
|
||||
|
||||
interface Equals { schemas: IJSONSchema[]; id?: string }
|
||||
|
||||
export function getCompressedContent(schema: IJSONSchema): string {
|
||||
let hasDups = false;
|
||||
|
||||
|
||||
// visit all schema nodes and collect the ones that are equal
|
||||
const equalsByString = new Map<string, Equals>();
|
||||
const nodeToEquals = new Map<IJSONSchema, Equals>();
|
||||
const visitSchemas = (next: IJSONSchema) => {
|
||||
if (schema === next) {
|
||||
return true;
|
||||
}
|
||||
const val = JSON.stringify(next);
|
||||
if (val.length < 30) {
|
||||
// the $ref takes around 25 chars, so we don't save anything
|
||||
return true;
|
||||
}
|
||||
const eq = equalsByString.get(val);
|
||||
if (!eq) {
|
||||
const newEq = { schemas: [next] };
|
||||
equalsByString.set(val, newEq);
|
||||
nodeToEquals.set(next, newEq);
|
||||
return true;
|
||||
}
|
||||
eq.schemas.push(next);
|
||||
nodeToEquals.set(next, eq);
|
||||
hasDups = true;
|
||||
return false;
|
||||
};
|
||||
traverseNodes(schema, visitSchemas);
|
||||
equalsByString.clear();
|
||||
|
||||
if (!hasDups) {
|
||||
return JSON.stringify(schema);
|
||||
}
|
||||
|
||||
let defNodeName = '$defs';
|
||||
while (schema.hasOwnProperty(defNodeName)) {
|
||||
defNodeName += '_';
|
||||
}
|
||||
|
||||
// used to collect all schemas that are later put in `$defs`. The index in the array is the id of the schema.
|
||||
const definitions: IJSONSchema[] = [];
|
||||
|
||||
function stringify(root: IJSONSchema): string {
|
||||
return JSON.stringify(root, (_key: string, value: any) => {
|
||||
if (value !== root) {
|
||||
const eq = nodeToEquals.get(value);
|
||||
if (eq && eq.schemas.length > 1) {
|
||||
if (!eq.id) {
|
||||
eq.id = `_${definitions.length}`;
|
||||
definitions.push(eq.schemas[0]);
|
||||
}
|
||||
return { $ref: `#/${defNodeName}/${eq.id}` };
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
// stringify the schema and replace duplicate subtrees with $ref
|
||||
// this will add new items to the definitions array
|
||||
const str = stringify(schema);
|
||||
|
||||
// now stringify the definitions. Each invication of stringify cann add new items to the definitions array, so the length can grow while we iterate
|
||||
const defStrings: string[] = [];
|
||||
for (let i = 0; i < definitions.length; i++) {
|
||||
defStrings.push(`"_${i}":${stringify(definitions[i])}`);
|
||||
}
|
||||
if (defStrings.length) {
|
||||
return `${str.substring(0, str.length - 1)},"${defNodeName}":{${defStrings.join(',')}}}`;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
type IJSONSchemaRef = IJSONSchema | boolean;
|
||||
|
||||
function isObject(thing: any): thing is object {
|
||||
return typeof thing === 'object' && thing !== null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Traverse a JSON schema and visit each schema node
|
||||
*/
|
||||
function traverseNodes(root: IJSONSchema, visit: (schema: IJSONSchema) => boolean) {
|
||||
if (!root || typeof root !== 'object') {
|
||||
return;
|
||||
}
|
||||
const collectEntries = (...entries: (IJSONSchemaRef | undefined)[]) => {
|
||||
for (const entry of entries) {
|
||||
if (isObject(entry)) {
|
||||
toWalk.push(entry);
|
||||
}
|
||||
}
|
||||
};
|
||||
const collectMapEntries = (...maps: (IJSONSchemaMap | undefined)[]) => {
|
||||
for (const map of maps) {
|
||||
if (isObject(map)) {
|
||||
for (const key in map) {
|
||||
const entry = map[key];
|
||||
if (isObject(entry)) {
|
||||
toWalk.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const collectArrayEntries = (...arrays: (IJSONSchemaRef[] | undefined)[]) => {
|
||||
for (const array of arrays) {
|
||||
if (Array.isArray(array)) {
|
||||
for (const entry of array) {
|
||||
if (isObject(entry)) {
|
||||
toWalk.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const collectEntryOrArrayEntries = (items: (IJSONSchemaRef[] | IJSONSchemaRef | undefined)) => {
|
||||
if (Array.isArray(items)) {
|
||||
for (const entry of items) {
|
||||
if (isObject(entry)) {
|
||||
toWalk.push(entry);
|
||||
}
|
||||
}
|
||||
} else if (isObject(items)) {
|
||||
toWalk.push(items);
|
||||
}
|
||||
};
|
||||
|
||||
const toWalk: IJSONSchema[] = [root];
|
||||
|
||||
let next = toWalk.pop();
|
||||
while (next) {
|
||||
const visitChildern = visit(next);
|
||||
if (visitChildern) {
|
||||
collectEntries(next.additionalItems, next.additionalProperties, next.not, next.contains, next.propertyNames, next.if, next.then, next.else, next.unevaluatedItems, next.unevaluatedProperties);
|
||||
collectMapEntries(next.definitions, next.$defs, next.properties, next.patternProperties, <IJSONSchemaMap>next.dependencies, next.dependentSchemas);
|
||||
collectArrayEntries(next.anyOf, next.allOf, next.oneOf, next.prefixItems);
|
||||
collectEntryOrArrayEntries(next.items);
|
||||
}
|
||||
next = toWalk.pop();
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
-24
@@ -1,24 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* A drop-in replacement for JSON.parse that can parse
|
||||
* JSON with comments and trailing commas.
|
||||
*
|
||||
* @param content the content to strip comments from
|
||||
* @returns the parsed content as JSON
|
||||
*/
|
||||
export function parse(content: string): any;
|
||||
|
||||
/**
|
||||
* Strips single and multi line JavaScript comments from JSON
|
||||
* content. Ignores characters in strings BUT doesn't support
|
||||
* string continuation across multiple lines since it is not
|
||||
* supported in JSON.
|
||||
*
|
||||
* @param content the content to strip comments from
|
||||
* @returns the content without comments
|
||||
*/
|
||||
export function stripComments(content: string): string;
|
||||
@@ -1,89 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/// <reference path="../../../typings/require.d.ts" />
|
||||
|
||||
//@ts-check
|
||||
'use strict';
|
||||
|
||||
// ESM-uncomment-begin
|
||||
// const module = { exports: {} };
|
||||
// ESM-uncomment-end
|
||||
|
||||
(function () {
|
||||
|
||||
function factory() {
|
||||
// First group matches a double quoted string
|
||||
// Second group matches a single quoted string
|
||||
// Third group matches a multi line comment
|
||||
// Forth group matches a single line comment
|
||||
// Fifth group matches a trailing comma
|
||||
const regexp = /("[^"\\]*(?:\\.[^"\\]*)*")|('[^'\\]*(?:\\.[^'\\]*)*')|(\/\*[^\/\*]*(?:(?:\*|\/)[^\/\*]*)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))|(,\s*[}\]])/g;
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @returns {string}
|
||||
*/
|
||||
function stripComments(content) {
|
||||
return content.replace(regexp, function (match, _m1, _m2, m3, m4, m5) {
|
||||
// Only one of m1, m2, m3, m4, m5 matches
|
||||
if (m3) {
|
||||
// A block comment. Replace with nothing
|
||||
return '';
|
||||
} else if (m4) {
|
||||
// Since m4 is a single line comment is is at least of length 2 (e.g. //)
|
||||
// If it ends in \r?\n then keep it.
|
||||
const length = m4.length;
|
||||
if (m4[length - 1] === '\n') {
|
||||
return m4[length - 2] === '\r' ? '\r\n' : '\n';
|
||||
}
|
||||
else {
|
||||
return '';
|
||||
}
|
||||
} else if (m5) {
|
||||
// Remove the trailing comma
|
||||
return match.substring(1);
|
||||
} else {
|
||||
// We match a string
|
||||
return match;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @returns {any}
|
||||
*/
|
||||
function parse(content) {
|
||||
const commentsStripped = stripComments(content);
|
||||
|
||||
try {
|
||||
return JSON.parse(commentsStripped);
|
||||
} catch (error) {
|
||||
const trailingCommasStriped = commentsStripped.replace(/,\s*([}\]])/g, '$1');
|
||||
return JSON.parse(trailingCommasStriped);
|
||||
}
|
||||
}
|
||||
return {
|
||||
stripComments,
|
||||
parse
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof define === 'function') {
|
||||
// amd
|
||||
define([], function () { return factory(); });
|
||||
} else if (typeof module === 'object' && typeof module.exports === 'object') {
|
||||
// commonjs
|
||||
module.exports = factory();
|
||||
} else {
|
||||
console.trace('jsonc defined in UNKNOWN context (neither requirejs or commonjs)');
|
||||
}
|
||||
})();
|
||||
|
||||
// ESM-uncomment-begin
|
||||
// export const stripComments = module.exports.stripComments;
|
||||
// export const parse = module.exports.parse;
|
||||
// ESM-uncomment-end
|
||||
@@ -1,184 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Modifiers } from 'vs/base/common/keybindings';
|
||||
import { OperatingSystem } from 'vs/base/common/platform';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
export interface ModifierLabels {
|
||||
readonly ctrlKey: string;
|
||||
readonly shiftKey: string;
|
||||
readonly altKey: string;
|
||||
readonly metaKey: string;
|
||||
readonly separator: string;
|
||||
}
|
||||
|
||||
export interface KeyLabelProvider<T extends Modifiers> {
|
||||
(keybinding: T): string | null;
|
||||
}
|
||||
|
||||
export class ModifierLabelProvider {
|
||||
|
||||
public readonly modifierLabels: ModifierLabels[];
|
||||
|
||||
constructor(mac: ModifierLabels, windows: ModifierLabels, linux: ModifierLabels = windows) {
|
||||
this.modifierLabels = [null!]; // index 0 will never me accessed.
|
||||
this.modifierLabels[OperatingSystem.Macintosh] = mac;
|
||||
this.modifierLabels[OperatingSystem.Windows] = windows;
|
||||
this.modifierLabels[OperatingSystem.Linux] = linux;
|
||||
}
|
||||
|
||||
public toLabel<T extends Modifiers>(OS: OperatingSystem, chords: readonly T[], keyLabelProvider: KeyLabelProvider<T>): string | null {
|
||||
if (chords.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: string[] = [];
|
||||
for (let i = 0, len = chords.length; i < len; i++) {
|
||||
const chord = chords[i];
|
||||
const keyLabel = keyLabelProvider(chord);
|
||||
if (keyLabel === null) {
|
||||
// this keybinding cannot be expressed...
|
||||
return null;
|
||||
}
|
||||
result[i] = _simpleAsString(chord, keyLabel, this.modifierLabels[OS]);
|
||||
}
|
||||
return result.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A label provider that prints modifiers in a suitable format for displaying in the UI.
|
||||
*/
|
||||
export const UILabelProvider = new ModifierLabelProvider(
|
||||
{
|
||||
ctrlKey: '\u2303',
|
||||
shiftKey: '⇧',
|
||||
altKey: '⌥',
|
||||
metaKey: '⌘',
|
||||
separator: '',
|
||||
},
|
||||
{
|
||||
ctrlKey: nls.localize({ key: 'ctrlKey', comment: ['This is the short form for the Control key on the keyboard'] }, "Ctrl"),
|
||||
shiftKey: nls.localize({ key: 'shiftKey', comment: ['This is the short form for the Shift key on the keyboard'] }, "Shift"),
|
||||
altKey: nls.localize({ key: 'altKey', comment: ['This is the short form for the Alt key on the keyboard'] }, "Alt"),
|
||||
metaKey: nls.localize({ key: 'windowsKey', comment: ['This is the short form for the Windows key on the keyboard'] }, "Windows"),
|
||||
separator: '+',
|
||||
},
|
||||
{
|
||||
ctrlKey: nls.localize({ key: 'ctrlKey', comment: ['This is the short form for the Control key on the keyboard'] }, "Ctrl"),
|
||||
shiftKey: nls.localize({ key: 'shiftKey', comment: ['This is the short form for the Shift key on the keyboard'] }, "Shift"),
|
||||
altKey: nls.localize({ key: 'altKey', comment: ['This is the short form for the Alt key on the keyboard'] }, "Alt"),
|
||||
metaKey: nls.localize({ key: 'superKey', comment: ['This is the short form for the Super key on the keyboard'] }, "Super"),
|
||||
separator: '+',
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* A label provider that prints modifiers in a suitable format for ARIA.
|
||||
*/
|
||||
export const AriaLabelProvider = new ModifierLabelProvider(
|
||||
{
|
||||
ctrlKey: nls.localize({ key: 'ctrlKey.long', comment: ['This is the long form for the Control key on the keyboard'] }, "Control"),
|
||||
shiftKey: nls.localize({ key: 'shiftKey.long', comment: ['This is the long form for the Shift key on the keyboard'] }, "Shift"),
|
||||
altKey: nls.localize({ key: 'optKey.long', comment: ['This is the long form for the Alt/Option key on the keyboard'] }, "Option"),
|
||||
metaKey: nls.localize({ key: 'cmdKey.long', comment: ['This is the long form for the Command key on the keyboard'] }, "Command"),
|
||||
separator: '+',
|
||||
},
|
||||
{
|
||||
ctrlKey: nls.localize({ key: 'ctrlKey.long', comment: ['This is the long form for the Control key on the keyboard'] }, "Control"),
|
||||
shiftKey: nls.localize({ key: 'shiftKey.long', comment: ['This is the long form for the Shift key on the keyboard'] }, "Shift"),
|
||||
altKey: nls.localize({ key: 'altKey.long', comment: ['This is the long form for the Alt key on the keyboard'] }, "Alt"),
|
||||
metaKey: nls.localize({ key: 'windowsKey.long', comment: ['This is the long form for the Windows key on the keyboard'] }, "Windows"),
|
||||
separator: '+',
|
||||
},
|
||||
{
|
||||
ctrlKey: nls.localize({ key: 'ctrlKey.long', comment: ['This is the long form for the Control key on the keyboard'] }, "Control"),
|
||||
shiftKey: nls.localize({ key: 'shiftKey.long', comment: ['This is the long form for the Shift key on the keyboard'] }, "Shift"),
|
||||
altKey: nls.localize({ key: 'altKey.long', comment: ['This is the long form for the Alt key on the keyboard'] }, "Alt"),
|
||||
metaKey: nls.localize({ key: 'superKey.long', comment: ['This is the long form for the Super key on the keyboard'] }, "Super"),
|
||||
separator: '+',
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* A label provider that prints modifiers in a suitable format for Electron Accelerators.
|
||||
* See https://github.com/electron/electron/blob/master/docs/api/accelerator.md
|
||||
*/
|
||||
export const ElectronAcceleratorLabelProvider = new ModifierLabelProvider(
|
||||
{
|
||||
ctrlKey: 'Ctrl',
|
||||
shiftKey: 'Shift',
|
||||
altKey: 'Alt',
|
||||
metaKey: 'Cmd',
|
||||
separator: '+',
|
||||
},
|
||||
{
|
||||
ctrlKey: 'Ctrl',
|
||||
shiftKey: 'Shift',
|
||||
altKey: 'Alt',
|
||||
metaKey: 'Super',
|
||||
separator: '+',
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* A label provider that prints modifiers in a suitable format for user settings.
|
||||
*/
|
||||
export const UserSettingsLabelProvider = new ModifierLabelProvider(
|
||||
{
|
||||
ctrlKey: 'ctrl',
|
||||
shiftKey: 'shift',
|
||||
altKey: 'alt',
|
||||
metaKey: 'cmd',
|
||||
separator: '+',
|
||||
},
|
||||
{
|
||||
ctrlKey: 'ctrl',
|
||||
shiftKey: 'shift',
|
||||
altKey: 'alt',
|
||||
metaKey: 'win',
|
||||
separator: '+',
|
||||
},
|
||||
{
|
||||
ctrlKey: 'ctrl',
|
||||
shiftKey: 'shift',
|
||||
altKey: 'alt',
|
||||
metaKey: 'meta',
|
||||
separator: '+',
|
||||
}
|
||||
);
|
||||
|
||||
function _simpleAsString(modifiers: Modifiers, key: string, labels: ModifierLabels): string {
|
||||
if (key === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const result: string[] = [];
|
||||
|
||||
// translate modifier keys: Ctrl-Shift-Alt-Meta
|
||||
if (modifiers.ctrlKey) {
|
||||
result.push(labels.ctrlKey);
|
||||
}
|
||||
|
||||
if (modifiers.shiftKey) {
|
||||
result.push(labels.shiftKey);
|
||||
}
|
||||
|
||||
if (modifiers.altKey) {
|
||||
result.push(labels.altKey);
|
||||
}
|
||||
|
||||
if (modifiers.metaKey) {
|
||||
result.push(labels.metaKey);
|
||||
}
|
||||
|
||||
// the actual key
|
||||
if (key !== '') {
|
||||
result.push(key);
|
||||
}
|
||||
|
||||
return result.join(labels.separator);
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { KeyCodeUtils, ScanCodeUtils } from 'vs/base/common/keyCodes';
|
||||
import { KeyCodeChord, ScanCodeChord, Keybinding, Chord } from 'vs/base/common/keybindings';
|
||||
|
||||
export class KeybindingParser {
|
||||
|
||||
private static _readModifiers(input: string) {
|
||||
input = input.toLowerCase().trim();
|
||||
|
||||
let ctrl = false;
|
||||
let shift = false;
|
||||
let alt = false;
|
||||
let meta = false;
|
||||
|
||||
let matchedModifier: boolean;
|
||||
|
||||
do {
|
||||
matchedModifier = false;
|
||||
if (/^ctrl(\+|\-)/.test(input)) {
|
||||
ctrl = true;
|
||||
input = input.substr('ctrl-'.length);
|
||||
matchedModifier = true;
|
||||
}
|
||||
if (/^shift(\+|\-)/.test(input)) {
|
||||
shift = true;
|
||||
input = input.substr('shift-'.length);
|
||||
matchedModifier = true;
|
||||
}
|
||||
if (/^alt(\+|\-)/.test(input)) {
|
||||
alt = true;
|
||||
input = input.substr('alt-'.length);
|
||||
matchedModifier = true;
|
||||
}
|
||||
if (/^meta(\+|\-)/.test(input)) {
|
||||
meta = true;
|
||||
input = input.substr('meta-'.length);
|
||||
matchedModifier = true;
|
||||
}
|
||||
if (/^win(\+|\-)/.test(input)) {
|
||||
meta = true;
|
||||
input = input.substr('win-'.length);
|
||||
matchedModifier = true;
|
||||
}
|
||||
if (/^cmd(\+|\-)/.test(input)) {
|
||||
meta = true;
|
||||
input = input.substr('cmd-'.length);
|
||||
matchedModifier = true;
|
||||
}
|
||||
} while (matchedModifier);
|
||||
|
||||
let key: string;
|
||||
|
||||
const firstSpaceIdx = input.indexOf(' ');
|
||||
if (firstSpaceIdx > 0) {
|
||||
key = input.substring(0, firstSpaceIdx);
|
||||
input = input.substring(firstSpaceIdx);
|
||||
} else {
|
||||
key = input;
|
||||
input = '';
|
||||
}
|
||||
|
||||
return {
|
||||
remains: input,
|
||||
ctrl,
|
||||
shift,
|
||||
alt,
|
||||
meta,
|
||||
key
|
||||
};
|
||||
}
|
||||
|
||||
private static parseChord(input: string): [Chord, string] {
|
||||
const mods = this._readModifiers(input);
|
||||
const scanCodeMatch = mods.key.match(/^\[([^\]]+)\]$/);
|
||||
if (scanCodeMatch) {
|
||||
const strScanCode = scanCodeMatch[1];
|
||||
const scanCode = ScanCodeUtils.lowerCaseToEnum(strScanCode);
|
||||
return [new ScanCodeChord(mods.ctrl, mods.shift, mods.alt, mods.meta, scanCode), mods.remains];
|
||||
}
|
||||
const keyCode = KeyCodeUtils.fromUserSettings(mods.key);
|
||||
return [new KeyCodeChord(mods.ctrl, mods.shift, mods.alt, mods.meta, keyCode), mods.remains];
|
||||
}
|
||||
|
||||
static parseKeybinding(input: string): Keybinding | null {
|
||||
if (!input) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const chords: Chord[] = [];
|
||||
let chord: Chord;
|
||||
|
||||
while (input.length > 0) {
|
||||
[chord, input] = this.parseChord(input);
|
||||
chords.push(chord);
|
||||
}
|
||||
return (chords.length > 0 ? new Keybinding(chords) : null);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
|
||||
export interface ILink {
|
||||
readonly label: string;
|
||||
readonly href: string;
|
||||
readonly title?: string;
|
||||
}
|
||||
|
||||
export type LinkedTextNode = string | ILink;
|
||||
|
||||
export class LinkedText {
|
||||
|
||||
constructor(readonly nodes: LinkedTextNode[]) { }
|
||||
|
||||
@memoize
|
||||
toString(): string {
|
||||
return this.nodes.map(node => typeof node === 'string' ? node : node.label).join('');
|
||||
}
|
||||
}
|
||||
|
||||
const LINK_REGEX = /\[([^\]]+)\]\(((?:https?:\/\/|command:|file:)[^\)\s]+)(?: (["'])(.+?)(\3))?\)/gi;
|
||||
|
||||
export function parseLinkedText(text: string): LinkedText {
|
||||
const result: LinkedTextNode[] = [];
|
||||
|
||||
let index = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while (match = LINK_REGEX.exec(text)) {
|
||||
if (match.index - index > 0) {
|
||||
result.push(text.substring(index, match.index));
|
||||
}
|
||||
|
||||
const [, label, href, , title] = match;
|
||||
|
||||
if (title) {
|
||||
result.push({ label, href, title });
|
||||
} else {
|
||||
result.push({ label, href });
|
||||
}
|
||||
|
||||
index = match.index + match[0].length;
|
||||
}
|
||||
|
||||
if (index < text.length) {
|
||||
result.push(text.substring(index));
|
||||
}
|
||||
|
||||
return new LinkedText(result);
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { extname } from 'vs/base/common/path';
|
||||
|
||||
export const Mimes = Object.freeze({
|
||||
text: 'text/plain',
|
||||
binary: 'application/octet-stream',
|
||||
unknown: 'application/unknown',
|
||||
markdown: 'text/markdown',
|
||||
latex: 'text/latex',
|
||||
uriList: 'text/uri-list',
|
||||
});
|
||||
|
||||
interface MapExtToMediaMimes {
|
||||
[index: string]: string;
|
||||
}
|
||||
|
||||
const mapExtToTextMimes: MapExtToMediaMimes = {
|
||||
'.css': 'text/css',
|
||||
'.csv': 'text/csv',
|
||||
'.htm': 'text/html',
|
||||
'.html': 'text/html',
|
||||
'.ics': 'text/calendar',
|
||||
'.js': 'text/javascript',
|
||||
'.mjs': 'text/javascript',
|
||||
'.txt': 'text/plain',
|
||||
'.xml': 'text/xml'
|
||||
};
|
||||
|
||||
// Known media mimes that we can handle
|
||||
const mapExtToMediaMimes: MapExtToMediaMimes = {
|
||||
'.aac': 'audio/x-aac',
|
||||
'.avi': 'video/x-msvideo',
|
||||
'.bmp': 'image/bmp',
|
||||
'.flv': 'video/x-flv',
|
||||
'.gif': 'image/gif',
|
||||
'.ico': 'image/x-icon',
|
||||
'.jpe': 'image/jpg',
|
||||
'.jpeg': 'image/jpg',
|
||||
'.jpg': 'image/jpg',
|
||||
'.m1v': 'video/mpeg',
|
||||
'.m2a': 'audio/mpeg',
|
||||
'.m2v': 'video/mpeg',
|
||||
'.m3a': 'audio/mpeg',
|
||||
'.mid': 'audio/midi',
|
||||
'.midi': 'audio/midi',
|
||||
'.mk3d': 'video/x-matroska',
|
||||
'.mks': 'video/x-matroska',
|
||||
'.mkv': 'video/x-matroska',
|
||||
'.mov': 'video/quicktime',
|
||||
'.movie': 'video/x-sgi-movie',
|
||||
'.mp2': 'audio/mpeg',
|
||||
'.mp2a': 'audio/mpeg',
|
||||
'.mp3': 'audio/mpeg',
|
||||
'.mp4': 'video/mp4',
|
||||
'.mp4a': 'audio/mp4',
|
||||
'.mp4v': 'video/mp4',
|
||||
'.mpe': 'video/mpeg',
|
||||
'.mpeg': 'video/mpeg',
|
||||
'.mpg': 'video/mpeg',
|
||||
'.mpg4': 'video/mp4',
|
||||
'.mpga': 'audio/mpeg',
|
||||
'.oga': 'audio/ogg',
|
||||
'.ogg': 'audio/ogg',
|
||||
'.opus': 'audio/opus',
|
||||
'.ogv': 'video/ogg',
|
||||
'.png': 'image/png',
|
||||
'.psd': 'image/vnd.adobe.photoshop',
|
||||
'.qt': 'video/quicktime',
|
||||
'.spx': 'audio/ogg',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.tga': 'image/x-tga',
|
||||
'.tif': 'image/tiff',
|
||||
'.tiff': 'image/tiff',
|
||||
'.wav': 'audio/x-wav',
|
||||
'.webm': 'video/webm',
|
||||
'.webp': 'image/webp',
|
||||
'.wma': 'audio/x-ms-wma',
|
||||
'.wmv': 'video/x-ms-wmv',
|
||||
'.woff': 'application/font-woff',
|
||||
};
|
||||
|
||||
export function getMediaOrTextMime(path: string): string | undefined {
|
||||
const ext = extname(path);
|
||||
const textMime = mapExtToTextMimes[ext.toLowerCase()];
|
||||
if (textMime !== undefined) {
|
||||
return textMime;
|
||||
} else {
|
||||
return getMediaMime(path);
|
||||
}
|
||||
}
|
||||
|
||||
export function getMediaMime(path: string): string | undefined {
|
||||
const ext = extname(path);
|
||||
return mapExtToMediaMimes[ext.toLowerCase()];
|
||||
}
|
||||
|
||||
export function getExtensionForMimeType(mimeType: string): string | undefined {
|
||||
for (const extension in mapExtToMediaMimes) {
|
||||
if (mapExtToMediaMimes[extension] === mimeType) {
|
||||
return extension;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const _simplePattern = /^(.+)\/(.+?)(;.+)?$/;
|
||||
|
||||
export function normalizeMimeType(mimeType: string): string;
|
||||
export function normalizeMimeType(mimeType: string, strict: true): string | undefined;
|
||||
export function normalizeMimeType(mimeType: string, strict?: true): string | undefined {
|
||||
|
||||
const match = _simplePattern.exec(mimeType);
|
||||
if (!match) {
|
||||
return strict
|
||||
? undefined
|
||||
: mimeType;
|
||||
}
|
||||
// https://datatracker.ietf.org/doc/html/rfc2045#section-5.1
|
||||
// media and subtype must ALWAYS be lowercase, parameter not
|
||||
return `${match[1].toLowerCase()}/${match[2].toLowerCase()}${match[3] ?? ''}`;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface INavigator<T> {
|
||||
current(): T | null;
|
||||
previous(): T | null;
|
||||
first(): T | null;
|
||||
last(): T | null;
|
||||
next(): T | null;
|
||||
}
|
||||
|
||||
export class ArrayNavigator<T> implements INavigator<T> {
|
||||
|
||||
constructor(
|
||||
private readonly items: readonly T[],
|
||||
protected start: number = 0,
|
||||
protected end: number = items.length,
|
||||
protected index = start - 1
|
||||
) { }
|
||||
|
||||
current(): T | null {
|
||||
if (this.index === this.start - 1 || this.index === this.end) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.items[this.index];
|
||||
}
|
||||
|
||||
next(): T | null {
|
||||
this.index = Math.min(this.index + 1, this.end);
|
||||
return this.current();
|
||||
}
|
||||
|
||||
previous(): T | null {
|
||||
this.index = Math.max(this.index - 1, this.start - 1);
|
||||
return this.current();
|
||||
}
|
||||
|
||||
first(): T | null {
|
||||
this.index = this.start;
|
||||
return this.current();
|
||||
}
|
||||
|
||||
last(): T | null {
|
||||
this.index = this.end - 1;
|
||||
return this.current();
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export namespace Schemas {
|
||||
|
||||
/**
|
||||
* A schema that is used for models that exist in memory
|
||||
* only and that have no correspondence on a server or such.
|
||||
*/
|
||||
export const inMemory = 'inmemory';
|
||||
|
||||
/**
|
||||
* A schema that is used for setting files
|
||||
*/
|
||||
export const vscode = 'vscode';
|
||||
|
||||
/**
|
||||
* A schema that is used for internal private files
|
||||
*/
|
||||
export const internal = 'private';
|
||||
|
||||
/**
|
||||
* A walk-through document.
|
||||
*/
|
||||
export const walkThrough = 'walkThrough';
|
||||
|
||||
/**
|
||||
* An embedded code snippet.
|
||||
*/
|
||||
export const walkThroughSnippet = 'walkThroughSnippet';
|
||||
|
||||
export const http = 'http';
|
||||
|
||||
export const https = 'https';
|
||||
|
||||
export const file = 'file';
|
||||
|
||||
export const mailto = 'mailto';
|
||||
|
||||
export const untitled = 'untitled';
|
||||
|
||||
export const data = 'data';
|
||||
|
||||
export const command = 'command';
|
||||
|
||||
export const vscodeRemote = 'vscode-remote';
|
||||
|
||||
export const vscodeRemoteResource = 'vscode-remote-resource';
|
||||
|
||||
export const vscodeManagedRemoteResource = 'vscode-managed-remote-resource';
|
||||
|
||||
export const vscodeUserData = 'vscode-userdata';
|
||||
|
||||
export const vscodeCustomEditor = 'vscode-custom-editor';
|
||||
|
||||
export const vscodeNotebookCell = 'vscode-notebook-cell';
|
||||
export const vscodeNotebookCellMetadata = 'vscode-notebook-cell-metadata';
|
||||
export const vscodeNotebookCellOutput = 'vscode-notebook-cell-output';
|
||||
export const vscodeInteractiveInput = 'vscode-interactive-input';
|
||||
|
||||
export const vscodeSettings = 'vscode-settings';
|
||||
|
||||
export const vscodeWorkspaceTrust = 'vscode-workspace-trust';
|
||||
|
||||
export const vscodeTerminal = 'vscode-terminal';
|
||||
|
||||
/** Scheme used for code blocks in chat. */
|
||||
export const vscodeChatCodeBlock = 'vscode-chat-code-block';
|
||||
|
||||
/**
|
||||
* Scheme used for backing documents created by copilot for chat.
|
||||
*/
|
||||
export const vscodeCopilotBackingChatCodeBlock = 'vscode-copilot-chat-code-block';
|
||||
|
||||
/** Scheme used for LHS of code compare (aka diff) blocks in chat. */
|
||||
export const vscodeChatCodeCompareBlock = 'vscode-chat-code-compare-block';
|
||||
|
||||
/** Scheme used for the chat input editor. */
|
||||
export const vscodeChatSesssion = 'vscode-chat-editor';
|
||||
|
||||
/**
|
||||
* Scheme used internally for webviews that aren't linked to a resource (i.e. not custom editors)
|
||||
*/
|
||||
export const webviewPanel = 'webview-panel';
|
||||
|
||||
/**
|
||||
* Scheme used for loading the wrapper html and script in webviews.
|
||||
*/
|
||||
export const vscodeWebview = 'vscode-webview';
|
||||
|
||||
/**
|
||||
* Scheme used for extension pages
|
||||
*/
|
||||
export const extension = 'extension';
|
||||
|
||||
/**
|
||||
* Scheme used as a replacement of `file` scheme to load
|
||||
* files with our custom protocol handler (desktop only).
|
||||
*/
|
||||
export const vscodeFileResource = 'vscode-file';
|
||||
|
||||
/**
|
||||
* Scheme used for temporary resources
|
||||
*/
|
||||
export const tmp = 'tmp';
|
||||
|
||||
/**
|
||||
* Scheme used vs live share
|
||||
*/
|
||||
export const vsls = 'vsls';
|
||||
|
||||
/**
|
||||
* Scheme used for the Source Control commit input's text document
|
||||
*/
|
||||
export const vscodeSourceControl = 'vscode-scm';
|
||||
|
||||
/**
|
||||
* Scheme used for input box for creating comments.
|
||||
*/
|
||||
export const commentsInput = 'comment';
|
||||
|
||||
/**
|
||||
* Scheme used for special rendering of settings in the release notes
|
||||
*/
|
||||
export const codeSetting = 'code-setting';
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isTypedArray, isObject, isUndefinedOrNull } from 'vs/base/common/types';
|
||||
|
||||
export function deepClone<T>(obj: T): T {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
if (obj instanceof RegExp) {
|
||||
return obj;
|
||||
}
|
||||
const result: any = Array.isArray(obj) ? [] : {};
|
||||
Object.entries(obj).forEach(([key, value]) => {
|
||||
result[key] = value && typeof value === 'object' ? deepClone(value) : value;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function deepFreeze<T>(obj: T): T {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
const stack: any[] = [obj];
|
||||
while (stack.length > 0) {
|
||||
const obj = stack.shift();
|
||||
Object.freeze(obj);
|
||||
for (const key in obj) {
|
||||
if (_hasOwnProperty.call(obj, key)) {
|
||||
const prop = obj[key];
|
||||
if (typeof prop === 'object' && !Object.isFrozen(prop) && !isTypedArray(prop)) {
|
||||
stack.push(prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
const _hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
|
||||
|
||||
export function cloneAndChange(obj: any, changer: (orig: any) => any): any {
|
||||
return _cloneAndChange(obj, changer, new Set());
|
||||
}
|
||||
|
||||
function _cloneAndChange(obj: any, changer: (orig: any) => any, seen: Set<any>): any {
|
||||
if (isUndefinedOrNull(obj)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const changed = changer(obj);
|
||||
if (typeof changed !== 'undefined') {
|
||||
return changed;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
const r1: any[] = [];
|
||||
for (const e of obj) {
|
||||
r1.push(_cloneAndChange(e, changer, seen));
|
||||
}
|
||||
return r1;
|
||||
}
|
||||
|
||||
if (isObject(obj)) {
|
||||
if (seen.has(obj)) {
|
||||
throw new Error('Cannot clone recursive data-structure');
|
||||
}
|
||||
seen.add(obj);
|
||||
const r2 = {};
|
||||
for (const i2 in obj) {
|
||||
if (_hasOwnProperty.call(obj, i2)) {
|
||||
(r2 as any)[i2] = _cloneAndChange(obj[i2], changer, seen);
|
||||
}
|
||||
}
|
||||
seen.delete(obj);
|
||||
return r2;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies all properties of source into destination. The optional parameter "overwrite" allows to control
|
||||
* if existing properties on the destination should be overwritten or not. Defaults to true (overwrite).
|
||||
*/
|
||||
export function mixin(destination: any, source: any, overwrite: boolean = true): any {
|
||||
if (!isObject(destination)) {
|
||||
return source;
|
||||
}
|
||||
|
||||
if (isObject(source)) {
|
||||
Object.keys(source).forEach(key => {
|
||||
if (key in destination) {
|
||||
if (overwrite) {
|
||||
if (isObject(destination[key]) && isObject(source[key])) {
|
||||
mixin(destination[key], source[key], overwrite);
|
||||
} else {
|
||||
destination[key] = source[key];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
destination[key] = source[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
|
||||
export function equals(one: any, other: any): boolean {
|
||||
if (one === other) {
|
||||
return true;
|
||||
}
|
||||
if (one === null || one === undefined || other === null || other === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (typeof one !== typeof other) {
|
||||
return false;
|
||||
}
|
||||
if (typeof one !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if ((Array.isArray(one)) !== (Array.isArray(other))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let i: number;
|
||||
let key: string;
|
||||
|
||||
if (Array.isArray(one)) {
|
||||
if (one.length !== other.length) {
|
||||
return false;
|
||||
}
|
||||
for (i = 0; i < one.length; i++) {
|
||||
if (!equals(one[i], other[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const oneKeys: string[] = [];
|
||||
|
||||
for (key in one) {
|
||||
oneKeys.push(key);
|
||||
}
|
||||
oneKeys.sort();
|
||||
const otherKeys: string[] = [];
|
||||
for (key in other) {
|
||||
otherKeys.push(key);
|
||||
}
|
||||
otherKeys.sort();
|
||||
if (!equals(oneKeys, otherKeys)) {
|
||||
return false;
|
||||
}
|
||||
for (i = 0; i < oneKeys.length; i++) {
|
||||
if (!equals(one[oneKeys[i]], other[oneKeys[i]])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls `JSON.Stringify` with a replacer to break apart any circular references.
|
||||
* This prevents `JSON`.stringify` from throwing the exception
|
||||
* "Uncaught TypeError: Converting circular structure to JSON"
|
||||
*/
|
||||
export function safeStringify(obj: any): string {
|
||||
const seen = new Set<any>();
|
||||
return JSON.stringify(obj, (key, value) => {
|
||||
if (isObject(value) || Array.isArray(value)) {
|
||||
if (seen.has(value)) {
|
||||
return '[Circular]';
|
||||
} else {
|
||||
seen.add(value);
|
||||
}
|
||||
}
|
||||
if (typeof value === 'bigint') {
|
||||
return `[BigInt ${value.toString()}]`;
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
type obj = { [key: string]: any };
|
||||
/**
|
||||
* Returns an object that has keys for each value that is different in the base object. Keys
|
||||
* that do not exist in the target but in the base object are not considered.
|
||||
*
|
||||
* Note: This is not a deep-diffing method, so the values are strictly taken into the resulting
|
||||
* object if they differ.
|
||||
*
|
||||
* @param base the object to diff against
|
||||
* @param obj the object to use for diffing
|
||||
*/
|
||||
export function distinct(base: obj, target: obj): obj {
|
||||
const result = Object.create(null);
|
||||
|
||||
if (!base || !target) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const targetKeys = Object.keys(target);
|
||||
targetKeys.forEach(k => {
|
||||
const baseValue = base[k];
|
||||
const targetValue = target[k];
|
||||
|
||||
if (!equals(baseValue, targetValue)) {
|
||||
result[k] = targetValue;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getCaseInsensitive(target: obj, key: string): any {
|
||||
const lowercaseKey = key.toLowerCase();
|
||||
const equivalentKey = Object.keys(target).find(k => k.toLowerCase() === lowercaseKey);
|
||||
return equivalentKey ? target[equivalentKey] : target[key];
|
||||
}
|
||||
|
||||
export function filter(obj: obj, predicate: (key: string, value: any) => boolean): obj {
|
||||
const result = Object.create(null);
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (predicate(key, value)) {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getAllPropertyNames(obj: object): string[] {
|
||||
let res: string[] = [];
|
||||
while (Object.prototype !== obj) {
|
||||
res = res.concat(Object.getOwnPropertyNames(obj));
|
||||
obj = Object.getPrototypeOf(obj);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export function getAllMethodNames(obj: object): string[] {
|
||||
const methods: string[] = [];
|
||||
for (const prop of getAllPropertyNames(obj)) {
|
||||
if (typeof (obj as any)[prop] === 'function') {
|
||||
methods.push(prop);
|
||||
}
|
||||
}
|
||||
return methods;
|
||||
}
|
||||
|
||||
export function createProxyObject<T extends object>(methodNames: string[], invoke: (method: string, args: unknown[]) => unknown): T {
|
||||
const createProxyMethod = (method: string): () => unknown => {
|
||||
return function () {
|
||||
const args = Array.prototype.slice.call(arguments, 0);
|
||||
return invoke(method, args);
|
||||
};
|
||||
};
|
||||
|
||||
const result = {} as T;
|
||||
for (const methodName of methodNames) {
|
||||
(<any>result)[methodName] = createProxyMethod(methodName);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function mapValues<T extends {}, R>(obj: T, fn: (value: T[keyof T], key: string) => R): { [K in keyof T]: R } {
|
||||
const result: { [key: string]: R } = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
result[key] = fn(<T[keyof T]>value, key);
|
||||
}
|
||||
return result as { [K in keyof T]: R };
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { range } from 'vs/base/common/arrays';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { CancellationError } from 'vs/base/common/errors';
|
||||
|
||||
/**
|
||||
* A Pager is a stateless abstraction over a paged collection.
|
||||
*/
|
||||
export interface IPager<T> {
|
||||
firstPage: T[];
|
||||
total: number;
|
||||
pageSize: number;
|
||||
getPage(pageIndex: number, cancellationToken: CancellationToken): Promise<T[]>;
|
||||
}
|
||||
|
||||
interface IPage<T> {
|
||||
isResolved: boolean;
|
||||
promise: Promise<void> | null;
|
||||
cts: CancellationTokenSource | null;
|
||||
promiseIndexes: Set<number>;
|
||||
elements: T[];
|
||||
}
|
||||
|
||||
function createPage<T>(elements?: T[]): IPage<T> {
|
||||
return {
|
||||
isResolved: !!elements,
|
||||
promise: null,
|
||||
cts: null,
|
||||
promiseIndexes: new Set<number>(),
|
||||
elements: elements || []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A PagedModel is a stateful model over an abstracted paged collection.
|
||||
*/
|
||||
export interface IPagedModel<T> {
|
||||
length: number;
|
||||
isResolved(index: number): boolean;
|
||||
get(index: number): T;
|
||||
resolve(index: number, cancellationToken: CancellationToken): Promise<T>;
|
||||
}
|
||||
|
||||
export function singlePagePager<T>(elements: T[]): IPager<T> {
|
||||
return {
|
||||
firstPage: elements,
|
||||
total: elements.length,
|
||||
pageSize: elements.length,
|
||||
getPage: (pageIndex: number, cancellationToken: CancellationToken): Promise<T[]> => {
|
||||
return Promise.resolve(elements);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class PagedModel<T> implements IPagedModel<T> {
|
||||
|
||||
private pager: IPager<T>;
|
||||
private pages: IPage<T>[] = [];
|
||||
|
||||
get length(): number { return this.pager.total; }
|
||||
|
||||
constructor(arg: IPager<T> | T[]) {
|
||||
this.pager = Array.isArray(arg) ? singlePagePager<T>(arg) : arg;
|
||||
|
||||
const totalPages = Math.ceil(this.pager.total / this.pager.pageSize);
|
||||
|
||||
this.pages = [
|
||||
createPage(this.pager.firstPage.slice()),
|
||||
...range(totalPages - 1).map(() => createPage<T>())
|
||||
];
|
||||
}
|
||||
|
||||
isResolved(index: number): boolean {
|
||||
const pageIndex = Math.floor(index / this.pager.pageSize);
|
||||
const page = this.pages[pageIndex];
|
||||
|
||||
return !!page.isResolved;
|
||||
}
|
||||
|
||||
get(index: number): T {
|
||||
const pageIndex = Math.floor(index / this.pager.pageSize);
|
||||
const indexInPage = index % this.pager.pageSize;
|
||||
const page = this.pages[pageIndex];
|
||||
|
||||
return page.elements[indexInPage];
|
||||
}
|
||||
|
||||
resolve(index: number, cancellationToken: CancellationToken): Promise<T> {
|
||||
if (cancellationToken.isCancellationRequested) {
|
||||
return Promise.reject(new CancellationError());
|
||||
}
|
||||
|
||||
const pageIndex = Math.floor(index / this.pager.pageSize);
|
||||
const indexInPage = index % this.pager.pageSize;
|
||||
const page = this.pages[pageIndex];
|
||||
|
||||
if (page.isResolved) {
|
||||
return Promise.resolve(page.elements[indexInPage]);
|
||||
}
|
||||
|
||||
if (!page.promise) {
|
||||
page.cts = new CancellationTokenSource();
|
||||
page.promise = this.pager.getPage(pageIndex, page.cts.token)
|
||||
.then(elements => {
|
||||
page.elements = elements;
|
||||
page.isResolved = true;
|
||||
page.promise = null;
|
||||
page.cts = null;
|
||||
}, err => {
|
||||
page.isResolved = false;
|
||||
page.promise = null;
|
||||
page.cts = null;
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
const listener = cancellationToken.onCancellationRequested(() => {
|
||||
if (!page.cts) {
|
||||
return;
|
||||
}
|
||||
|
||||
page.promiseIndexes.delete(index);
|
||||
|
||||
if (page.promiseIndexes.size === 0) {
|
||||
page.cts.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
page.promiseIndexes.add(index);
|
||||
|
||||
return page.promise.then(() => page.elements[indexInPage])
|
||||
.finally(() => listener.dispose());
|
||||
}
|
||||
}
|
||||
|
||||
export class DelayedPagedModel<T> implements IPagedModel<T> {
|
||||
|
||||
get length(): number { return this.model.length; }
|
||||
|
||||
constructor(private model: IPagedModel<T>, private timeout: number = 500) { }
|
||||
|
||||
isResolved(index: number): boolean {
|
||||
return this.model.isResolved(index);
|
||||
}
|
||||
|
||||
get(index: number): T {
|
||||
return this.model.get(index);
|
||||
}
|
||||
|
||||
resolve(index: number, cancellationToken: CancellationToken): Promise<T> {
|
||||
return new Promise((c, e) => {
|
||||
if (cancellationToken.isCancellationRequested) {
|
||||
return e(new CancellationError());
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (cancellationToken.isCancellationRequested) {
|
||||
return e(new CancellationError());
|
||||
}
|
||||
|
||||
timeoutCancellation.dispose();
|
||||
this.model.resolve(index, cancellationToken).then(c, e);
|
||||
}, this.timeout);
|
||||
|
||||
const timeoutCancellation = cancellationToken.onCancellationRequested(() => {
|
||||
clearTimeout(timer);
|
||||
timeoutCancellation.dispose();
|
||||
e(new CancellationError());
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to array.map, `mapPager` lets you map the elements of an
|
||||
* abstract paged collection to another type.
|
||||
*/
|
||||
export function mapPager<T, R>(pager: IPager<T>, fn: (t: T) => R): IPager<R> {
|
||||
return {
|
||||
firstPage: pager.firstPage.map(fn),
|
||||
total: pager.total,
|
||||
pageSize: pager.pageSize,
|
||||
getPage: (pageIndex, token) => pager.getPage(pageIndex, token).then(r => r.map(fn))
|
||||
};
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export const enum ValidationState {
|
||||
OK = 0,
|
||||
Info = 1,
|
||||
Warning = 2,
|
||||
Error = 3,
|
||||
Fatal = 4
|
||||
}
|
||||
|
||||
export class ValidationStatus {
|
||||
private _state: ValidationState;
|
||||
|
||||
constructor() {
|
||||
this._state = ValidationState.OK;
|
||||
}
|
||||
|
||||
public get state(): ValidationState {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public set state(value: ValidationState) {
|
||||
if (value > this._state) {
|
||||
this._state = value;
|
||||
}
|
||||
}
|
||||
|
||||
public isOK(): boolean {
|
||||
return this._state === ValidationState.OK;
|
||||
}
|
||||
|
||||
public isFatal(): boolean {
|
||||
return this._state === ValidationState.Fatal;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IProblemReporter {
|
||||
info(message: string): void;
|
||||
warn(message: string): void;
|
||||
error(message: string): void;
|
||||
fatal(message: string): void;
|
||||
status: ValidationStatus;
|
||||
}
|
||||
|
||||
export abstract class Parser {
|
||||
|
||||
private _problemReporter: IProblemReporter;
|
||||
|
||||
constructor(problemReporter: IProblemReporter) {
|
||||
this._problemReporter = problemReporter;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._problemReporter.status.state = ValidationState.OK;
|
||||
}
|
||||
|
||||
public get problemReporter(): IProblemReporter {
|
||||
return this._problemReporter;
|
||||
}
|
||||
|
||||
public info(message: string): void {
|
||||
this._problemReporter.info(message);
|
||||
}
|
||||
|
||||
public warn(message: string): void {
|
||||
this._problemReporter.warn(message);
|
||||
}
|
||||
|
||||
public error(message: string): void {
|
||||
this._problemReporter.error(message);
|
||||
}
|
||||
|
||||
public fatal(message: string): void {
|
||||
this._problemReporter.fatal(message);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Vendored
-16
@@ -1,16 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface PerformanceMark {
|
||||
readonly name: string;
|
||||
readonly startTime: number;
|
||||
}
|
||||
|
||||
export function mark(name: string): void;
|
||||
|
||||
/**
|
||||
* Returns all marks, sorted by `startTime`.
|
||||
*/
|
||||
export function getMarks(): PerformanceMark[];
|
||||
@@ -1,135 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
'use strict';
|
||||
|
||||
// ESM-uncomment-begin
|
||||
// const module = { exports: {} };
|
||||
// ESM-uncomment-end
|
||||
|
||||
(function () {
|
||||
|
||||
/**
|
||||
* @returns {{mark(name:string):void, getMarks():{name:string, startTime:number}[]}}
|
||||
*/
|
||||
function _definePolyfillMarks(timeOrigin) {
|
||||
|
||||
const _data = [];
|
||||
if (typeof timeOrigin === 'number') {
|
||||
_data.push('code/timeOrigin', timeOrigin);
|
||||
}
|
||||
|
||||
function mark(name) {
|
||||
_data.push(name, Date.now());
|
||||
}
|
||||
function getMarks() {
|
||||
const result = [];
|
||||
for (let i = 0; i < _data.length; i += 2) {
|
||||
result.push({
|
||||
name: _data[i],
|
||||
startTime: _data[i + 1],
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return { mark, getMarks };
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {{mark(name:string):void, getMarks():{name:string, startTime:number}[]}}
|
||||
*/
|
||||
function _define() {
|
||||
|
||||
// Identify browser environment when following property is not present
|
||||
// https://nodejs.org/dist/latest-v16.x/docs/api/perf_hooks.html#performancenodetiming
|
||||
// @ts-ignore
|
||||
if (typeof performance === 'object' && typeof performance.mark === 'function' && !performance.nodeTiming) {
|
||||
// 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
} 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 = performance?.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 _factory(sharedObj) {
|
||||
if (!sharedObj.MonacoPerformanceMarks) {
|
||||
sharedObj.MonacoPerformanceMarks = _define();
|
||||
}
|
||||
return sharedObj.MonacoPerformanceMarks;
|
||||
}
|
||||
|
||||
// This module can be loaded in an amd and commonjs-context.
|
||||
// Because we want both instances to use the same perf-data
|
||||
// we store them globally
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var sharedObj;
|
||||
if (typeof global === 'object') {
|
||||
// nodejs
|
||||
sharedObj = global;
|
||||
} else if (typeof self === 'object') {
|
||||
// browser
|
||||
sharedObj = self;
|
||||
} else {
|
||||
sharedObj = {};
|
||||
}
|
||||
|
||||
if (typeof define === 'function') {
|
||||
// amd
|
||||
define([], function () { return _factory(sharedObj); });
|
||||
} else if (typeof module === 'object' && typeof module.exports === 'object') {
|
||||
// commonjs
|
||||
module.exports = _factory(sharedObj);
|
||||
} else {
|
||||
console.trace('perf-util defined in UNKNOWN context (neither requirejs or commonjs)');
|
||||
// @ts-ignore
|
||||
sharedObj.perf = _factory(sharedObj);
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
// ESM-uncomment-begin
|
||||
// export const mark = module.exports.mark;
|
||||
// export const getMarks = module.exports.getMarks;
|
||||
// ESM-uncomment-end
|
||||
@@ -1,13 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* @returns Returns a random port between 1025 and 65535.
|
||||
*/
|
||||
export function randomPort(): number {
|
||||
const min = 1025;
|
||||
const max = 65535;
|
||||
return min + Math.floor((max - min) * Math.random());
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
|
||||
const unset = Symbol('unset');
|
||||
|
||||
export interface IPrefixTreeNode<T> {
|
||||
/** Possible children of the node. */
|
||||
children?: ReadonlyMap<string, Node<T>>;
|
||||
|
||||
/** The value if data exists for this node in the tree. Mutable. */
|
||||
value: T | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple prefix tree implementation where a value is stored based on
|
||||
* well-defined prefix segments.
|
||||
*/
|
||||
export class WellDefinedPrefixTree<V> {
|
||||
private readonly root = new Node<V>();
|
||||
private _size = 0;
|
||||
|
||||
public get size() {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
/** Gets the top-level nodes of the tree */
|
||||
public get nodes(): Iterable<IPrefixTreeNode<V>> {
|
||||
return this.root.children?.values() || Iterable.empty();
|
||||
}
|
||||
|
||||
/** Gets the top-level nodes of the tree */
|
||||
public get entries(): Iterable<[string, IPrefixTreeNode<V>]> {
|
||||
return this.root.children?.entries() || Iterable.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new value in the prefix tree.
|
||||
* @param onNode - called for each node as we descend to the insertion point,
|
||||
* including the insertion point itself.
|
||||
*/
|
||||
insert(key: Iterable<string>, value: V, onNode?: (n: IPrefixTreeNode<V>) => void): void {
|
||||
this.opNode(key, n => n._value = value, onNode);
|
||||
}
|
||||
|
||||
/** Mutates a value in the prefix tree. */
|
||||
mutate(key: Iterable<string>, mutate: (value?: V) => V): void {
|
||||
this.opNode(key, n => n._value = mutate(n._value === unset ? undefined : n._value));
|
||||
}
|
||||
|
||||
/** Mutates nodes along the path in the prefix tree. */
|
||||
mutatePath(key: Iterable<string>, mutate: (node: IPrefixTreeNode<V>) => void): void {
|
||||
this.opNode(key, () => { }, n => mutate(n));
|
||||
}
|
||||
|
||||
/** Deletes a node from the prefix tree, returning the value it contained. */
|
||||
delete(key: Iterable<string>): V | undefined {
|
||||
const path = this.getPathToKey(key);
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
|
||||
let i = path.length - 1;
|
||||
const value = path[i].node._value;
|
||||
if (value === unset) {
|
||||
return; // not actually a real node
|
||||
}
|
||||
|
||||
this._size--;
|
||||
path[i].node._value = unset;
|
||||
|
||||
for (; i > 0; i--) {
|
||||
const { node, part } = path[i];
|
||||
if (node.children?.size || node._value !== unset) {
|
||||
break;
|
||||
}
|
||||
|
||||
path[i - 1].node.children!.delete(part);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Deletes a subtree from the prefix tree, returning the values they contained. */
|
||||
*deleteRecursive(key: Iterable<string>): Iterable<V> {
|
||||
const path = this.getPathToKey(key);
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subtree = path[path.length - 1].node;
|
||||
|
||||
// important: run the deletion before we start to yield results, so that
|
||||
// it still runs even if the caller doesn't consumer the iterator
|
||||
for (let i = path.length - 1; i > 0; i--) {
|
||||
const parent = path[i - 1];
|
||||
parent.node.children!.delete(path[i].part);
|
||||
if (parent.node.children!.size > 0 || parent.node._value !== unset) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of bfsIterate(subtree)) {
|
||||
if (node._value !== unset) {
|
||||
this._size--;
|
||||
yield node._value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets a value from the tree. */
|
||||
find(key: Iterable<string>): V | undefined {
|
||||
let node = this.root;
|
||||
for (const segment of key) {
|
||||
const next = node.children?.get(segment);
|
||||
if (!next) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
node = next;
|
||||
}
|
||||
|
||||
return node._value === unset ? undefined : node._value;
|
||||
}
|
||||
|
||||
/** Gets whether the tree has the key, or a parent of the key, already inserted. */
|
||||
hasKeyOrParent(key: Iterable<string>): boolean {
|
||||
let node = this.root;
|
||||
for (const segment of key) {
|
||||
const next = node.children?.get(segment);
|
||||
if (!next) {
|
||||
return false;
|
||||
}
|
||||
if (next._value !== unset) {
|
||||
return true;
|
||||
}
|
||||
|
||||
node = next;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Gets whether the tree has the given key or any children. */
|
||||
hasKeyOrChildren(key: Iterable<string>): boolean {
|
||||
let node = this.root;
|
||||
for (const segment of key) {
|
||||
const next = node.children?.get(segment);
|
||||
if (!next) {
|
||||
return false;
|
||||
}
|
||||
|
||||
node = next;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Gets whether the tree has the given key. */
|
||||
hasKey(key: Iterable<string>): boolean {
|
||||
let node = this.root;
|
||||
for (const segment of key) {
|
||||
const next = node.children?.get(segment);
|
||||
if (!next) {
|
||||
return false;
|
||||
}
|
||||
|
||||
node = next;
|
||||
}
|
||||
|
||||
return node._value !== unset;
|
||||
}
|
||||
|
||||
private getPathToKey(key: Iterable<string>) {
|
||||
const path = [{ part: '', node: this.root }];
|
||||
let i = 0;
|
||||
for (const part of key) {
|
||||
const node = path[i].node.children?.get(part);
|
||||
if (!node) {
|
||||
return; // node not in tree
|
||||
}
|
||||
|
||||
path.push({ part, node });
|
||||
i++;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private opNode(key: Iterable<string>, fn: (node: Node<V>) => void, onDescend?: (node: Node<V>) => void): void {
|
||||
let node = this.root;
|
||||
for (const part of key) {
|
||||
if (!node.children) {
|
||||
const next = new Node<V>();
|
||||
node.children = new Map([[part, next]]);
|
||||
node = next;
|
||||
} else if (!node.children.has(part)) {
|
||||
const next = new Node<V>();
|
||||
node.children.set(part, next);
|
||||
node = next;
|
||||
} else {
|
||||
node = node.children.get(part)!;
|
||||
}
|
||||
onDescend?.(node);
|
||||
}
|
||||
|
||||
const sizeBefore = node._value === unset ? 0 : 1;
|
||||
fn(node);
|
||||
const sizeAfter = node._value === unset ? 0 : 1;
|
||||
this._size += sizeAfter - sizeBefore;
|
||||
}
|
||||
|
||||
/** Returns an iterable of the tree values in no defined order. */
|
||||
*values() {
|
||||
for (const { _value } of bfsIterate(this.root)) {
|
||||
if (_value !== unset) {
|
||||
yield _value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function* bfsIterate<T>(root: Node<T>): Iterable<Node<T>> {
|
||||
const stack = [root];
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
yield node;
|
||||
|
||||
if (node.children) {
|
||||
for (const child of node.children.values()) {
|
||||
stack.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Node<T> implements IPrefixTreeNode<T> {
|
||||
public children?: Map<string, Node<T>>;
|
||||
|
||||
public get value() {
|
||||
return this._value === unset ? undefined : this._value;
|
||||
}
|
||||
|
||||
public set value(value: T | undefined) {
|
||||
this._value = value === undefined ? unset : value;
|
||||
}
|
||||
|
||||
public _value: T | typeof unset = unset;
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { INodeProcess, isMacintosh, isWindows } from 'vs/base/common/platform';
|
||||
|
||||
let safeProcess: Omit<INodeProcess, 'arch'> & { arch: string | undefined };
|
||||
declare const process: INodeProcess;
|
||||
|
||||
// Native sandbox environment
|
||||
const vscodeGlobal = (globalThis as any).vscode;
|
||||
if (typeof vscodeGlobal !== 'undefined' && typeof vscodeGlobal.process !== 'undefined') {
|
||||
const sandboxProcess: INodeProcess = vscodeGlobal.process;
|
||||
safeProcess = {
|
||||
get platform() { return sandboxProcess.platform; },
|
||||
get arch() { return sandboxProcess.arch; },
|
||||
get env() { return sandboxProcess.env; },
|
||||
cwd() { return sandboxProcess.cwd(); }
|
||||
};
|
||||
}
|
||||
|
||||
// Native node.js environment
|
||||
else if (typeof process !== 'undefined') {
|
||||
safeProcess = {
|
||||
get platform() { return process.platform; },
|
||||
get arch() { return process.arch; },
|
||||
get env() { return process.env; },
|
||||
cwd() { return process.env['VSCODE_CWD'] || process.cwd(); }
|
||||
};
|
||||
}
|
||||
|
||||
// Web environment
|
||||
else {
|
||||
safeProcess = {
|
||||
|
||||
// Supported
|
||||
get platform() { return isWindows ? 'win32' : isMacintosh ? 'darwin' : 'linux'; },
|
||||
get arch() { return undefined; /* arch is undefined in web */ },
|
||||
|
||||
// Unsupported
|
||||
get env() { return {}; },
|
||||
cwd() { return '/'; }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides safe access to the `cwd` property in node.js, sandboxed or web
|
||||
* environments.
|
||||
*
|
||||
* Note: in web, this property is hardcoded to be `/`.
|
||||
*
|
||||
* @skipMangle
|
||||
*/
|
||||
export const cwd = safeProcess.cwd;
|
||||
|
||||
/**
|
||||
* Provides safe access to the `env` property in node.js, sandboxed or web
|
||||
* environments.
|
||||
*
|
||||
* Note: in web, this property is hardcoded to be `{}`.
|
||||
*/
|
||||
export const env = safeProcess.env;
|
||||
|
||||
/**
|
||||
* Provides safe access to the `platform` property in node.js, sandboxed or web
|
||||
* environments.
|
||||
*/
|
||||
export const platform = safeProcess.platform;
|
||||
|
||||
/**
|
||||
* Provides safe access to the `arch` method in node.js, sandboxed or web
|
||||
* environments.
|
||||
* Note: `arch` is `undefined` in web
|
||||
*/
|
||||
export const arch = safeProcess.arch;
|
||||
@@ -1,148 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IProcessEnvironment, isLinux } from 'vs/base/common/platform';
|
||||
|
||||
/**
|
||||
* Options to be passed to the external program or shell.
|
||||
*/
|
||||
export interface CommandOptions {
|
||||
/**
|
||||
* The current working directory of the executed program or shell.
|
||||
* If omitted VSCode's current workspace root is used.
|
||||
*/
|
||||
cwd?: string;
|
||||
|
||||
/**
|
||||
* The environment of the executed program or shell. If omitted
|
||||
* the parent process' environment is used.
|
||||
*/
|
||||
env?: { [key: string]: string };
|
||||
}
|
||||
|
||||
export interface Executable {
|
||||
/**
|
||||
* The command to be executed. Can be an external program or a shell
|
||||
* command.
|
||||
*/
|
||||
command: string;
|
||||
|
||||
/**
|
||||
* Specifies whether the command is a shell command and therefore must
|
||||
* be executed in a shell interpreter (e.g. cmd.exe, bash, ...).
|
||||
*/
|
||||
isShellCommand: boolean;
|
||||
|
||||
/**
|
||||
* The arguments passed to the command.
|
||||
*/
|
||||
args: string[];
|
||||
|
||||
/**
|
||||
* The command options used when the command is executed. Can be omitted.
|
||||
*/
|
||||
options?: CommandOptions;
|
||||
}
|
||||
|
||||
export interface ForkOptions extends CommandOptions {
|
||||
execArgv?: string[];
|
||||
}
|
||||
|
||||
export const enum Source {
|
||||
stdout,
|
||||
stderr
|
||||
}
|
||||
|
||||
/**
|
||||
* The data send via a success callback
|
||||
*/
|
||||
export interface SuccessData {
|
||||
error?: Error;
|
||||
cmdCode?: number;
|
||||
terminated?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The data send via a error callback
|
||||
*/
|
||||
export interface ErrorData {
|
||||
error?: Error;
|
||||
terminated?: boolean;
|
||||
stdout?: string;
|
||||
stderr?: string;
|
||||
}
|
||||
|
||||
export interface TerminateResponse {
|
||||
success: boolean;
|
||||
code?: TerminateResponseCode;
|
||||
error?: any;
|
||||
}
|
||||
|
||||
export const enum TerminateResponseCode {
|
||||
Success = 0,
|
||||
Unknown = 1,
|
||||
AccessDenied = 2,
|
||||
ProcessNotFound = 3,
|
||||
}
|
||||
|
||||
export interface ProcessItem {
|
||||
name: string;
|
||||
cmd: string;
|
||||
pid: number;
|
||||
ppid: number;
|
||||
load: number;
|
||||
mem: number;
|
||||
|
||||
children?: ProcessItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a VS Code process environment by removing all Electron/VS Code-related values.
|
||||
*/
|
||||
export function sanitizeProcessEnvironment(env: IProcessEnvironment, ...preserve: string[]): void {
|
||||
const set = preserve.reduce<Record<string, boolean>>((set, key) => {
|
||||
set[key] = true;
|
||||
return set;
|
||||
}, {});
|
||||
const keysToRemove = [
|
||||
/^ELECTRON_.+$/,
|
||||
/^VSCODE_(?!(PORTABLE|SHELL_LOGIN|ENV_REPLACE|ENV_APPEND|ENV_PREPEND)).+$/,
|
||||
/^SNAP(|_.*)$/,
|
||||
/^GDK_PIXBUF_.+$/,
|
||||
];
|
||||
const envKeys = Object.keys(env);
|
||||
envKeys
|
||||
.filter(key => !set[key])
|
||||
.forEach(envKey => {
|
||||
for (let i = 0; i < keysToRemove.length; i++) {
|
||||
if (envKey.search(keysToRemove[i]) !== -1) {
|
||||
delete env[envKey];
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove dangerous environment variables that have caused crashes
|
||||
* in forked processes (i.e. in ELECTRON_RUN_AS_NODE processes)
|
||||
*
|
||||
* @param env The env object to change
|
||||
*/
|
||||
export function removeDangerousEnvVariables(env: IProcessEnvironment | undefined): void {
|
||||
if (!env) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unset `DEBUG`, as an invalid value might lead to process crashes
|
||||
// See https://github.com/microsoft/vscode/issues/130072
|
||||
delete env['DEBUG'];
|
||||
|
||||
if (isLinux) {
|
||||
// Unset `LD_PRELOAD`, as it might lead to process crashes
|
||||
// See https://github.com/microsoft/vscode/issues/134177
|
||||
delete env['LD_PRELOAD'];
|
||||
}
|
||||
}
|
||||
@@ -1,314 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { PlatformName } from 'vs/base/common/platform';
|
||||
|
||||
export interface IBuiltInExtension {
|
||||
readonly name: string;
|
||||
readonly version: string;
|
||||
readonly repo: string;
|
||||
readonly metadata: any;
|
||||
}
|
||||
|
||||
export interface IProductWalkthrough {
|
||||
id: string;
|
||||
steps: IProductWalkthroughStep[];
|
||||
}
|
||||
|
||||
export interface IProductWalkthroughStep {
|
||||
id: string;
|
||||
title: string;
|
||||
when: string;
|
||||
description: string;
|
||||
media:
|
||||
| { type: 'image'; path: string | { hc: string; hcLight?: string; light: string; dark: string }; altText: string }
|
||||
| { type: 'svg'; path: string; altText: string }
|
||||
| { type: 'markdown'; path: string };
|
||||
}
|
||||
|
||||
export interface IFeaturedExtension {
|
||||
readonly id: string;
|
||||
readonly title: string;
|
||||
readonly description: string;
|
||||
readonly imagePath: string;
|
||||
}
|
||||
|
||||
export type ConfigurationSyncStore = {
|
||||
url: string;
|
||||
insidersUrl: string;
|
||||
stableUrl: string;
|
||||
canSwitch?: boolean;
|
||||
authenticationProviders: IStringDictionary<{ scopes: string[] }>;
|
||||
};
|
||||
|
||||
export type ExtensionUntrustedWorkspaceSupport = {
|
||||
readonly default?: boolean | 'limited';
|
||||
readonly override?: boolean | 'limited';
|
||||
};
|
||||
|
||||
export type ExtensionVirtualWorkspaceSupport = {
|
||||
readonly default?: boolean;
|
||||
readonly override?: boolean;
|
||||
};
|
||||
|
||||
export interface IProductConfiguration {
|
||||
readonly version: string;
|
||||
readonly date?: string;
|
||||
readonly quality?: string;
|
||||
readonly commit?: string;
|
||||
|
||||
readonly nameShort: string;
|
||||
readonly nameLong: string;
|
||||
|
||||
readonly win32AppUserModelId?: string;
|
||||
readonly win32MutexName?: string;
|
||||
readonly win32RegValueName?: string;
|
||||
readonly applicationName: string;
|
||||
readonly embedderIdentifier?: string;
|
||||
|
||||
readonly urlProtocol: string;
|
||||
readonly dataFolderName: string; // location for extensions (e.g. ~/.vscode-insiders)
|
||||
|
||||
readonly builtInExtensions?: IBuiltInExtension[];
|
||||
readonly walkthroughMetadata?: IProductWalkthrough[];
|
||||
readonly featuredExtensions?: IFeaturedExtension[];
|
||||
|
||||
readonly downloadUrl?: string;
|
||||
readonly updateUrl?: string;
|
||||
readonly webUrl?: string;
|
||||
readonly webEndpointUrlTemplate?: string;
|
||||
readonly webviewContentExternalBaseUrlTemplate?: string;
|
||||
readonly target?: string;
|
||||
readonly nlsCoreBaseUrl?: string;
|
||||
|
||||
readonly settingsSearchBuildId?: number;
|
||||
readonly settingsSearchUrl?: string;
|
||||
|
||||
readonly tasConfig?: {
|
||||
endpoint: string;
|
||||
telemetryEventName: string;
|
||||
assignmentContextTelemetryPropertyName: string;
|
||||
};
|
||||
|
||||
readonly extensionsGallery?: {
|
||||
readonly serviceUrl: string;
|
||||
readonly servicePPEUrl?: string;
|
||||
readonly searchUrl?: string;
|
||||
readonly itemUrl: string;
|
||||
readonly publisherUrl: string;
|
||||
readonly resourceUrlTemplate: string;
|
||||
readonly controlUrl: string;
|
||||
readonly nlsBaseUrl: string;
|
||||
};
|
||||
|
||||
readonly extensionRecommendations?: IStringDictionary<IExtensionRecommendations>;
|
||||
readonly configBasedExtensionTips?: IStringDictionary<IConfigBasedExtensionTip>;
|
||||
readonly exeBasedExtensionTips?: IStringDictionary<IExeBasedExtensionTip>;
|
||||
readonly remoteExtensionTips?: IStringDictionary<IRemoteExtensionTip>;
|
||||
readonly virtualWorkspaceExtensionTips?: IStringDictionary<IVirtualWorkspaceExtensionTip>;
|
||||
readonly extensionKeywords?: IStringDictionary<string[]>;
|
||||
readonly keymapExtensionTips?: readonly string[];
|
||||
readonly webExtensionTips?: readonly string[];
|
||||
readonly languageExtensionTips?: readonly string[];
|
||||
readonly trustedExtensionUrlPublicKeys?: IStringDictionary<string[]>;
|
||||
readonly trustedExtensionAuthAccess?: string[] | IStringDictionary<string[]>;
|
||||
readonly trustedExtensionProtocolHandlers?: readonly string[];
|
||||
|
||||
readonly commandPaletteSuggestedCommandIds?: string[];
|
||||
|
||||
readonly crashReporter?: {
|
||||
readonly companyName: string;
|
||||
readonly productName: string;
|
||||
};
|
||||
|
||||
readonly removeTelemetryMachineId?: boolean;
|
||||
readonly enabledTelemetryLevels?: { error: boolean; usage: boolean };
|
||||
readonly enableTelemetry?: boolean;
|
||||
readonly openToWelcomeMainPage?: boolean;
|
||||
readonly aiConfig?: {
|
||||
readonly ariaKey: string;
|
||||
};
|
||||
|
||||
readonly documentationUrl?: string;
|
||||
readonly serverDocumentationUrl?: string;
|
||||
readonly releaseNotesUrl?: string;
|
||||
readonly keyboardShortcutsUrlMac?: string;
|
||||
readonly keyboardShortcutsUrlLinux?: string;
|
||||
readonly keyboardShortcutsUrlWin?: string;
|
||||
readonly introductoryVideosUrl?: string;
|
||||
readonly tipsAndTricksUrl?: string;
|
||||
readonly newsletterSignupUrl?: string;
|
||||
readonly youTubeUrl?: string;
|
||||
readonly requestFeatureUrl?: string;
|
||||
readonly reportIssueUrl?: string;
|
||||
readonly reportMarketplaceIssueUrl?: string;
|
||||
readonly licenseUrl?: string;
|
||||
readonly serverLicenseUrl?: string;
|
||||
readonly privacyStatementUrl?: string;
|
||||
readonly showTelemetryOptOut?: boolean;
|
||||
|
||||
readonly serverGreeting?: string[];
|
||||
readonly serverLicense?: string[];
|
||||
readonly serverLicensePrompt?: string;
|
||||
readonly serverApplicationName: string;
|
||||
readonly serverDataFolderName?: string;
|
||||
|
||||
readonly tunnelApplicationName?: string;
|
||||
readonly tunnelApplicationConfig?: ITunnelApplicationConfig;
|
||||
|
||||
readonly npsSurveyUrl?: string;
|
||||
readonly cesSurveyUrl?: string;
|
||||
readonly surveys?: readonly ISurveyData[];
|
||||
|
||||
readonly checksums?: { [path: string]: string };
|
||||
readonly checksumFailMoreInfoUrl?: string;
|
||||
|
||||
readonly appCenter?: IAppCenterConfiguration;
|
||||
|
||||
readonly portable?: string;
|
||||
|
||||
readonly extensionKind?: { readonly [extensionId: string]: ('ui' | 'workspace' | 'web')[] };
|
||||
readonly extensionPointExtensionKind?: { readonly [extensionPointId: string]: ('ui' | 'workspace' | 'web')[] };
|
||||
readonly extensionSyncedKeys?: { readonly [extensionId: string]: string[] };
|
||||
|
||||
readonly extensionsEnabledWithApiProposalVersion?: string[];
|
||||
readonly extensionEnabledApiProposals?: { readonly [extensionId: string]: string[] };
|
||||
readonly extensionUntrustedWorkspaceSupport?: { readonly [extensionId: string]: ExtensionUntrustedWorkspaceSupport };
|
||||
readonly extensionVirtualWorkspacesSupport?: { readonly [extensionId: string]: ExtensionVirtualWorkspaceSupport };
|
||||
|
||||
readonly msftInternalDomains?: string[];
|
||||
readonly linkProtectionTrustedDomains?: readonly string[];
|
||||
|
||||
readonly 'configurationSync.store'?: ConfigurationSyncStore;
|
||||
|
||||
readonly 'editSessions.store'?: Omit<ConfigurationSyncStore, 'insidersUrl' | 'stableUrl'>;
|
||||
readonly darwinUniversalAssetId?: string;
|
||||
readonly profileTemplatesUrl?: string;
|
||||
|
||||
readonly commonlyUsedSettings?: string[];
|
||||
readonly aiGeneratedWorkspaceTrust?: IAiGeneratedWorkspaceTrust;
|
||||
readonly gitHubEntitlement?: IGitHubEntitlement;
|
||||
readonly chatWelcomeView?: IChatWelcomeView;
|
||||
readonly chatParticipantRegistry?: string;
|
||||
}
|
||||
|
||||
export interface ITunnelApplicationConfig {
|
||||
authenticationProviders: IStringDictionary<{ scopes: string[] }>;
|
||||
editorWebUrl: string;
|
||||
extension: IRemoteExtensionTip;
|
||||
}
|
||||
|
||||
export interface IExtensionRecommendations {
|
||||
readonly onFileOpen: IFileOpenCondition[];
|
||||
readonly onSettingsEditorOpen?: ISettingsEditorOpenCondition;
|
||||
}
|
||||
|
||||
export interface ISettingsEditorOpenCondition {
|
||||
readonly prerelease?: boolean | string;
|
||||
}
|
||||
|
||||
export interface IExtensionRecommendationCondition {
|
||||
readonly important?: boolean;
|
||||
readonly whenInstalled?: string[];
|
||||
readonly whenNotInstalled?: string[];
|
||||
}
|
||||
|
||||
export type IFileOpenCondition = IFileLanguageCondition | IFilePathCondition | IFileContentCondition;
|
||||
|
||||
export interface IFileLanguageCondition extends IExtensionRecommendationCondition {
|
||||
readonly languages: string[];
|
||||
}
|
||||
|
||||
export interface IFilePathCondition extends IExtensionRecommendationCondition {
|
||||
readonly pathGlob: string;
|
||||
}
|
||||
|
||||
export type IFileContentCondition = (IFileLanguageCondition | IFilePathCondition) & { readonly contentPattern: string };
|
||||
|
||||
export interface IAppCenterConfiguration {
|
||||
readonly 'win32-x64': string;
|
||||
readonly 'win32-arm64': string;
|
||||
readonly 'linux-x64': string;
|
||||
readonly 'darwin': string;
|
||||
readonly 'darwin-universal': string;
|
||||
readonly 'darwin-arm64': string;
|
||||
}
|
||||
|
||||
export interface IConfigBasedExtensionTip {
|
||||
configPath: string;
|
||||
configName: string;
|
||||
configScheme?: string;
|
||||
recommendations: IStringDictionary<{
|
||||
name: string;
|
||||
contentPattern?: string;
|
||||
important?: boolean;
|
||||
isExtensionPack?: boolean;
|
||||
whenNotInstalled?: string[];
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface IExeBasedExtensionTip {
|
||||
friendlyName: string;
|
||||
windowsPath?: string;
|
||||
important?: boolean;
|
||||
recommendations: IStringDictionary<{ name: string; important?: boolean; isExtensionPack?: boolean; whenNotInstalled?: string[] }>;
|
||||
}
|
||||
|
||||
export interface IRemoteExtensionTip {
|
||||
friendlyName: string;
|
||||
extensionId: string;
|
||||
supportedPlatforms?: PlatformName[];
|
||||
startEntry?: {
|
||||
helpLink: string;
|
||||
startConnectLabel: string;
|
||||
startCommand: string;
|
||||
priority: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IVirtualWorkspaceExtensionTip {
|
||||
friendlyName: string;
|
||||
extensionId: string;
|
||||
supportedPlatforms?: PlatformName[];
|
||||
startEntry: {
|
||||
helpLink: string;
|
||||
startConnectLabel: string;
|
||||
startCommand: string;
|
||||
priority: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ISurveyData {
|
||||
surveyId: string;
|
||||
surveyUrl: string;
|
||||
languageId: string;
|
||||
editCount: number;
|
||||
userProbability: number;
|
||||
}
|
||||
|
||||
export interface IAiGeneratedWorkspaceTrust {
|
||||
readonly title: string;
|
||||
readonly checkboxText: string;
|
||||
readonly trustOption: string;
|
||||
readonly dontTrustOption: string;
|
||||
readonly startupTrustRequestLearnMore: string;
|
||||
}
|
||||
|
||||
export interface IGitHubEntitlement {
|
||||
providerId: string;
|
||||
command: { title: string; titleWithoutPlaceHolder: string; action: string; when: string };
|
||||
entitlementUrl: string;
|
||||
extensionId: string;
|
||||
enablementKey: string;
|
||||
confirmationMessage: string;
|
||||
confirmationAction: string;
|
||||
}
|
||||
|
||||
export interface IChatWelcomeView {
|
||||
welcomeViewId: string;
|
||||
welcomeViewTitle: string;
|
||||
welcomeViewContent: string;
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface IRange {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface IRangedGroup {
|
||||
range: IRange;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export namespace Range {
|
||||
|
||||
/**
|
||||
* Returns the intersection between two ranges as a range itself.
|
||||
* Returns `{ start: 0, end: 0 }` if the intersection is empty.
|
||||
*/
|
||||
export function intersect(one: IRange, other: IRange): IRange {
|
||||
if (one.start >= other.end || other.start >= one.end) {
|
||||
return { start: 0, end: 0 };
|
||||
}
|
||||
|
||||
const start = Math.max(one.start, other.start);
|
||||
const end = Math.min(one.end, other.end);
|
||||
|
||||
if (end - start <= 0) {
|
||||
return { start: 0, end: 0 };
|
||||
}
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
export function isEmpty(range: IRange): boolean {
|
||||
return range.end - range.start <= 0;
|
||||
}
|
||||
|
||||
export function intersects(one: IRange, other: IRange): boolean {
|
||||
return !isEmpty(intersect(one, other));
|
||||
}
|
||||
|
||||
export function relativeComplement(one: IRange, other: IRange): IRange[] {
|
||||
const result: IRange[] = [];
|
||||
const first = { start: one.start, end: Math.min(other.start, one.end) };
|
||||
const second = { start: Math.max(other.end, one.start), end: one.end };
|
||||
|
||||
if (!isEmpty(first)) {
|
||||
result.push(first);
|
||||
}
|
||||
|
||||
if (!isEmpty(second)) {
|
||||
result.push(second);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as strings from './strings';
|
||||
|
||||
export function buildReplaceStringWithCasePreserved(matches: string[] | null, pattern: string): string {
|
||||
if (matches && (matches[0] !== '')) {
|
||||
const containsHyphens = validateSpecificSpecialCharacter(matches, pattern, '-');
|
||||
const containsUnderscores = validateSpecificSpecialCharacter(matches, pattern, '_');
|
||||
if (containsHyphens && !containsUnderscores) {
|
||||
return buildReplaceStringForSpecificSpecialCharacter(matches, pattern, '-');
|
||||
} else if (!containsHyphens && containsUnderscores) {
|
||||
return buildReplaceStringForSpecificSpecialCharacter(matches, pattern, '_');
|
||||
}
|
||||
if (matches[0].toUpperCase() === matches[0]) {
|
||||
return pattern.toUpperCase();
|
||||
} else if (matches[0].toLowerCase() === matches[0]) {
|
||||
return pattern.toLowerCase();
|
||||
} else if (strings.containsUppercaseCharacter(matches[0][0]) && pattern.length > 0) {
|
||||
return pattern[0].toUpperCase() + pattern.substr(1);
|
||||
} else if (matches[0][0].toUpperCase() !== matches[0][0] && pattern.length > 0) {
|
||||
return pattern[0].toLowerCase() + pattern.substr(1);
|
||||
} else {
|
||||
// we don't understand its pattern yet.
|
||||
return pattern;
|
||||
}
|
||||
} else {
|
||||
return pattern;
|
||||
}
|
||||
}
|
||||
|
||||
function validateSpecificSpecialCharacter(matches: string[], pattern: string, specialCharacter: string): boolean {
|
||||
const doesContainSpecialCharacter = matches[0].indexOf(specialCharacter) !== -1 && pattern.indexOf(specialCharacter) !== -1;
|
||||
return doesContainSpecialCharacter && matches[0].split(specialCharacter).length === pattern.split(specialCharacter).length;
|
||||
}
|
||||
|
||||
function buildReplaceStringForSpecificSpecialCharacter(matches: string[], pattern: string, specialCharacter: string): string {
|
||||
const splitPatternAtSpecialCharacter = pattern.split(specialCharacter);
|
||||
const splitMatchAtSpecialCharacter = matches[0].split(specialCharacter);
|
||||
let replaceString: string = '';
|
||||
splitPatternAtSpecialCharacter.forEach((splitValue, index) => {
|
||||
replaceString += buildReplaceStringWithCasePreserved([splitMatchAtSpecialCharacter[index]], splitValue) + specialCharacter;
|
||||
});
|
||||
|
||||
return replaceString.slice(0, -1);
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
|
||||
enum Severity {
|
||||
Ignore = 0,
|
||||
Info = 1,
|
||||
Warning = 2,
|
||||
Error = 3
|
||||
}
|
||||
|
||||
namespace Severity {
|
||||
|
||||
const _error = 'error';
|
||||
const _warning = 'warning';
|
||||
const _warn = 'warn';
|
||||
const _info = 'info';
|
||||
const _ignore = 'ignore';
|
||||
|
||||
/**
|
||||
* Parses 'error', 'warning', 'warn', 'info' in call casings
|
||||
* and falls back to ignore.
|
||||
*/
|
||||
export function fromValue(value: string): Severity {
|
||||
if (!value) {
|
||||
return Severity.Ignore;
|
||||
}
|
||||
|
||||
if (strings.equalsIgnoreCase(_error, value)) {
|
||||
return Severity.Error;
|
||||
}
|
||||
|
||||
if (strings.equalsIgnoreCase(_warning, value) || strings.equalsIgnoreCase(_warn, value)) {
|
||||
return Severity.Warning;
|
||||
}
|
||||
|
||||
if (strings.equalsIgnoreCase(_info, value)) {
|
||||
return Severity.Info;
|
||||
}
|
||||
return Severity.Ignore;
|
||||
}
|
||||
|
||||
export function toString(severity: Severity): string {
|
||||
switch (severity) {
|
||||
case Severity.Error: return _error;
|
||||
case Severity.Warning: return _warning;
|
||||
case Severity.Info: return _info;
|
||||
default: return _ignore;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Severity;
|
||||
@@ -1,204 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
class Node<K, V> {
|
||||
readonly forward: Node<K, V>[];
|
||||
constructor(readonly level: number, readonly key: K, public value: V) {
|
||||
this.forward = [];
|
||||
}
|
||||
}
|
||||
|
||||
const NIL: undefined = undefined;
|
||||
|
||||
interface Comparator<K> {
|
||||
(a: K, b: K): number;
|
||||
}
|
||||
|
||||
export class SkipList<K, V> implements Map<K, V> {
|
||||
|
||||
readonly [Symbol.toStringTag] = 'SkipList';
|
||||
|
||||
private _maxLevel: number;
|
||||
private _level: number = 0;
|
||||
private _header: Node<K, V>;
|
||||
private _size: number = 0;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param capacity Capacity at which the list performs best
|
||||
*/
|
||||
constructor(
|
||||
readonly comparator: (a: K, b: K) => number,
|
||||
capacity: number = 2 ** 16
|
||||
) {
|
||||
this._maxLevel = Math.max(1, Math.log2(capacity) | 0);
|
||||
this._header = <any>new Node(this._maxLevel, NIL, NIL);
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this._header = <any>new Node(this._maxLevel, NIL, NIL);
|
||||
this._size = 0;
|
||||
}
|
||||
|
||||
has(key: K): boolean {
|
||||
return Boolean(SkipList._search(this, key, this.comparator));
|
||||
}
|
||||
|
||||
get(key: K): V | undefined {
|
||||
return SkipList._search(this, key, this.comparator)?.value;
|
||||
}
|
||||
|
||||
set(key: K, value: V): this {
|
||||
if (SkipList._insert(this, key, value, this.comparator)) {
|
||||
this._size += 1;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
delete(key: K): boolean {
|
||||
const didDelete = SkipList._delete(this, key, this.comparator);
|
||||
if (didDelete) {
|
||||
this._size -= 1;
|
||||
}
|
||||
return didDelete;
|
||||
}
|
||||
|
||||
// --- iteration
|
||||
|
||||
forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void {
|
||||
let node = this._header.forward[0];
|
||||
while (node) {
|
||||
callbackfn.call(thisArg, node.value, node.key, this);
|
||||
node = node.forward[0];
|
||||
}
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<[K, V]> {
|
||||
return this.entries();
|
||||
}
|
||||
|
||||
*entries(): IterableIterator<[K, V]> {
|
||||
let node = this._header.forward[0];
|
||||
while (node) {
|
||||
yield [node.key, node.value];
|
||||
node = node.forward[0];
|
||||
}
|
||||
}
|
||||
|
||||
*keys(): IterableIterator<K> {
|
||||
let node = this._header.forward[0];
|
||||
while (node) {
|
||||
yield node.key;
|
||||
node = node.forward[0];
|
||||
}
|
||||
}
|
||||
|
||||
*values(): IterableIterator<V> {
|
||||
let node = this._header.forward[0];
|
||||
while (node) {
|
||||
yield node.value;
|
||||
node = node.forward[0];
|
||||
}
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
// debug string...
|
||||
let result = '[SkipList]:';
|
||||
let node = this._header.forward[0];
|
||||
while (node) {
|
||||
result += `node(${node.key}, ${node.value}, lvl:${node.level})`;
|
||||
node = node.forward[0];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// from https://www.epaperpress.com/sortsearch/download/skiplist.pdf
|
||||
|
||||
private static _search<K, V>(list: SkipList<K, V>, searchKey: K, comparator: Comparator<K>) {
|
||||
let x = list._header;
|
||||
for (let i = list._level - 1; i >= 0; i--) {
|
||||
while (x.forward[i] && comparator(x.forward[i].key, searchKey) < 0) {
|
||||
x = x.forward[i];
|
||||
}
|
||||
}
|
||||
x = x.forward[0];
|
||||
if (x && comparator(x.key, searchKey) === 0) {
|
||||
return x;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private static _insert<K, V>(list: SkipList<K, V>, searchKey: K, value: V, comparator: Comparator<K>) {
|
||||
const update: Node<K, V>[] = [];
|
||||
let x = list._header;
|
||||
for (let i = list._level - 1; i >= 0; i--) {
|
||||
while (x.forward[i] && comparator(x.forward[i].key, searchKey) < 0) {
|
||||
x = x.forward[i];
|
||||
}
|
||||
update[i] = x;
|
||||
}
|
||||
x = x.forward[0];
|
||||
if (x && comparator(x.key, searchKey) === 0) {
|
||||
// update
|
||||
x.value = value;
|
||||
return false;
|
||||
} else {
|
||||
// insert
|
||||
const lvl = SkipList._randomLevel(list);
|
||||
if (lvl > list._level) {
|
||||
for (let i = list._level; i < lvl; i++) {
|
||||
update[i] = list._header;
|
||||
}
|
||||
list._level = lvl;
|
||||
}
|
||||
x = new Node<K, V>(lvl, searchKey, value);
|
||||
for (let i = 0; i < lvl; i++) {
|
||||
x.forward[i] = update[i].forward[i];
|
||||
update[i].forward[i] = x;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static _randomLevel(list: SkipList<any, any>, p: number = 0.5): number {
|
||||
let lvl = 1;
|
||||
while (Math.random() < p && lvl < list._maxLevel) {
|
||||
lvl += 1;
|
||||
}
|
||||
return lvl;
|
||||
}
|
||||
|
||||
private static _delete<K, V>(list: SkipList<K, V>, searchKey: K, comparator: Comparator<K>) {
|
||||
const update: Node<K, V>[] = [];
|
||||
let x = list._header;
|
||||
for (let i = list._level - 1; i >= 0; i--) {
|
||||
while (x.forward[i] && comparator(x.forward[i].key, searchKey) < 0) {
|
||||
x = x.forward[i];
|
||||
}
|
||||
update[i] = x;
|
||||
}
|
||||
x = x.forward[0];
|
||||
if (!x || comparator(x.key, searchKey) !== 0) {
|
||||
// not found
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < list._level; i++) {
|
||||
if (update[i].forward[i] !== x) {
|
||||
break;
|
||||
}
|
||||
update[i].forward[i] = x.forward[i];
|
||||
}
|
||||
while (list._level > 0 && list._header.forward[list._level - 1] === NIL) {
|
||||
list._level -= 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,772 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
/**
|
||||
* The payload that flows in readable stream events.
|
||||
*/
|
||||
export type ReadableStreamEventPayload<T> = T | Error | 'end';
|
||||
|
||||
export interface ReadableStreamEvents<T> {
|
||||
|
||||
/**
|
||||
* The 'data' event is emitted whenever the stream is
|
||||
* relinquishing ownership of a chunk of data to a consumer.
|
||||
*
|
||||
* NOTE: PLEASE UNDERSTAND THAT ADDING A DATA LISTENER CAN
|
||||
* TURN THE STREAM INTO FLOWING MODE. IT IS THEREFOR THE
|
||||
* LAST LISTENER THAT SHOULD BE ADDED AND NOT THE FIRST
|
||||
*
|
||||
* Use `listenStream` as a helper method to listen to
|
||||
* stream events in the right order.
|
||||
*/
|
||||
on(event: 'data', callback: (data: T) => void): void;
|
||||
|
||||
/**
|
||||
* Emitted when any error occurs.
|
||||
*/
|
||||
on(event: 'error', callback: (err: Error) => void): void;
|
||||
|
||||
/**
|
||||
* The 'end' event is emitted when there is no more data
|
||||
* to be consumed from the stream. The 'end' event will
|
||||
* not be emitted unless the data is completely consumed.
|
||||
*/
|
||||
on(event: 'end', callback: () => void): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A interface that emulates the API shape of a node.js readable
|
||||
* stream for use in native and web environments.
|
||||
*/
|
||||
export interface ReadableStream<T> extends ReadableStreamEvents<T> {
|
||||
|
||||
/**
|
||||
* Stops emitting any events until resume() is called.
|
||||
*/
|
||||
pause(): void;
|
||||
|
||||
/**
|
||||
* Starts emitting events again after pause() was called.
|
||||
*/
|
||||
resume(): void;
|
||||
|
||||
/**
|
||||
* Destroys the stream and stops emitting any event.
|
||||
*/
|
||||
destroy(): void;
|
||||
|
||||
/**
|
||||
* Allows to remove a listener that was previously added.
|
||||
*/
|
||||
removeListener(event: string, callback: Function): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A interface that emulates the API shape of a node.js readable
|
||||
* for use in native and web environments.
|
||||
*/
|
||||
export interface Readable<T> {
|
||||
|
||||
/**
|
||||
* Read data from the underlying source. Will return
|
||||
* null to indicate that no more data can be read.
|
||||
*/
|
||||
read(): T | null;
|
||||
}
|
||||
|
||||
export function isReadable<T>(obj: unknown): obj is Readable<T> {
|
||||
const candidate = obj as Readable<T> | undefined;
|
||||
if (!candidate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return typeof candidate.read === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* A interface that emulates the API shape of a node.js writeable
|
||||
* stream for use in native and web environments.
|
||||
*/
|
||||
export interface WriteableStream<T> extends ReadableStream<T> {
|
||||
|
||||
/**
|
||||
* Writing data to the stream will trigger the on('data')
|
||||
* event listener if the stream is flowing and buffer the
|
||||
* data otherwise until the stream is flowing.
|
||||
*
|
||||
* If a `highWaterMark` is configured and writing to the
|
||||
* stream reaches this mark, a promise will be returned
|
||||
* that should be awaited on before writing more data.
|
||||
* Otherwise there is a risk of buffering a large number
|
||||
* of data chunks without consumer.
|
||||
*/
|
||||
write(data: T): void | Promise<void>;
|
||||
|
||||
/**
|
||||
* Signals an error to the consumer of the stream via the
|
||||
* on('error') handler if the stream is flowing.
|
||||
*
|
||||
* NOTE: call `end` to signal that the stream has ended,
|
||||
* this DOES NOT happen automatically from `error`.
|
||||
*/
|
||||
error(error: Error): void;
|
||||
|
||||
/**
|
||||
* Signals the end of the stream to the consumer. If the
|
||||
* result is provided, will trigger the on('data') event
|
||||
* listener if the stream is flowing and buffer the data
|
||||
* otherwise until the stream is flowing.
|
||||
*/
|
||||
end(result?: T): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A stream that has a buffer already read. Returns the original stream
|
||||
* that was read as well as the chunks that got read.
|
||||
*
|
||||
* The `ended` flag indicates if the stream has been fully consumed.
|
||||
*/
|
||||
export interface ReadableBufferedStream<T> {
|
||||
|
||||
/**
|
||||
* The original stream that is being read.
|
||||
*/
|
||||
stream: ReadableStream<T>;
|
||||
|
||||
/**
|
||||
* An array of chunks already read from this stream.
|
||||
*/
|
||||
buffer: T[];
|
||||
|
||||
/**
|
||||
* Signals if the stream has ended or not. If not, consumers
|
||||
* should continue to read from the stream until consumed.
|
||||
*/
|
||||
ended: boolean;
|
||||
}
|
||||
|
||||
export function isReadableStream<T>(obj: unknown): obj is ReadableStream<T> {
|
||||
const candidate = obj as ReadableStream<T> | undefined;
|
||||
if (!candidate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [candidate.on, candidate.pause, candidate.resume, candidate.destroy].every(fn => typeof fn === 'function');
|
||||
}
|
||||
|
||||
export function isReadableBufferedStream<T>(obj: unknown): obj is ReadableBufferedStream<T> {
|
||||
const candidate = obj as ReadableBufferedStream<T> | undefined;
|
||||
if (!candidate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isReadableStream(candidate.stream) && Array.isArray(candidate.buffer) && typeof candidate.ended === 'boolean';
|
||||
}
|
||||
|
||||
export interface IReducer<T, R = T> {
|
||||
(data: T[]): R;
|
||||
}
|
||||
|
||||
export interface IDataTransformer<Original, Transformed> {
|
||||
(data: Original): Transformed;
|
||||
}
|
||||
|
||||
export interface IErrorTransformer {
|
||||
(error: Error): Error;
|
||||
}
|
||||
|
||||
export interface ITransformer<Original, Transformed> {
|
||||
data: IDataTransformer<Original, Transformed>;
|
||||
error?: IErrorTransformer;
|
||||
}
|
||||
|
||||
export function newWriteableStream<T>(reducer: IReducer<T>, options?: WriteableStreamOptions): WriteableStream<T> {
|
||||
return new WriteableStreamImpl<T>(reducer, options);
|
||||
}
|
||||
|
||||
export interface WriteableStreamOptions {
|
||||
|
||||
/**
|
||||
* The number of objects to buffer before WriteableStream#write()
|
||||
* signals back that the buffer is full. Can be used to reduce
|
||||
* the memory pressure when the stream is not flowing.
|
||||
*/
|
||||
highWaterMark?: number;
|
||||
}
|
||||
|
||||
class WriteableStreamImpl<T> implements WriteableStream<T> {
|
||||
|
||||
private readonly state = {
|
||||
flowing: false,
|
||||
ended: false,
|
||||
destroyed: false
|
||||
};
|
||||
|
||||
private readonly buffer = {
|
||||
data: [] as T[],
|
||||
error: [] as Error[]
|
||||
};
|
||||
|
||||
private readonly listeners = {
|
||||
data: [] as { (data: T): void }[],
|
||||
error: [] as { (error: Error): void }[],
|
||||
end: [] as { (): void }[]
|
||||
};
|
||||
|
||||
private readonly pendingWritePromises: Function[] = [];
|
||||
|
||||
constructor(private reducer: IReducer<T>, private options?: WriteableStreamOptions) { }
|
||||
|
||||
pause(): void {
|
||||
if (this.state.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.flowing = false;
|
||||
}
|
||||
|
||||
resume(): void {
|
||||
if (this.state.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.flowing) {
|
||||
this.state.flowing = true;
|
||||
|
||||
// emit buffered events
|
||||
this.flowData();
|
||||
this.flowErrors();
|
||||
this.flowEnd();
|
||||
}
|
||||
}
|
||||
|
||||
write(data: T): void | Promise<void> {
|
||||
if (this.state.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// flowing: directly send the data to listeners
|
||||
if (this.state.flowing) {
|
||||
this.emitData(data);
|
||||
}
|
||||
|
||||
// not yet flowing: buffer data until flowing
|
||||
else {
|
||||
this.buffer.data.push(data);
|
||||
|
||||
// highWaterMark: if configured, signal back when buffer reached limits
|
||||
if (typeof this.options?.highWaterMark === 'number' && this.buffer.data.length > this.options.highWaterMark) {
|
||||
return new Promise(resolve => this.pendingWritePromises.push(resolve));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
error(error: Error): void {
|
||||
if (this.state.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// flowing: directly send the error to listeners
|
||||
if (this.state.flowing) {
|
||||
this.emitError(error);
|
||||
}
|
||||
|
||||
// not yet flowing: buffer errors until flowing
|
||||
else {
|
||||
this.buffer.error.push(error);
|
||||
}
|
||||
}
|
||||
|
||||
end(result?: T): void {
|
||||
if (this.state.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// end with data if provided
|
||||
if (typeof result !== 'undefined') {
|
||||
this.write(result);
|
||||
}
|
||||
|
||||
// flowing: send end event to listeners
|
||||
if (this.state.flowing) {
|
||||
this.emitEnd();
|
||||
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
// not yet flowing: remember state
|
||||
else {
|
||||
this.state.ended = true;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
on(event: 'data' | 'error' | 'end', callback: (arg0?: any) => void): void {
|
||||
if (this.state.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event) {
|
||||
case 'data':
|
||||
this.listeners.data.push(callback);
|
||||
|
||||
// switch into flowing mode as soon as the first 'data'
|
||||
// listener is added and we are not yet in flowing mode
|
||||
this.resume();
|
||||
|
||||
break;
|
||||
|
||||
case 'end':
|
||||
this.listeners.end.push(callback);
|
||||
|
||||
// emit 'end' event directly if we are flowing
|
||||
// and the end has already been reached
|
||||
//
|
||||
// finish() when it went through
|
||||
if (this.state.flowing && this.flowEnd()) {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
this.listeners.error.push(callback);
|
||||
|
||||
// emit buffered 'error' events unless done already
|
||||
// now that we know that we have at least one listener
|
||||
if (this.state.flowing) {
|
||||
this.flowErrors();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
removeListener(event: string, callback: Function): void {
|
||||
if (this.state.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let listeners: unknown[] | undefined = undefined;
|
||||
|
||||
switch (event) {
|
||||
case 'data':
|
||||
listeners = this.listeners.data;
|
||||
break;
|
||||
|
||||
case 'end':
|
||||
listeners = this.listeners.end;
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
listeners = this.listeners.error;
|
||||
break;
|
||||
}
|
||||
|
||||
if (listeners) {
|
||||
const index = listeners.indexOf(callback);
|
||||
if (index >= 0) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private flowData(): void {
|
||||
if (this.buffer.data.length > 0) {
|
||||
const fullDataBuffer = this.reducer(this.buffer.data);
|
||||
|
||||
this.emitData(fullDataBuffer);
|
||||
|
||||
this.buffer.data.length = 0;
|
||||
|
||||
// When the buffer is empty, resolve all pending writers
|
||||
const pendingWritePromises = [...this.pendingWritePromises];
|
||||
this.pendingWritePromises.length = 0;
|
||||
pendingWritePromises.forEach(pendingWritePromise => pendingWritePromise());
|
||||
}
|
||||
}
|
||||
|
||||
private flowErrors(): void {
|
||||
if (this.listeners.error.length > 0) {
|
||||
for (const error of this.buffer.error) {
|
||||
this.emitError(error);
|
||||
}
|
||||
|
||||
this.buffer.error.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private flowEnd(): boolean {
|
||||
if (this.state.ended) {
|
||||
this.emitEnd();
|
||||
|
||||
return this.listeners.end.length > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (!this.state.destroyed) {
|
||||
this.state.destroyed = true;
|
||||
this.state.ended = true;
|
||||
|
||||
this.buffer.data.length = 0;
|
||||
this.buffer.error.length = 0;
|
||||
|
||||
this.listeners.data.length = 0;
|
||||
this.listeners.error.length = 0;
|
||||
this.listeners.end.length = 0;
|
||||
|
||||
this.pendingWritePromises.length = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to fully read a T readable into a T.
|
||||
*/
|
||||
export function consumeReadable<T>(readable: Readable<T>, reducer: IReducer<T>): T {
|
||||
const chunks: T[] = [];
|
||||
|
||||
let chunk: T | null;
|
||||
while ((chunk = readable.read()) !== null) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
return reducer(chunks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to read a T readable up to a maximum of chunks. If the limit is
|
||||
* reached, will return a readable instead to ensure all data can still
|
||||
* be read.
|
||||
*/
|
||||
export function peekReadable<T>(readable: Readable<T>, reducer: IReducer<T>, maxChunks: number): T | Readable<T> {
|
||||
const chunks: T[] = [];
|
||||
|
||||
let chunk: T | null | undefined = undefined;
|
||||
while ((chunk = readable.read()) !== null && chunks.length < maxChunks) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
// If the last chunk is null, it means we reached the end of
|
||||
// the readable and return all the data at once
|
||||
if (chunk === null && chunks.length > 0) {
|
||||
return reducer(chunks);
|
||||
}
|
||||
|
||||
// Otherwise, we still have a chunk, it means we reached the maxChunks
|
||||
// value and as such we return a new Readable that first returns
|
||||
// the existing read chunks and then continues with reading from
|
||||
// the underlying readable.
|
||||
return {
|
||||
read: () => {
|
||||
|
||||
// First consume chunks from our array
|
||||
if (chunks.length > 0) {
|
||||
return chunks.shift()!;
|
||||
}
|
||||
|
||||
// Then ensure to return our last read chunk
|
||||
if (typeof chunk !== 'undefined') {
|
||||
const lastReadChunk = chunk;
|
||||
|
||||
// explicitly use undefined here to indicate that we consumed
|
||||
// the chunk, which could have either been null or valued.
|
||||
chunk = undefined;
|
||||
|
||||
return lastReadChunk;
|
||||
}
|
||||
|
||||
// Finally delegate back to the Readable
|
||||
return readable.read();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to fully read a T stream into a T or consuming
|
||||
* a stream fully, awaiting all the events without caring
|
||||
* about the data.
|
||||
*/
|
||||
export function consumeStream<T, R = T>(stream: ReadableStreamEvents<T>, reducer: IReducer<T, R>): Promise<R>;
|
||||
export function consumeStream(stream: ReadableStreamEvents<unknown>): Promise<undefined>;
|
||||
export function consumeStream<T, R = T>(stream: ReadableStreamEvents<T>, reducer?: IReducer<T, R>): Promise<R | undefined> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: T[] = [];
|
||||
|
||||
listenStream(stream, {
|
||||
onData: chunk => {
|
||||
if (reducer) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
},
|
||||
onError: error => {
|
||||
if (reducer) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
},
|
||||
onEnd: () => {
|
||||
if (reducer) {
|
||||
resolve(reducer(chunks));
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export interface IStreamListener<T> {
|
||||
|
||||
/**
|
||||
* The 'data' event is emitted whenever the stream is
|
||||
* relinquishing ownership of a chunk of data to a consumer.
|
||||
*/
|
||||
onData(data: T): void;
|
||||
|
||||
/**
|
||||
* Emitted when any error occurs.
|
||||
*/
|
||||
onError(err: Error): void;
|
||||
|
||||
/**
|
||||
* The 'end' event is emitted when there is no more data
|
||||
* to be consumed from the stream. The 'end' event will
|
||||
* not be emitted unless the data is completely consumed.
|
||||
*/
|
||||
onEnd(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to listen to all events of a T stream in proper order.
|
||||
*/
|
||||
export function listenStream<T>(stream: ReadableStreamEvents<T>, listener: IStreamListener<T>, token?: CancellationToken): void {
|
||||
|
||||
stream.on('error', error => {
|
||||
if (!token?.isCancellationRequested) {
|
||||
listener.onError(error);
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
if (!token?.isCancellationRequested) {
|
||||
listener.onEnd();
|
||||
}
|
||||
});
|
||||
|
||||
// Adding the `data` listener will turn the stream
|
||||
// into flowing mode. As such it is important to
|
||||
// add this listener last (DO NOT CHANGE!)
|
||||
stream.on('data', data => {
|
||||
if (!token?.isCancellationRequested) {
|
||||
listener.onData(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to peek up to `maxChunks` into a stream. The return type signals if
|
||||
* the stream has ended or not. If not, caller needs to add a `data` listener
|
||||
* to continue reading.
|
||||
*/
|
||||
export function peekStream<T>(stream: ReadableStream<T>, maxChunks: number): Promise<ReadableBufferedStream<T>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const streamListeners = new DisposableStore();
|
||||
const buffer: T[] = [];
|
||||
|
||||
// Data Listener
|
||||
const dataListener = (chunk: T) => {
|
||||
|
||||
// Add to buffer
|
||||
buffer.push(chunk);
|
||||
|
||||
// We reached maxChunks and thus need to return
|
||||
if (buffer.length > maxChunks) {
|
||||
|
||||
// Dispose any listeners and ensure to pause the
|
||||
// stream so that it can be consumed again by caller
|
||||
streamListeners.dispose();
|
||||
stream.pause();
|
||||
|
||||
return resolve({ stream, buffer, ended: false });
|
||||
}
|
||||
};
|
||||
|
||||
// Error Listener
|
||||
const errorListener = (error: Error) => {
|
||||
streamListeners.dispose();
|
||||
|
||||
return reject(error);
|
||||
};
|
||||
|
||||
// End Listener
|
||||
const endListener = () => {
|
||||
streamListeners.dispose();
|
||||
|
||||
return resolve({ stream, buffer, ended: true });
|
||||
};
|
||||
|
||||
streamListeners.add(toDisposable(() => stream.removeListener('error', errorListener)));
|
||||
stream.on('error', errorListener);
|
||||
|
||||
streamListeners.add(toDisposable(() => stream.removeListener('end', endListener)));
|
||||
stream.on('end', endListener);
|
||||
|
||||
// Important: leave the `data` listener last because
|
||||
// this can turn the stream into flowing mode and we
|
||||
// want `error` events to be received as well.
|
||||
streamListeners.add(toDisposable(() => stream.removeListener('data', dataListener)));
|
||||
stream.on('data', dataListener);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create a readable stream from an existing T.
|
||||
*/
|
||||
export function toStream<T>(t: T, reducer: IReducer<T>): ReadableStream<T> {
|
||||
const stream = newWriteableStream<T>(reducer);
|
||||
|
||||
stream.end(t);
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create an empty stream
|
||||
*/
|
||||
export function emptyStream(): ReadableStream<never> {
|
||||
const stream = newWriteableStream<never>(() => { throw new Error('not supported'); });
|
||||
stream.end();
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to convert a T into a Readable<T>.
|
||||
*/
|
||||
export function toReadable<T>(t: T): Readable<T> {
|
||||
let consumed = false;
|
||||
|
||||
return {
|
||||
read: () => {
|
||||
if (consumed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
consumed = true;
|
||||
|
||||
return t;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to transform a readable stream into another stream.
|
||||
*/
|
||||
export function transform<Original, Transformed>(stream: ReadableStreamEvents<Original>, transformer: ITransformer<Original, Transformed>, reducer: IReducer<Transformed>): ReadableStream<Transformed> {
|
||||
const target = newWriteableStream<Transformed>(reducer);
|
||||
|
||||
listenStream(stream, {
|
||||
onData: data => target.write(transformer.data(data)),
|
||||
onError: error => target.error(transformer.error ? transformer.error(error) : error),
|
||||
onEnd: () => target.end()
|
||||
});
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to take an existing readable that will
|
||||
* have a prefix injected to the beginning.
|
||||
*/
|
||||
export function prefixedReadable<T>(prefix: T, readable: Readable<T>, reducer: IReducer<T>): Readable<T> {
|
||||
let prefixHandled = false;
|
||||
|
||||
return {
|
||||
read: () => {
|
||||
const chunk = readable.read();
|
||||
|
||||
// Handle prefix only once
|
||||
if (!prefixHandled) {
|
||||
prefixHandled = true;
|
||||
|
||||
// If we have also a read-result, make
|
||||
// sure to reduce it to a single result
|
||||
if (chunk !== null) {
|
||||
return reducer([prefix, chunk]);
|
||||
}
|
||||
|
||||
// Otherwise, just return prefix directly
|
||||
return prefix;
|
||||
}
|
||||
|
||||
return chunk;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to take an existing stream that will
|
||||
* have a prefix injected to the beginning.
|
||||
*/
|
||||
export function prefixedStream<T>(prefix: T, stream: ReadableStream<T>, reducer: IReducer<T>): ReadableStream<T> {
|
||||
let prefixHandled = false;
|
||||
|
||||
const target = newWriteableStream<T>(reducer);
|
||||
|
||||
listenStream(stream, {
|
||||
onData: data => {
|
||||
|
||||
// Handle prefix only once
|
||||
if (!prefixHandled) {
|
||||
prefixHandled = true;
|
||||
|
||||
return target.write(reducer([prefix, data]));
|
||||
}
|
||||
|
||||
return target.write(data);
|
||||
},
|
||||
onError: error => target.error(error),
|
||||
onEnd: () => {
|
||||
|
||||
// Handle prefix only once
|
||||
if (!prefixHandled) {
|
||||
prefixHandled = true;
|
||||
|
||||
target.write(prefix);
|
||||
}
|
||||
|
||||
target.end();
|
||||
}
|
||||
});
|
||||
|
||||
return target;
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
type SparseEmbedding = Record</* word */ string, /* weight */number>;
|
||||
type TermFrequencies = Map</* word */ string, /*occurrences*/ number>;
|
||||
type DocumentOccurrences = Map</* word */ string, /*documentOccurrences*/ number>;
|
||||
|
||||
function countMapFrom<K>(values: Iterable<K>): Map<K, number> {
|
||||
const map = new Map<K, number>();
|
||||
for (const value of values) {
|
||||
map.set(value, (map.get(value) ?? 0) + 1);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
interface DocumentChunkEntry {
|
||||
readonly text: string;
|
||||
readonly tf: TermFrequencies;
|
||||
}
|
||||
|
||||
export interface TfIdfDocument {
|
||||
readonly key: string;
|
||||
readonly textChunks: readonly string[];
|
||||
}
|
||||
|
||||
export interface TfIdfScore {
|
||||
readonly key: string;
|
||||
/**
|
||||
* An unbounded number.
|
||||
*/
|
||||
readonly score: number;
|
||||
}
|
||||
|
||||
export interface NormalizedTfIdfScore {
|
||||
readonly key: string;
|
||||
/**
|
||||
* A number between 0 and 1.
|
||||
*/
|
||||
readonly score: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of tf-idf (term frequency-inverse document frequency) for a set of
|
||||
* documents where each document contains one or more chunks of text.
|
||||
* Each document is identified by a key, and the score for each document is computed
|
||||
* by taking the max score over all the chunks in the document.
|
||||
*/
|
||||
export class TfIdfCalculator {
|
||||
calculateScores(query: string, token: CancellationToken): TfIdfScore[] {
|
||||
const embedding = this.computeEmbedding(query);
|
||||
const idfCache = new Map<string, number>();
|
||||
const scores: TfIdfScore[] = [];
|
||||
// For each document, generate one score
|
||||
for (const [key, doc] of this.documents) {
|
||||
if (token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const chunk of doc.chunks) {
|
||||
const score = this.computeSimilarityScore(chunk, embedding, idfCache);
|
||||
if (score > 0) {
|
||||
scores.push({ key, score });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scores;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count how many times each term (word) appears in a string.
|
||||
*/
|
||||
private static termFrequencies(input: string): TermFrequencies {
|
||||
return countMapFrom(TfIdfCalculator.splitTerms(input));
|
||||
}
|
||||
|
||||
/**
|
||||
* Break a string into terms (words).
|
||||
*/
|
||||
private static *splitTerms(input: string): Iterable<string> {
|
||||
const normalize = (word: string) => word.toLowerCase();
|
||||
|
||||
// Only match on words that are at least 3 characters long and start with a letter
|
||||
for (const [word] of input.matchAll(/\b\p{Letter}[\p{Letter}\d]{2,}\b/gu)) {
|
||||
yield normalize(word);
|
||||
|
||||
const camelParts = word.replace(/([a-z])([A-Z])/g, '$1 $2').split(/\s+/g);
|
||||
if (camelParts.length > 1) {
|
||||
for (const part of camelParts) {
|
||||
// Require at least 3 letters in the parts of a camel case word
|
||||
if (part.length > 2 && /\p{Letter}{3,}/gu.test(part)) {
|
||||
yield normalize(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Total number of chunks
|
||||
*/
|
||||
private chunkCount = 0;
|
||||
|
||||
private readonly chunkOccurrences: DocumentOccurrences = new Map</* word */ string, /*documentOccurrences*/ number>();
|
||||
|
||||
private readonly documents = new Map</* key */ string, {
|
||||
readonly chunks: ReadonlyArray<DocumentChunkEntry>;
|
||||
}>();
|
||||
|
||||
updateDocuments(documents: ReadonlyArray<TfIdfDocument>): this {
|
||||
for (const { key } of documents) {
|
||||
this.deleteDocument(key);
|
||||
}
|
||||
|
||||
for (const doc of documents) {
|
||||
const chunks: Array<{ text: string; tf: TermFrequencies }> = [];
|
||||
for (const text of doc.textChunks) {
|
||||
// TODO: See if we can compute the tf lazily
|
||||
// The challenge is that we need to also update the `chunkOccurrences`
|
||||
// and all of those updates need to get flushed before the real TF-IDF of
|
||||
// anything is computed.
|
||||
const tf = TfIdfCalculator.termFrequencies(text);
|
||||
|
||||
// Update occurrences list
|
||||
for (const term of tf.keys()) {
|
||||
this.chunkOccurrences.set(term, (this.chunkOccurrences.get(term) ?? 0) + 1);
|
||||
}
|
||||
|
||||
chunks.push({ text, tf });
|
||||
}
|
||||
|
||||
this.chunkCount += chunks.length;
|
||||
this.documents.set(doc.key, { chunks });
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
deleteDocument(key: string) {
|
||||
const doc = this.documents.get(key);
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.documents.delete(key);
|
||||
this.chunkCount -= doc.chunks.length;
|
||||
|
||||
// Update term occurrences for the document
|
||||
for (const chunk of doc.chunks) {
|
||||
for (const term of chunk.tf.keys()) {
|
||||
const currentOccurrences = this.chunkOccurrences.get(term);
|
||||
if (typeof currentOccurrences === 'number') {
|
||||
const newOccurrences = currentOccurrences - 1;
|
||||
if (newOccurrences <= 0) {
|
||||
this.chunkOccurrences.delete(term);
|
||||
} else {
|
||||
this.chunkOccurrences.set(term, newOccurrences);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private computeSimilarityScore(chunk: DocumentChunkEntry, queryEmbedding: SparseEmbedding, idfCache: Map<string, number>): number {
|
||||
// Compute the dot product between the chunk's embedding and the query embedding
|
||||
|
||||
// Note that the chunk embedding is computed lazily on a per-term basis.
|
||||
// This lets us skip a large number of calculations because the majority
|
||||
// of chunks do not share any terms with the query.
|
||||
|
||||
let sum = 0;
|
||||
for (const [term, termTfidf] of Object.entries(queryEmbedding)) {
|
||||
const chunkTf = chunk.tf.get(term);
|
||||
if (!chunkTf) {
|
||||
// Term does not appear in chunk so it has no contribution
|
||||
continue;
|
||||
}
|
||||
|
||||
let chunkIdf = idfCache.get(term);
|
||||
if (typeof chunkIdf !== 'number') {
|
||||
chunkIdf = this.computeIdf(term);
|
||||
idfCache.set(term, chunkIdf);
|
||||
}
|
||||
|
||||
const chunkTfidf = chunkTf * chunkIdf;
|
||||
sum += chunkTfidf * termTfidf;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
private computeEmbedding(input: string): SparseEmbedding {
|
||||
const tf = TfIdfCalculator.termFrequencies(input);
|
||||
return this.computeTfidf(tf);
|
||||
}
|
||||
|
||||
private computeIdf(term: string): number {
|
||||
const chunkOccurrences = this.chunkOccurrences.get(term) ?? 0;
|
||||
return chunkOccurrences > 0
|
||||
? Math.log((this.chunkCount + 1) / chunkOccurrences)
|
||||
: 0;
|
||||
}
|
||||
|
||||
private computeTfidf(termFrequencies: TermFrequencies): SparseEmbedding {
|
||||
const embedding = Object.create(null);
|
||||
for (const [word, occurrences] of termFrequencies) {
|
||||
const idf = this.computeIdf(word);
|
||||
if (idf > 0) {
|
||||
embedding[word] = occurrences * idf;
|
||||
}
|
||||
}
|
||||
return embedding;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the scores to be between 0 and 1 and sort them decending.
|
||||
* @param scores array of scores from {@link TfIdfCalculator.calculateScores}
|
||||
* @returns normalized scores
|
||||
*/
|
||||
export function normalizeTfIdfScores(scores: TfIdfScore[]): NormalizedTfIdfScore[] {
|
||||
|
||||
// copy of scores
|
||||
const result = scores.slice(0) as { score: number }[];
|
||||
|
||||
// sort descending
|
||||
result.sort((a, b) => b.score - a.score);
|
||||
|
||||
// normalize
|
||||
const max = result[0]?.score ?? 0;
|
||||
if (max > 0) {
|
||||
for (const score of result) {
|
||||
score.score /= max;
|
||||
}
|
||||
}
|
||||
|
||||
return result as TfIdfScore[];
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is a JavaScript String or not.
|
||||
*/
|
||||
export function isString(str: unknown): str is string {
|
||||
return (typeof str === 'string');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is a JavaScript Array and each element in the array is a string.
|
||||
*/
|
||||
export function isStringArray(value: unknown): value is string[] {
|
||||
return Array.isArray(value) && (<unknown[]>value).every(elem => isString(elem));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is of type `object` but **not**
|
||||
* `null`, an `array`, a `regexp`, nor a `date`.
|
||||
*/
|
||||
export function isObject(obj: unknown): obj is Object {
|
||||
// The method can't do a type cast since there are type (like strings) which
|
||||
// are subclasses of any put not positvely matched by the function. Hence type
|
||||
// narrowing results in wrong results.
|
||||
return typeof obj === 'object'
|
||||
&& obj !== null
|
||||
&& !Array.isArray(obj)
|
||||
&& !(obj instanceof RegExp)
|
||||
&& !(obj instanceof Date);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is of type `Buffer` or Uint8Array dervived type
|
||||
*/
|
||||
export function isTypedArray(obj: unknown): obj is Object {
|
||||
const TypedArray = Object.getPrototypeOf(Uint8Array);
|
||||
return typeof obj === 'object'
|
||||
&& obj instanceof TypedArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* In **contrast** to just checking `typeof` this will return `false` for `NaN`.
|
||||
* @returns whether the provided parameter is a JavaScript Number or not.
|
||||
*/
|
||||
export function isNumber(obj: unknown): obj is number {
|
||||
return (typeof obj === 'number' && !isNaN(obj));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is an Iterable, casting to the given generic
|
||||
*/
|
||||
export function isIterable<T>(obj: unknown): obj is Iterable<T> {
|
||||
return !!obj && typeof (obj as any)[Symbol.iterator] === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is a JavaScript Boolean or not.
|
||||
*/
|
||||
export function isBoolean(obj: unknown): obj is boolean {
|
||||
return (obj === true || obj === false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is undefined.
|
||||
*/
|
||||
export function isUndefined(obj: unknown): obj is undefined {
|
||||
return (typeof obj === 'undefined');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is defined.
|
||||
*/
|
||||
export function isDefined<T>(arg: T | null | undefined): arg is T {
|
||||
return !isUndefinedOrNull(arg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is undefined or null.
|
||||
*/
|
||||
export function isUndefinedOrNull(obj: unknown): obj is undefined | null {
|
||||
return (isUndefined(obj) || obj === null);
|
||||
}
|
||||
|
||||
|
||||
export function assertType(condition: unknown, type?: string): asserts condition {
|
||||
if (!condition) {
|
||||
throw new Error(type ? `Unexpected type, expected '${type}'` : 'Unexpected type');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the argument passed in is neither undefined nor null.
|
||||
*/
|
||||
export function assertIsDefined<T>(arg: T | null | undefined): T {
|
||||
if (isUndefinedOrNull(arg)) {
|
||||
throw new Error('Assertion Failed: argument is undefined or null');
|
||||
}
|
||||
|
||||
return arg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that each argument passed in is neither undefined nor null.
|
||||
*/
|
||||
export function assertAllDefined<T1, T2>(t1: T1 | null | undefined, t2: T2 | null | undefined): [T1, T2];
|
||||
export function assertAllDefined<T1, T2, T3>(t1: T1 | null | undefined, t2: T2 | null | undefined, t3: T3 | null | undefined): [T1, T2, T3];
|
||||
export function assertAllDefined<T1, T2, T3, T4>(t1: T1 | null | undefined, t2: T2 | null | undefined, t3: T3 | null | undefined, t4: T4 | null | undefined): [T1, T2, T3, T4];
|
||||
export function assertAllDefined(...args: (unknown | null | undefined)[]): unknown[] {
|
||||
const result = [];
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
|
||||
if (isUndefinedOrNull(arg)) {
|
||||
throw new Error(`Assertion Failed: argument at index ${i} is undefined or null`);
|
||||
}
|
||||
|
||||
result.push(arg);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is an empty JavaScript Object or not.
|
||||
*/
|
||||
export function isEmptyObject(obj: unknown): obj is object {
|
||||
if (!isObject(obj)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const key in obj) {
|
||||
if (hasOwnProperty.call(obj, key)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is a JavaScript Function or not.
|
||||
*/
|
||||
export function isFunction(obj: unknown): obj is Function {
|
||||
return (typeof obj === 'function');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameters is are JavaScript Function or not.
|
||||
*/
|
||||
export function areFunctions(...objects: unknown[]): boolean {
|
||||
return objects.length > 0 && objects.every(isFunction);
|
||||
}
|
||||
|
||||
export type TypeConstraint = string | Function;
|
||||
|
||||
export function validateConstraints(args: unknown[], constraints: Array<TypeConstraint | undefined>): void {
|
||||
const len = Math.min(args.length, constraints.length);
|
||||
for (let i = 0; i < len; i++) {
|
||||
validateConstraint(args[i], constraints[i]);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateConstraint(arg: unknown, constraint: TypeConstraint | undefined): void {
|
||||
|
||||
if (isString(constraint)) {
|
||||
if (typeof arg !== constraint) {
|
||||
throw new Error(`argument does not match constraint: typeof ${constraint}`);
|
||||
}
|
||||
} else if (isFunction(constraint)) {
|
||||
try {
|
||||
if (arg instanceof constraint) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (!isUndefinedOrNull(arg) && (arg as any).constructor === constraint) {
|
||||
return;
|
||||
}
|
||||
if (constraint.length === 1 && constraint.call(undefined, arg) === true) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`argument does not match one of these constraints: arg instanceof constraint, arg.constructor === constraint, nor constraint(arg) === true`);
|
||||
}
|
||||
}
|
||||
|
||||
type AddFirstParameterToFunction<T, TargetFunctionsReturnType, FirstParameter> = T extends (...args: any[]) => TargetFunctionsReturnType ?
|
||||
// Function: add param to function
|
||||
(firstArg: FirstParameter, ...args: Parameters<T>) => ReturnType<T> :
|
||||
|
||||
// Else: just leave as is
|
||||
T;
|
||||
|
||||
/**
|
||||
* Allows to add a first parameter to functions of a type.
|
||||
*/
|
||||
export type AddFirstParameterToFunctions<Target, TargetFunctionsReturnType, FirstParameter> = {
|
||||
// For every property
|
||||
[K in keyof Target]: AddFirstParameterToFunction<Target[K], TargetFunctionsReturnType, FirstParameter>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given an object with all optional properties, requires at least one to be defined.
|
||||
* i.e. AtLeastOne<MyObject>;
|
||||
*/
|
||||
export type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U];
|
||||
|
||||
/**
|
||||
* Only picks the non-optional properties of a type.
|
||||
*/
|
||||
export type OmitOptional<T> = { [K in keyof T as T[K] extends Required<T>[K] ? K : never]: T[K] };
|
||||
|
||||
/**
|
||||
* A type that removed readonly-less from all properties of `T`
|
||||
*/
|
||||
export type Mutable<T> = {
|
||||
-readonly [P in keyof T]: T[P]
|
||||
};
|
||||
|
||||
/**
|
||||
* A single object or an array of the objects.
|
||||
*/
|
||||
export type SingleOrMany<T> = T | T[];
|
||||
|
||||
|
||||
/**
|
||||
* A type that recursively makes all properties of `T` required
|
||||
*/
|
||||
export type DeepRequiredNonNullable<T> = {
|
||||
[P in keyof T]-?: T[P] extends object ? DeepRequiredNonNullable<T[P]> : Required<NonNullable<T[P]>>;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Represents a type that is a partial version of a given type `T`, where all properties are optional and can be deeply nested.
|
||||
*/
|
||||
export type DeepPartial<T> = {
|
||||
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : Partial<T[P]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a type that is a partial version of a given type `T`, except a subset.
|
||||
*/
|
||||
export type PartialExcept<T, K extends keyof T> = Partial<Omit<T, K>> & Pick<T, K>;
|
||||
@@ -1,81 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
const _UUIDPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
export function isUUID(value: string): boolean {
|
||||
return _UUIDPattern.test(value);
|
||||
}
|
||||
|
||||
declare const crypto: undefined | {
|
||||
//https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues#browser_compatibility
|
||||
getRandomValues?(data: Uint8Array): Uint8Array;
|
||||
//https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID#browser_compatibility
|
||||
randomUUID?(): string;
|
||||
};
|
||||
|
||||
export const generateUuid = (function (): () => string {
|
||||
|
||||
// use `randomUUID` if possible
|
||||
if (typeof crypto === 'object' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID.bind(crypto);
|
||||
}
|
||||
|
||||
// use `randomValues` if possible
|
||||
let getRandomValues: (bucket: Uint8Array) => Uint8Array;
|
||||
if (typeof crypto === 'object' && typeof crypto.getRandomValues === 'function') {
|
||||
getRandomValues = crypto.getRandomValues.bind(crypto);
|
||||
|
||||
} else {
|
||||
getRandomValues = function (bucket: Uint8Array): Uint8Array {
|
||||
for (let i = 0; i < bucket.length; i++) {
|
||||
bucket[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
return bucket;
|
||||
};
|
||||
}
|
||||
|
||||
// prep-work
|
||||
const _data = new Uint8Array(16);
|
||||
const _hex: string[] = [];
|
||||
for (let i = 0; i < 256; i++) {
|
||||
_hex.push(i.toString(16).padStart(2, '0'));
|
||||
}
|
||||
|
||||
return function generateUuid(): string {
|
||||
// get data
|
||||
getRandomValues(_data);
|
||||
|
||||
// set version bits
|
||||
_data[6] = (_data[6] & 0x0f) | 0x40;
|
||||
_data[8] = (_data[8] & 0x3f) | 0x80;
|
||||
|
||||
// print as string
|
||||
let i = 0;
|
||||
let result = '';
|
||||
result += _hex[_data[i++]];
|
||||
result += _hex[_data[i++]];
|
||||
result += _hex[_data[i++]];
|
||||
result += _hex[_data[i++]];
|
||||
result += '-';
|
||||
result += _hex[_data[i++]];
|
||||
result += _hex[_data[i++]];
|
||||
result += '-';
|
||||
result += _hex[_data[i++]];
|
||||
result += _hex[_data[i++]];
|
||||
result += '-';
|
||||
result += _hex[_data[i++]];
|
||||
result += _hex[_data[i++]];
|
||||
result += '-';
|
||||
result += _hex[_data[i++]];
|
||||
result += _hex[_data[i++]];
|
||||
result += _hex[_data[i++]];
|
||||
result += _hex[_data[i++]];
|
||||
result += _hex[_data[i++]];
|
||||
result += _hex[_data[i++]];
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
@@ -1,87 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isObject } from 'vs/base/common/types';
|
||||
|
||||
interface IVerifier<T> {
|
||||
verify(value: unknown): T;
|
||||
}
|
||||
|
||||
abstract class Verifier<T> implements IVerifier<T> {
|
||||
|
||||
constructor(protected readonly defaultValue: T) { }
|
||||
|
||||
verify(value: unknown): T {
|
||||
if (!this.isType(value)) {
|
||||
return this.defaultValue;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
protected abstract isType(value: unknown): value is T;
|
||||
}
|
||||
|
||||
export class BooleanVerifier extends Verifier<boolean> {
|
||||
protected isType(value: unknown): value is boolean {
|
||||
return typeof value === 'boolean';
|
||||
}
|
||||
}
|
||||
|
||||
export class NumberVerifier extends Verifier<number> {
|
||||
protected isType(value: unknown): value is number {
|
||||
return typeof value === 'number';
|
||||
}
|
||||
}
|
||||
|
||||
export class SetVerifier<T> extends Verifier<Set<T>> {
|
||||
protected isType(value: unknown): value is Set<T> {
|
||||
return value instanceof Set;
|
||||
}
|
||||
}
|
||||
|
||||
export class EnumVerifier<T> extends Verifier<T> {
|
||||
private readonly allowedValues: ReadonlyArray<T>;
|
||||
|
||||
constructor(defaultValue: T, allowedValues: ReadonlyArray<T>) {
|
||||
super(defaultValue);
|
||||
this.allowedValues = allowedValues;
|
||||
}
|
||||
|
||||
protected isType(value: unknown): value is T {
|
||||
return this.allowedValues.includes(value as T);
|
||||
}
|
||||
}
|
||||
|
||||
export class ObjectVerifier<T extends Object> extends Verifier<T> {
|
||||
|
||||
constructor(defaultValue: T, private readonly verifier: { [K in keyof T]: IVerifier<T[K]> }) {
|
||||
super(defaultValue);
|
||||
}
|
||||
|
||||
override verify(value: unknown): T {
|
||||
if (!this.isType(value)) {
|
||||
return this.defaultValue;
|
||||
}
|
||||
return verifyObject<T>(this.verifier, value);
|
||||
}
|
||||
|
||||
protected isType(value: unknown): value is T {
|
||||
return isObject(value);
|
||||
}
|
||||
}
|
||||
|
||||
export function verifyObject<T extends Object>(verifiers: { [K in keyof T]: IVerifier<T[K]> }, value: Object): T {
|
||||
const result = Object.create(null);
|
||||
|
||||
for (const key in verifiers) {
|
||||
if (Object.hasOwnProperty.call(verifiers, key)) {
|
||||
const verifier = verifiers[key];
|
||||
result[key] = verifier.verify((value as any)[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user