This commit is contained in:
Daniel Imms
2024-07-09 11:59:22 -07:00
parent 4789c24e1e
commit 99db197472
153 changed files with 39831 additions and 9 deletions
+1
View File
@@ -11,6 +11,7 @@
"src/browser/tsconfig.json",
"src/common/tsconfig.json",
"src/headless/tsconfig.json",
"src/vs/tsconfig.json",
"test/benchmark/tsconfig.json",
"test/playwright/tsconfig.json",
"addons/addon-attach/src/tsconfig.json",
+3 -3
View File
@@ -4,9 +4,9 @@
},
// Hide output files from the file explorer, comment this out to see the build output
"files.exclude": {
"**/lib": true,
"**/out": true,
"**/out-*": true,
// "**/lib": true,
// "**/out": true,
// "**/out-*": true,
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.quoteStyle": "single",
+6 -3
View File
@@ -29,6 +29,7 @@ const commonOptions = {
/** @type {esbuild.BuildOptions} */
const devOptions = {
minify: false,
treeShaking: true,
};
/** @type {esbuild.BuildOptions} */
@@ -174,9 +175,9 @@ if (config.addon) {
entryPoints: [
`src/browser/public/Terminal.ts`,
`src/headless/public/Terminal.ts`,
`src/browser/*.test.ts`,
`src/common/*.test.ts`,
`src/headless/*.test.ts`
`src/browser/**/*.test.ts`,
`src/common/**/*.test.ts`,
`src/headless/**/*.test.ts`
],
outdir: 'out-esbuild/'
};
@@ -187,6 +188,8 @@ if (config.addon) {
};
}
console.log('Building bundle with config:', JSON.stringify(bundleConfig, undefined, 2));
if (config.isWatch) {
context(bundleConfig).then(e => e.watch());
if (!skipOut) {
+70
View File
@@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* 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));
}
}
}
+141
View File
@@ -0,0 +1,141 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CodeWindow, mainWindow } from 'vs/base/browser/window';
import { Emitter } from 'vs/base/common/event';
class WindowManager {
static readonly INSTANCE = new WindowManager();
// --- Zoom Level
private readonly mapWindowIdToZoomLevel = new Map<number, number>();
private readonly _onDidChangeZoomLevel = new Emitter<number>();
readonly onDidChangeZoomLevel = this._onDidChangeZoomLevel.event;
getZoomLevel(targetWindow: Window): number {
return this.mapWindowIdToZoomLevel.get(this.getWindowId(targetWindow)) ?? 0;
}
setZoomLevel(zoomLevel: number, targetWindow: Window): void {
if (this.getZoomLevel(targetWindow) === zoomLevel) {
return;
}
const targetWindowId = this.getWindowId(targetWindow);
this.mapWindowIdToZoomLevel.set(targetWindowId, zoomLevel);
this._onDidChangeZoomLevel.fire(targetWindowId);
}
// --- Zoom Factor
private readonly mapWindowIdToZoomFactor = new Map<number, number>();
getZoomFactor(targetWindow: Window): number {
return this.mapWindowIdToZoomFactor.get(this.getWindowId(targetWindow)) ?? 1;
}
setZoomFactor(zoomFactor: number, targetWindow: Window): void {
this.mapWindowIdToZoomFactor.set(this.getWindowId(targetWindow), zoomFactor);
}
// --- Fullscreen
private readonly _onDidChangeFullscreen = new Emitter<number>();
readonly onDidChangeFullscreen = this._onDidChangeFullscreen.event;
private readonly mapWindowIdToFullScreen = new Map<number, boolean>();
setFullscreen(fullscreen: boolean, targetWindow: Window): void {
if (this.isFullscreen(targetWindow) === fullscreen) {
return;
}
const windowId = this.getWindowId(targetWindow);
this.mapWindowIdToFullScreen.set(windowId, fullscreen);
this._onDidChangeFullscreen.fire(windowId);
}
isFullscreen(targetWindow: Window): boolean {
return !!this.mapWindowIdToFullScreen.get(this.getWindowId(targetWindow));
}
private getWindowId(targetWindow: Window): number {
return (targetWindow as CodeWindow).vscodeWindowId;
}
}
export function addMatchMediaChangeListener(targetWindow: Window, query: string | MediaQueryList, callback: (this: MediaQueryList, ev: MediaQueryListEvent) => any): void {
if (typeof query === 'string') {
query = targetWindow.matchMedia(query);
}
query.addEventListener('change', callback);
}
/** A zoom index, e.g. 1, 2, 3 */
export function setZoomLevel(zoomLevel: number, targetWindow: Window): void {
WindowManager.INSTANCE.setZoomLevel(zoomLevel, targetWindow);
}
export function getZoomLevel(targetWindow: Window): number {
return WindowManager.INSTANCE.getZoomLevel(targetWindow);
}
export const onDidChangeZoomLevel = WindowManager.INSTANCE.onDidChangeZoomLevel;
/** The zoom scale for an index, e.g. 1, 1.2, 1.4 */
export function getZoomFactor(targetWindow: Window): number {
return WindowManager.INSTANCE.getZoomFactor(targetWindow);
}
export function setZoomFactor(zoomFactor: number, targetWindow: Window): void {
WindowManager.INSTANCE.setZoomFactor(zoomFactor, targetWindow);
}
export function setFullscreen(fullscreen: boolean, targetWindow: Window): void {
WindowManager.INSTANCE.setFullscreen(fullscreen, targetWindow);
}
export function isFullscreen(targetWindow: Window): boolean {
return WindowManager.INSTANCE.isFullscreen(targetWindow);
}
export const onDidChangeFullscreen = WindowManager.INSTANCE.onDidChangeFullscreen;
const userAgent = navigator.userAgent;
export const isFirefox = (userAgent.indexOf('Firefox') >= 0);
export const isWebKit = (userAgent.indexOf('AppleWebKit') >= 0);
export const isChrome = (userAgent.indexOf('Chrome') >= 0);
export const isSafari = (!isChrome && (userAgent.indexOf('Safari') >= 0));
export const isWebkitWebView = (!isChrome && !isSafari && isWebKit);
export const isElectron = (userAgent.indexOf('Electron/') >= 0);
export const isAndroid = (userAgent.indexOf('Android') >= 0);
let standalone = false;
if (typeof mainWindow.matchMedia === 'function') {
const standaloneMatchMedia = mainWindow.matchMedia('(display-mode: standalone) or (display-mode: window-controls-overlay)');
const fullScreenMatchMedia = mainWindow.matchMedia('(display-mode: fullscreen)');
standalone = standaloneMatchMedia.matches;
addMatchMediaChangeListener(mainWindow, standaloneMatchMedia, ({ matches }) => {
// entering fullscreen would change standaloneMatchMedia.matches to false
// if standalone is true (running as PWA) and entering fullscreen, skip this change
if (standalone && fullScreenMatchMedia.matches) {
return;
}
// otherwise update standalone (browser to PWA or PWA to browser)
standalone = matches;
});
}
export function isStandalone(): boolean {
return standalone;
}
// Visible means that the feature is enabled, not necessarily being rendered
// e.g. visible is true even in fullscreen mode where the controls are hidden
// See docs at https://developer.mozilla.org/en-US/docs/Web/API/WindowControlsOverlay/visible
export function isWCOEnabled(): boolean {
return (navigator as any)?.windowControlsOverlay?.visible;
}
// Returns the bounding rect of the titlebar area if it is supported and defined
// See docs at https://developer.mozilla.org/en-US/docs/Web/API/WindowControlsOverlay/getTitlebarAreaRect
export function getWCOBoundingRect(): DOMRect | undefined {
return (navigator as any)?.windowControlsOverlay?.getTitlebarAreaRect();
}
+47
View File
@@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as browser from 'vs/base/browser/browser';
import { mainWindow } from 'vs/base/browser/window';
import * as platform from 'vs/base/common/platform';
export const enum KeyboardSupport {
Always,
FullScreen,
None
}
/**
* Browser feature we can support in current platform, browser and environment.
*/
export const BrowserFeatures = {
clipboard: {
writeText: (
platform.isNative
|| (document.queryCommandSupported && document.queryCommandSupported('copy'))
|| !!(navigator && navigator.clipboard && navigator.clipboard.writeText)
),
readText: (
platform.isNative
|| !!(navigator && navigator.clipboard && navigator.clipboard.readText)
)
},
keyboard: (() => {
if (platform.isNative || browser.isStandalone()) {
return KeyboardSupport.Always;
}
if ((<any>navigator).keyboard || browser.isSafari) {
return KeyboardSupport.FullScreen;
}
return KeyboardSupport.None;
})(),
// 'ontouchstart' in window always evaluates to true with typescript's modern typings. This causes `window` to be
// `never` later in `window.navigator`. That's why we need the explicit `window as Window` cast
touch: 'ontouchstart' in mainWindow || navigator.maxTouchPoints > 0,
pointerEvents: mainWindow.PointerEvent && ('ontouchstart' in mainWindow || navigator.maxTouchPoints > 0)
};
+174
View File
@@ -0,0 +1,174 @@
/*---------------------------------------------------------------------------------------------
* 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);
});
}
}
+108
View File
@@ -0,0 +1,108 @@
/*---------------------------------------------------------------------------------------------
* 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
};
}
+110
View File
@@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* 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;
}
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* 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;
}
+50
View File
@@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* 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();
}
}
+316
View File
@@ -0,0 +1,316 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export class FastDomNode<T extends HTMLElement> {
private _maxWidth: string = '';
private _width: string = '';
private _height: string = '';
private _top: string = '';
private _left: string = '';
private _bottom: string = '';
private _right: string = '';
private _paddingTop: string = '';
private _paddingLeft: string = '';
private _paddingBottom: string = '';
private _paddingRight: string = '';
private _fontFamily: string = '';
private _fontWeight: string = '';
private _fontSize: string = '';
private _fontStyle: string = '';
private _fontFeatureSettings: string = '';
private _fontVariationSettings: string = '';
private _textDecoration: string = '';
private _lineHeight: string = '';
private _letterSpacing: string = '';
private _className: string = '';
private _display: string = '';
private _position: string = '';
private _visibility: string = '';
private _color: string = '';
private _backgroundColor: string = '';
private _layerHint: boolean = false;
private _contain: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint' = 'none';
private _boxShadow: string = '';
constructor(
public readonly domNode: T
) { }
public setMaxWidth(_maxWidth: number | string): void {
const maxWidth = numberAsPixels(_maxWidth);
if (this._maxWidth === maxWidth) {
return;
}
this._maxWidth = maxWidth;
this.domNode.style.maxWidth = this._maxWidth;
}
public setWidth(_width: number | string): void {
const width = numberAsPixels(_width);
if (this._width === width) {
return;
}
this._width = width;
this.domNode.style.width = this._width;
}
public setHeight(_height: number | string): void {
const height = numberAsPixels(_height);
if (this._height === height) {
return;
}
this._height = height;
this.domNode.style.height = this._height;
}
public setTop(_top: number | string): void {
const top = numberAsPixels(_top);
if (this._top === top) {
return;
}
this._top = top;
this.domNode.style.top = this._top;
}
public setLeft(_left: number | string): void {
const left = numberAsPixels(_left);
if (this._left === left) {
return;
}
this._left = left;
this.domNode.style.left = this._left;
}
public setBottom(_bottom: number | string): void {
const bottom = numberAsPixels(_bottom);
if (this._bottom === bottom) {
return;
}
this._bottom = bottom;
this.domNode.style.bottom = this._bottom;
}
public setRight(_right: number | string): void {
const right = numberAsPixels(_right);
if (this._right === right) {
return;
}
this._right = right;
this.domNode.style.right = this._right;
}
public setPaddingTop(_paddingTop: number | string): void {
const paddingTop = numberAsPixels(_paddingTop);
if (this._paddingTop === paddingTop) {
return;
}
this._paddingTop = paddingTop;
this.domNode.style.paddingTop = this._paddingTop;
}
public setPaddingLeft(_paddingLeft: number | string): void {
const paddingLeft = numberAsPixels(_paddingLeft);
if (this._paddingLeft === paddingLeft) {
return;
}
this._paddingLeft = paddingLeft;
this.domNode.style.paddingLeft = this._paddingLeft;
}
public setPaddingBottom(_paddingBottom: number | string): void {
const paddingBottom = numberAsPixels(_paddingBottom);
if (this._paddingBottom === paddingBottom) {
return;
}
this._paddingBottom = paddingBottom;
this.domNode.style.paddingBottom = this._paddingBottom;
}
public setPaddingRight(_paddingRight: number | string): void {
const paddingRight = numberAsPixels(_paddingRight);
if (this._paddingRight === paddingRight) {
return;
}
this._paddingRight = paddingRight;
this.domNode.style.paddingRight = this._paddingRight;
}
public setFontFamily(fontFamily: string): void {
if (this._fontFamily === fontFamily) {
return;
}
this._fontFamily = fontFamily;
this.domNode.style.fontFamily = this._fontFamily;
}
public setFontWeight(fontWeight: string): void {
if (this._fontWeight === fontWeight) {
return;
}
this._fontWeight = fontWeight;
this.domNode.style.fontWeight = this._fontWeight;
}
public setFontSize(_fontSize: number | string): void {
const fontSize = numberAsPixels(_fontSize);
if (this._fontSize === fontSize) {
return;
}
this._fontSize = fontSize;
this.domNode.style.fontSize = this._fontSize;
}
public setFontStyle(fontStyle: string): void {
if (this._fontStyle === fontStyle) {
return;
}
this._fontStyle = fontStyle;
this.domNode.style.fontStyle = this._fontStyle;
}
public setFontFeatureSettings(fontFeatureSettings: string): void {
if (this._fontFeatureSettings === fontFeatureSettings) {
return;
}
this._fontFeatureSettings = fontFeatureSettings;
this.domNode.style.fontFeatureSettings = this._fontFeatureSettings;
}
public setFontVariationSettings(fontVariationSettings: string): void {
if (this._fontVariationSettings === fontVariationSettings) {
return;
}
this._fontVariationSettings = fontVariationSettings;
this.domNode.style.fontVariationSettings = this._fontVariationSettings;
}
public setTextDecoration(textDecoration: string): void {
if (this._textDecoration === textDecoration) {
return;
}
this._textDecoration = textDecoration;
this.domNode.style.textDecoration = this._textDecoration;
}
public setLineHeight(_lineHeight: number | string): void {
const lineHeight = numberAsPixels(_lineHeight);
if (this._lineHeight === lineHeight) {
return;
}
this._lineHeight = lineHeight;
this.domNode.style.lineHeight = this._lineHeight;
}
public setLetterSpacing(_letterSpacing: number | string): void {
const letterSpacing = numberAsPixels(_letterSpacing);
if (this._letterSpacing === letterSpacing) {
return;
}
this._letterSpacing = letterSpacing;
this.domNode.style.letterSpacing = this._letterSpacing;
}
public setClassName(className: string): void {
if (this._className === className) {
return;
}
this._className = className;
this.domNode.className = this._className;
}
public toggleClassName(className: string, shouldHaveIt?: boolean): void {
this.domNode.classList.toggle(className, shouldHaveIt);
this._className = this.domNode.className;
}
public setDisplay(display: string): void {
if (this._display === display) {
return;
}
this._display = display;
this.domNode.style.display = this._display;
}
public setPosition(position: string): void {
if (this._position === position) {
return;
}
this._position = position;
this.domNode.style.position = this._position;
}
public setVisibility(visibility: string): void {
if (this._visibility === visibility) {
return;
}
this._visibility = visibility;
this.domNode.style.visibility = this._visibility;
}
public setColor(color: string): void {
if (this._color === color) {
return;
}
this._color = color;
this.domNode.style.color = this._color;
}
public setBackgroundColor(backgroundColor: string): void {
if (this._backgroundColor === backgroundColor) {
return;
}
this._backgroundColor = backgroundColor;
this.domNode.style.backgroundColor = this._backgroundColor;
}
public setLayerHinting(layerHint: boolean): void {
if (this._layerHint === layerHint) {
return;
}
this._layerHint = layerHint;
this.domNode.style.transform = this._layerHint ? 'translate3d(0px, 0px, 0px)' : '';
}
public setBoxShadow(boxShadow: string): void {
if (this._boxShadow === boxShadow) {
return;
}
this._boxShadow = boxShadow;
this.domNode.style.boxShadow = boxShadow;
}
public setContain(contain: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint'): void {
if (this._contain === contain) {
return;
}
this._contain = contain;
(<any>this.domNode.style).contain = this._contain;
}
public setAttribute(name: string, value: string): void {
this.domNode.setAttribute(name, value);
}
public removeAttribute(name: string): void {
this.domNode.removeAttribute(name);
}
public appendChild(child: FastDomNode<T>): void {
this.domNode.appendChild(child.domNode);
}
public removeChild(child: FastDomNode<T>): void {
this.domNode.removeChild(child.domNode);
}
}
function numberAsPixels(value: number | string): string {
return (typeof value === 'number' ? `${value}px` : value);
}
export function createFastDomNode<T extends HTMLElement>(domNode: T): FastDomNode<T> {
return new FastDomNode(domNode);
}
+16
View File
@@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* 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';
@@ -0,0 +1,226 @@
/*---------------------------------------------------------------------------------------------
* 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;
}
}
@@ -0,0 +1,112 @@
/*---------------------------------------------------------------------------------------------
* 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 { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
export interface IPointerMoveCallback {
(event: PointerEvent): void;
}
export interface IOnStopCallback {
(browserEvent?: PointerEvent | KeyboardEvent): void;
}
export class GlobalPointerMoveMonitor implements IDisposable {
private readonly _hooks = new DisposableStore();
private _pointerMoveCallback: IPointerMoveCallback | null = null;
private _onStopCallback: IOnStopCallback | null = null;
public dispose(): void {
this.stopMonitoring(false);
this._hooks.dispose();
}
public stopMonitoring(invokeStopCallback: boolean, browserEvent?: PointerEvent | KeyboardEvent): void {
if (!this.isMonitoring()) {
// Not monitoring
return;
}
// Unhook
this._hooks.clear();
this._pointerMoveCallback = null;
const onStopCallback = this._onStopCallback;
this._onStopCallback = null;
if (invokeStopCallback && onStopCallback) {
onStopCallback(browserEvent);
}
}
public isMonitoring(): boolean {
return !!this._pointerMoveCallback;
}
public startMonitoring(
initialElement: Element,
pointerId: number,
initialButtons: number,
pointerMoveCallback: IPointerMoveCallback,
onStopCallback: IOnStopCallback
): void {
if (this.isMonitoring()) {
this.stopMonitoring(false);
}
this._pointerMoveCallback = pointerMoveCallback;
this._onStopCallback = onStopCallback;
let eventSource: Element | Window = initialElement;
try {
initialElement.setPointerCapture(pointerId);
this._hooks.add(toDisposable(() => {
try {
initialElement.releasePointerCapture(pointerId);
} catch (err) {
// See https://github.com/microsoft/vscode/issues/161731
//
// `releasePointerCapture` sometimes fails when being invoked with the exception:
// DOMException: Failed to execute 'releasePointerCapture' on 'Element':
// No active pointer with the given id is found.
//
// There's no need to do anything in case of failure
}
}));
} catch (err) {
// See https://github.com/microsoft/vscode/issues/144584
// See https://github.com/microsoft/vscode/issues/146947
// `setPointerCapture` sometimes fails when being invoked
// from a `mousedown` listener on macOS and Windows
// and it always fails on Linux with the exception:
// DOMException: Failed to execute 'setPointerCapture' on 'Element':
// No active pointer with the given id is found.
// In case of failure, we bind the listeners on the window
eventSource = dom.getWindow(initialElement);
}
this._hooks.add(dom.addDisposableListener(
eventSource,
dom.EventType.POINTER_MOVE,
(e) => {
if (e.buttons !== initialButtons) {
// Buttons state has changed in the meantime
this.stopMonitoring(true);
return;
}
e.preventDefault();
this._pointerMoveCallback!(e);
}
));
this._hooks.add(dom.addDisposableListener(
eventSource,
dom.EventType.POINTER_UP,
(e: PointerEvent) => this.stopMonitoring(true)
));
}
}
+32
View File
@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* 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();
}
}
+20
View File
@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* 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>;
}
+135
View File
@@ -0,0 +1,135 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Represents a window in a possible chain of iframes
*/
interface IWindowChainElement {
/**
* The window object for it
*/
readonly window: WeakRef<Window>;
/**
* The iframe element inside the window.parent corresponding to window
*/
readonly iframeElement: Element | null;
}
const sameOriginWindowChainCache = new WeakMap<Window, IWindowChainElement[] | null>();
function getParentWindowIfSameOrigin(w: Window): Window | null {
if (!w.parent || w.parent === w) {
return null;
}
// Cannot really tell if we have access to the parent window unless we try to access something in it
try {
const location = w.location;
const parentLocation = w.parent.location;
if (location.origin !== 'null' && parentLocation.origin !== 'null' && location.origin !== parentLocation.origin) {
return null;
}
} catch (e) {
return null;
}
return w.parent;
}
export class IframeUtils {
/**
* Returns a chain of embedded windows with the same origin (which can be accessed programmatically).
* Having a chain of length 1 might mean that the current execution environment is running outside of an iframe or inside an iframe embedded in a window with a different origin.
*/
private static getSameOriginWindowChain(targetWindow: Window): IWindowChainElement[] {
let windowChainCache = sameOriginWindowChainCache.get(targetWindow);
if (!windowChainCache) {
windowChainCache = [];
sameOriginWindowChainCache.set(targetWindow, windowChainCache);
let w: Window | null = targetWindow;
let parent: Window | null;
do {
parent = getParentWindowIfSameOrigin(w);
if (parent) {
windowChainCache.push({
window: new WeakRef(w),
iframeElement: w.frameElement || null
});
} else {
windowChainCache.push({
window: new WeakRef(w),
iframeElement: null
});
}
w = parent;
} while (w);
}
return windowChainCache.slice(0);
}
/**
* Returns the position of `childWindow` relative to `ancestorWindow`
*/
public static getPositionOfChildWindowRelativeToAncestorWindow(childWindow: Window, ancestorWindow: Window | null) {
if (!ancestorWindow || childWindow === ancestorWindow) {
return {
top: 0,
left: 0
};
}
let top = 0, left = 0;
const windowChain = this.getSameOriginWindowChain(childWindow);
for (const windowChainEl of windowChain) {
const windowInChain = windowChainEl.window.deref();
top += windowInChain?.scrollY ?? 0;
left += windowInChain?.scrollX ?? 0;
if (windowInChain === ancestorWindow) {
break;
}
if (!windowChainEl.iframeElement) {
break;
}
const boundingRect = windowChainEl.iframeElement.getBoundingClientRect();
top += boundingRect.top;
left += boundingRect.left;
}
return {
top: top,
left: left
};
}
}
/**
* Returns a sha-256 composed of `parentOrigin` and `salt` converted to base 32
*/
export async function parentOriginHash(parentOrigin: string, salt: string): Promise<string> {
// This same code is also inlined at `src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html`
if (!crypto.subtle) {
throw new Error(`'crypto.subtle' is not available so webviews will not work. This is likely because the editor is not running in a secure context (https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts).`);
}
const strData = JSON.stringify({ parentOrigin, salt });
const encoder = new TextEncoder();
const arrData = encoder.encode(strData);
const hash = await crypto.subtle.digest('sha-256', arrData);
return sha256AsBase32(hash);
}
function sha256AsBase32(bytes: ArrayBuffer): string {
const array = Array.from(new Uint8Array(bytes));
const hexArray = array.map(b => b.toString(16).padStart(2, '0')).join('');
// sha256 has 256 bits, so we need at most ceil(lg(2^256-1)/lg(32)) = 52 chars to represent it in base 32
return BigInt(`0x${hexArray}`).toString(32).padStart(52, '0');
}
+177
View File
@@ -0,0 +1,177 @@
/*---------------------------------------------------------------------------------------------
* 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));
}
}

Some files were not shown because too many files have changed in this diff Show More