diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index d3cb465581d..0abdff1acbe 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -9,7 +9,7 @@ "activationEvents": [], "main": "./out/extension", "engines": { - "vscode": "^1.25.0" + "vscode": "^1.55.0" }, "contributes": { "configuration": { diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts index 14947da78e2..629fbaace14 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts @@ -399,6 +399,118 @@ suite.skip('vscode API - webview', () => { assert.strictEqual(await vscode.env.clipboard.readText(), expectedText); }); } + + test('webviews should transfer ArrayBuffers to and from webviews', async () => { + const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { enableScripts: true, retainContextWhenHidden: true })); + const ready = getMessage(webview); + webview.webview.html = createHtmlDocumentWithBody(/*html*/` + `); + await ready; + + const responsePromise = getMessage(webview); + + const bufferLen = 100; + + { + const arrayBuffer = new ArrayBuffer(bufferLen); + const uint8Array = new Uint8Array(arrayBuffer); + for (let i = 0; i < bufferLen; ++i) { + uint8Array[i] = i; + } + webview.webview.postMessage({ + type: 'add1', + array: arrayBuffer + }); + } + { + const response = await responsePromise; + assert.ok(response.array instanceof ArrayBuffer); + + const uint8Array = new Uint8Array(response.array); + for (let i = 0; i < bufferLen; ++i) { + assert.strictEqual(uint8Array[i], i + 1); + } + } + }); + + test('webviews should transfer Typed arrays to and from webviews', async () => { + const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { enableScripts: true, retainContextWhenHidden: true })); + const ready = getMessage(webview); + webview.webview.html = createHtmlDocumentWithBody(/*html*/` + `); + await ready; + + const responsePromise = getMessage(webview); + + const bufferLen = 100; + { + const arrayBuffer = new ArrayBuffer(bufferLen); + const uint8Array = new Uint8Array(arrayBuffer); + const uint16Array = new Uint16Array(arrayBuffer); + for (let i = 0; i < uint16Array.length; ++i) { + uint16Array[i] = i; + } + + webview.webview.postMessage({ + type: 'add1', + array1: uint8Array, + array2: uint16Array, + }); + } + { + const response = await responsePromise; + + assert.ok(response.array1 instanceof Uint8Array); + assert.ok(response.array2 instanceof Uint16Array); + assert.ok(response.array1.buffer === response.array2.buffer); + + const uint8Array = response.array1; + for (let i = 0; i < bufferLen; ++i) { + if (i % 2 === 0) { + assert.strictEqual(uint8Array[i], Math.floor(i / 2) + 1); + } else { + assert.strictEqual(uint8Array[i], 0); + } + } + } + }); }); function createHtmlDocumentWithBody(body: string): string { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 9474a7533f4..e5ffe981e95 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2850,6 +2850,28 @@ declare module 'vscode' { */ readonly triggerKind: CodeActionTriggerKind; } + + //#endregion + + //#region https://github.com/microsoft/vscode/issues/115807 + + export interface Webview { + /** + * @param message A json serializable message to send to the webview. + * + * For older versions of vscode, if an `ArrayBuffer` is included in `message`, + * it will not be serialized properly and will not be received by the webview. + * Similarly any TypedArrays, such as a `Uint8Array`, will be very inefficiently + * serialized and will also not be recreated as a typed array inside the webview. + * + * However if your extension targets vscode 1.55+ in the `engines` field of its + * `package.json` any `ArrayBuffer` values that appear in `message` will be more + * efficiently transferred to the webview and will also be recreated inside of + * the webview. + */ + postMessage(message: any): Thenable; + } + //#endregion //#region https://github.com/microsoft/vscode/issues/115616 @alexr00 diff --git a/src/vs/workbench/api/browser/mainThreadCodeInsets.ts b/src/vs/workbench/api/browser/mainThreadCodeInsets.ts index 140fa60e9d4..746d31461f7 100644 --- a/src/vs/workbench/api/browser/mainThreadCodeInsets.ts +++ b/src/vs/workbench/api/browser/mainThreadCodeInsets.ts @@ -105,7 +105,7 @@ export class MainThreadEditorInsets implements MainThreadEditorInsetsShape { disposables.add(editor.onDidDispose(remove)); disposables.add(webviewZone); disposables.add(webview); - disposables.add(webview.onMessage(msg => this._proxy.$onDidReceiveMessage(handle, msg))); + disposables.add(webview.onMessage(msg => this._proxy.$onDidReceiveMessage(handle, msg.message))); this._insets.set(handle, webviewZone); } diff --git a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts index 6693da4f051..fec3341bc32 100644 --- a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts @@ -99,12 +99,12 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc this._editorProviders.clear(); } - public $registerTextEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: extHostProtocol.IWebviewPanelOptions, capabilities: extHostProtocol.CustomTextEditorCapabilities): void { - this.registerEditorProvider(CustomEditorModelType.Text, reviveWebviewExtension(extensionData), viewType, options, capabilities, true); + public $registerTextEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: extHostProtocol.IWebviewPanelOptions, capabilities: extHostProtocol.CustomTextEditorCapabilities, serializeBuffersForPostMessage: boolean): void { + this.registerEditorProvider(CustomEditorModelType.Text, reviveWebviewExtension(extensionData), viewType, options, capabilities, true, serializeBuffersForPostMessage); } - public $registerCustomEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: extHostProtocol.IWebviewPanelOptions, supportsMultipleEditorsPerDocument: boolean): void { - this.registerEditorProvider(CustomEditorModelType.Custom, reviveWebviewExtension(extensionData), viewType, options, {}, supportsMultipleEditorsPerDocument); + public $registerCustomEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: extHostProtocol.IWebviewPanelOptions, supportsMultipleEditorsPerDocument: boolean, serializeBuffersForPostMessage: boolean): void { + this.registerEditorProvider(CustomEditorModelType.Custom, reviveWebviewExtension(extensionData), viewType, options, {}, supportsMultipleEditorsPerDocument, serializeBuffersForPostMessage); } private registerEditorProvider( @@ -114,6 +114,7 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc options: extHostProtocol.IWebviewPanelOptions, capabilities: extHostProtocol.CustomTextEditorCapabilities, supportsMultipleEditorsPerDocument: boolean, + serializeBuffersForPostMessage: boolean, ): void { if (this._editorProviders.has(viewType)) { throw new Error(`Provider for ${viewType} already registered`); @@ -133,7 +134,7 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc const handle = webviewInput.id; const resource = webviewInput.resource; - this.mainThreadWebviewPanels.addWebviewInput(handle, webviewInput); + this.mainThreadWebviewPanels.addWebviewInput(handle, webviewInput, { serializeBuffersForPostMessage }); webviewInput.webview.options = options; webviewInput.webview.extension = extension; diff --git a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts index 0d2dc68dc3b..f7177ef7717 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts @@ -137,9 +137,9 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc public get webviewInputs(): Iterable { return this._webviewInputs; } - public addWebviewInput(handle: extHostProtocol.WebviewHandle, input: WebviewInput): void { + public addWebviewInput(handle: extHostProtocol.WebviewHandle, input: WebviewInput, options: { serializeBuffersForPostMessage: boolean }): void { this._webviewInputs.add(handle, input); - this._mainThreadWebviews.addWebview(handle, input.webview); + this._mainThreadWebviews.addWebview(handle, input.webview, options); input.webview.onDidDispose(() => { this._proxy.$onDidDisposeWebviewPanel(handle).finally(() => { @@ -156,6 +156,7 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc title: string; webviewOptions: extHostProtocol.IWebviewOptions; panelOptions: extHostProtocol.IWebviewPanelOptions; + serializeBuffersForPostMessage: boolean; }, showOptions: { viewColumn?: EditorGroupColumn, preserveFocus?: boolean; }, ): void { @@ -168,7 +169,7 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc const extension = reviveWebviewExtension(extensionData); const webview = this._webviewWorkbenchService.createWebview(handle, this.webviewPanelViewType.fromExternal(viewType), initData.title, mainThreadShowOptions, reviveWebviewOptions(initData.panelOptions), reviveWebviewContentOptions(initData.webviewOptions), extension); - this.addWebviewInput(handle, webview); + this.addWebviewInput(handle, webview, { serializeBuffersForPostMessage: initData.serializeBuffersForPostMessage }); /* __GDPR__ "webviews:createWebviewPanel" : { @@ -205,7 +206,7 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc } } - public $registerSerializer(viewType: string): void { + public $registerSerializer(viewType: string, options: { serializeBuffersForPostMessage: boolean }): void { if (this._revivers.has(viewType)) { throw new Error(`Reviver for ${viewType} already registered`); } @@ -223,7 +224,7 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc const handle = webviewInput.id; - this.addWebviewInput(handle, webviewInput); + this.addWebviewInput(handle, webviewInput, options); let state = undefined; if (webviewInput.webview.state) { diff --git a/src/vs/workbench/api/browser/mainThreadWebviewViews.ts b/src/vs/workbench/api/browser/mainThreadWebviewViews.ts index e369b4f088d..d2d670a6089 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviewViews.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviewViews.ts @@ -55,7 +55,7 @@ export class MainThreadWebviewsViews extends Disposable implements extHostProtoc public $registerWebviewViewProvider( extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, - options?: { retainContextWhenHidden?: boolean } + options: { retainContextWhenHidden?: boolean, serializeBuffersForPostMessage: boolean } ): void { if (this._webviewViewProviders.has(viewType)) { throw new Error(`View provider for ${viewType} already registered`); @@ -68,7 +68,7 @@ export class MainThreadWebviewsViews extends Disposable implements extHostProtoc const handle = webviewView.webview.id; this._webviewViews.set(handle, webviewView); - this.mainThreadWebviews.addWebview(handle, webviewView.webview); + this.mainThreadWebviews.addWebview(handle, webviewView.webview, { serializeBuffersForPostMessage: options.serializeBuffersForPostMessage }); let state = undefined; if (webviewView.webview.state) { diff --git a/src/vs/workbench/api/browser/mainThreadWebviews.ts b/src/vs/workbench/api/browser/mainThreadWebviews.ts index b49181ee42d..86550fa8e5a 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviews.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviews.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { VSBuffer } from 'vs/base/common/buffer'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { isWeb } from 'vs/base/common/platform'; @@ -13,6 +14,8 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IProductService } from 'vs/platform/product/common/productService'; import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; +import { serializeMessage } from 'vs/workbench/api/common/extHostWebview'; +import { deserializeWebviewMessage } from 'vs/workbench/api/common/extHostWebviewMessaging'; import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; export class MainThreadWebviews extends Disposable implements extHostProtocol.MainThreadWebviewsShape { @@ -39,13 +42,13 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviews); } - public addWebview(handle: extHostProtocol.WebviewHandle, webview: WebviewOverlay): void { + public addWebview(handle: extHostProtocol.WebviewHandle, webview: WebviewOverlay, options: { serializeBuffersForPostMessage: boolean }): void { if (this._webviews.has(handle)) { throw new Error('Webview already registered'); } this._webviews.set(handle, webview); - this.hookupWebviewEventDelegate(handle, webview); + this.hookupWebviewEventDelegate(handle, webview, options); } public $setHtml(handle: extHostProtocol.WebviewHandle, value: string): void { @@ -58,17 +61,23 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma webview.contentOptions = reviveWebviewContentOptions(options); } - public async $postMessage(handle: extHostProtocol.WebviewHandle, message: any): Promise { + public async $postMessage(handle: extHostProtocol.WebviewHandle, jsonMessage: string, ...buffers: VSBuffer[]): Promise { const webview = this.getWebview(handle); - webview.postMessage(message); + const { message, arrayBuffers } = deserializeWebviewMessage(jsonMessage, buffers); + webview.postMessage(message, arrayBuffers); return true; } - private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewHandle, webview: WebviewOverlay) { + private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewHandle, webview: WebviewOverlay, options: { serializeBuffersForPostMessage: boolean }) { const disposables = new DisposableStore(); disposables.add(webview.onDidClickLink((uri) => this.onDidClickLink(handle, uri))); - disposables.add(webview.onMessage((message: any) => { this._proxy.$onMessage(handle, message); })); + + disposables.add(webview.onMessage((message) => { + const serialized = serializeMessage(message.message, options); + this._proxy.$onMessage(handle, serialized.message, ...serialized.buffers); + })); + disposables.add(webview.onMissingCsp((extension: ExtensionIdentifier) => this._proxy.$onMissingCsp(handle, extension.value))); disposables.add(webview.onDidDispose(() => { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 7d4e886828a..5362193a889 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -671,10 +671,39 @@ export interface CustomTextEditorCapabilities { readonly supportsMove?: boolean; } +export const enum WebviewMessageArrayBufferViewType { + Int8Array = 1, + Uint8Array = 2, + Uint8ClampedArray = 3, + Int16Array = 4, + Uint16Array = 5, + Int32Array = 6, + Uint32Array = 7, + Float32Array = 8, + Float64Array = 9, + BigInt64Array = 10, + BigUint64Array = 11, +} + +export interface WebviewMessageArrayBufferReference { + readonly $$vscode_array_buffer_reference$$: true, + + readonly index: number; + + /** + * Tracks if the reference is to a view instead of directly to an ArrayBuffer. + */ + readonly view?: { + readonly type: WebviewMessageArrayBufferViewType; + readonly byteLength: number; + readonly byteOffset: number; + }; +} + export interface MainThreadWebviewsShape extends IDisposable { $setHtml(handle: WebviewHandle, value: string): void; $setOptions(handle: WebviewHandle, options: IWebviewOptions): void; - $postMessage(handle: WebviewHandle, value: any): Promise + $postMessage(handle: WebviewHandle, value: any, ...buffers: VSBuffer[]): Promise } export interface MainThreadWebviewPanelsShape extends IDisposable { @@ -686,6 +715,7 @@ export interface MainThreadWebviewPanelsShape extends IDisposable { title: string; webviewOptions: IWebviewOptions; panelOptions: IWebviewPanelOptions; + serializeBuffersForPostMessage: boolean; }, showOptions: WebviewPanelShowOptions, ): void; @@ -694,13 +724,13 @@ export interface MainThreadWebviewPanelsShape extends IDisposable { $setTitle(handle: WebviewHandle, value: string): void; $setIconPath(handle: WebviewHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void; - $registerSerializer(viewType: string): void; + $registerSerializer(viewType: string, options: { serializeBuffersForPostMessage: boolean }): void; $unregisterSerializer(viewType: string): void; } export interface MainThreadCustomEditorsShape extends IDisposable { - $registerTextEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: IWebviewPanelOptions, capabilities: CustomTextEditorCapabilities): void; - $registerCustomEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: IWebviewPanelOptions, supportsMultipleEditorsPerDocument: boolean): void; + $registerTextEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: IWebviewPanelOptions, capabilities: CustomTextEditorCapabilities, serializeBuffersForPostMessage: boolean): void; + $registerCustomEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: IWebviewPanelOptions, supportsMultipleEditorsPerDocument: boolean, serializeBuffersForPostMessage: boolean): void; $unregisterEditorProvider(viewType: string): void; $onDidEdit(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void; @@ -708,7 +738,7 @@ export interface MainThreadCustomEditorsShape extends IDisposable { } export interface MainThreadWebviewViewsShape extends IDisposable { - $registerWebviewViewProvider(extension: WebviewExtensionDescription, viewType: string, options?: { retainContextWhenHidden?: boolean }): void; + $registerWebviewViewProvider(extension: WebviewExtensionDescription, viewType: string, options: { retainContextWhenHidden?: boolean, serializeBuffersForPostMessage: boolean }): void; $unregisterWebviewViewProvider(viewType: string): void; $setWebviewViewTitle(handle: WebviewHandle, value: string | undefined): void; @@ -726,7 +756,7 @@ export interface WebviewPanelViewStateData { } export interface ExtHostWebviewsShape { - $onMessage(handle: WebviewHandle, message: any): void; + $onMessage(handle: WebviewHandle, jsonSerializedMessage: string, ...buffers: VSBuffer[]): void; $onMissingCsp(handle: WebviewHandle, extensionId: string): void; } diff --git a/src/vs/workbench/api/common/extHostCustomEditors.ts b/src/vs/workbench/api/common/extHostCustomEditors.ts index ab9e63c1c15..69557c2a49b 100644 --- a/src/vs/workbench/api/common/extHostCustomEditors.ts +++ b/src/vs/workbench/api/common/extHostCustomEditors.ts @@ -14,7 +14,7 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions' import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; -import { ExtHostWebviews, toExtensionData } from 'vs/workbench/api/common/extHostWebview'; +import { ExtHostWebviews, shouldSerializeBuffersForPostMessage, toExtensionData } from 'vs/workbench/api/common/extHostWebview'; import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels'; import { EditorGroupColumn } from 'vs/workbench/common/editor'; import type * as vscode from 'vscode'; @@ -183,7 +183,7 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor disposables.add(this._editorProviders.addTextProvider(viewType, extension, provider)); this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, { supportsMove: !!provider.moveCustomTextEditor, - }); + }, shouldSerializeBuffersForPostMessage(extension)); } else { disposables.add(this._editorProviders.addCustomProvider(viewType, extension, provider)); @@ -199,7 +199,7 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor })); } - this._proxy.$registerCustomEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, !!options.supportsMultipleEditorsPerDocument); + this._proxy.$registerCustomEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, !!options.supportsMultipleEditorsPerDocument, shouldSerializeBuffersForPostMessage(extension)); } return extHostTypes.Disposable.from( diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index d6b9376d018..b4a280b116b 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -3,11 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { VSBuffer } from 'vs/base/common/buffer'; import { Emitter, Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; +import { deserializeWebviewMessage } from 'vs/workbench/api/common/extHostWebviewMessaging'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import type * as vscode from 'vscode'; @@ -28,6 +31,8 @@ export class ExtHostWebview implements vscode.Webview { #isDisposed: boolean = false; #hasCalledAsWebviewUri = false; + #serializeBuffersForPostMessage = false; + constructor( handle: extHostProtocol.WebviewHandle, proxy: extHostProtocol.MainThreadWebviewsShape, @@ -43,6 +48,7 @@ export class ExtHostWebview implements vscode.Webview { this.#initData = initData; this.#workspace = workspace; this.#extension = extension; + this.#serializeBuffersForPostMessage = shouldSerializeBuffersForPostMessage(extension); this.#deprecationService = deprecationService; } @@ -104,7 +110,8 @@ export class ExtHostWebview implements vscode.Webview { if (this.#isDisposed) { return false; } - return this.#proxy.$postMessage(this.#handle, message); + const serialized = serializeMessage(message, { serializeBuffersForPostMessage: this.#serializeBuffersForPostMessage }); + return this.#proxy.$postMessage(this.#handle, serialized.message, ...serialized.buffers); } private assertNotDisposed() { @@ -114,6 +121,49 @@ export class ExtHostWebview implements vscode.Webview { } } +export function shouldSerializeBuffersForPostMessage(extension: IExtensionDescription): boolean { + if (!extension.enableProposedApi) { + return false; + } + + try { + const version = normalizeVersion(parseVersion(extension.engines.vscode)); + return !!version && version.majorBase >= 1 && version.minorBase >= 56; + } catch { + return false; + } +} + +export function serializeMessage(message: any, options: { serializeBuffersForPostMessage?: boolean }): { message: string, buffers: VSBuffer[] } { + if (options.serializeBuffersForPostMessage) { + // Extract all ArrayBuffers from the message and replace them with references. + const vsBuffers: Array<{ original: ArrayBuffer, vsBuffer: VSBuffer }> = []; + + const replacer = (_key: string, value: any) => { + if (value && value instanceof ArrayBuffer) { + let index = vsBuffers.findIndex(x => x.original === value); + if (index === -1) { + const bytes = new Uint8Array(value); + const vsBuffer = VSBuffer.wrap(bytes); + index = vsBuffers.length; + vsBuffers.push({ original: value, vsBuffer }); + } + + return { + $$vscode_array_buffer_reference$$: true, + index, + }; + } + return value; + }; + + const serializedMessage = JSON.stringify(message, replacer); + return { message: serializedMessage, buffers: vsBuffers.map(x => x.vsBuffer) }; + } else { + return { message: JSON.stringify(message), buffers: [] }; + } +} + export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { private readonly _webviewProxy: extHostProtocol.MainThreadWebviewsShape; @@ -132,10 +182,12 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { public $onMessage( handle: extHostProtocol.WebviewHandle, - message: any + jsonMessage: string, + ...buffers: VSBuffer[] ): void { const webview = this.getWebview(handle); if (webview) { + const { message } = deserializeWebviewMessage(jsonMessage, buffers); webview._onMessageEmitter.fire(message); } } diff --git a/src/vs/workbench/api/common/extHostWebviewMessaging.ts b/src/vs/workbench/api/common/extHostWebviewMessaging.ts new file mode 100644 index 00000000000..06e76ab5b5e --- /dev/null +++ b/src/vs/workbench/api/common/extHostWebviewMessaging.ts @@ -0,0 +1,122 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as extHostProtocol from './extHost.protocol'; + +class ArrayBufferSet { + public readonly buffers: ArrayBuffer[] = []; + + public add(buffer: ArrayBuffer): number { + let index = this.buffers.indexOf(buffer); + if (index < 0) { + index = this.buffers.length; + this.buffers.push(buffer); + } + return index; + } +} + +export function serializeWebviewMessage( + message: any, + transfer?: readonly ArrayBuffer[] +): { message: string, buffers: VSBuffer[] } { + if (transfer) { + // Extract all ArrayBuffers from the message and replace them with references. + const arrayBuffers = new ArrayBufferSet(); + + const replacer = (_key: string, value: any) => { + if (value instanceof ArrayBuffer) { + const index = arrayBuffers.add(value); + return { + $$vscode_array_buffer_reference$$: true, + index, + }; + } else if (ArrayBuffer.isView(value)) { + const type = getTypedArrayType(value); + if (type) { + const index = arrayBuffers.add(value.buffer); + return { + $$vscode_array_buffer_reference$$: true, + index, + view: { + type: type, + byteLength: value.byteLength, + byteOffset: value.byteOffset, + } + }; + } + } + + return value; + }; + + const serializedMessage = JSON.stringify(message, replacer); + + const buffers = arrayBuffers.buffers.map(arrayBuffer => { + const bytes = new Uint8Array(arrayBuffer); + return VSBuffer.wrap(bytes); + }); + + return { message: serializedMessage, buffers }; + } else { + return { message: JSON.stringify(message), buffers: [] }; + } +} + +function getTypedArrayType(value: ArrayBufferView): extHostProtocol.WebviewMessageArrayBufferViewType | undefined { + switch (value.constructor.name) { + case 'Int8Array': return extHostProtocol.WebviewMessageArrayBufferViewType.Int8Array; + case 'Uint8Array': return extHostProtocol.WebviewMessageArrayBufferViewType.Uint8Array; + case 'Uint8ClampedArray': return extHostProtocol.WebviewMessageArrayBufferViewType.Uint8ClampedArray; + case 'Int16Array': return extHostProtocol.WebviewMessageArrayBufferViewType.Int16Array; + case 'Uint16Array': return extHostProtocol.WebviewMessageArrayBufferViewType.Uint16Array; + case 'Int32Array': return extHostProtocol.WebviewMessageArrayBufferViewType.Int32Array; + case 'Uint32Array': return extHostProtocol.WebviewMessageArrayBufferViewType.Uint32Array; + case 'Float32Array': return extHostProtocol.WebviewMessageArrayBufferViewType.Float32Array; + case 'Float64Array': return extHostProtocol.WebviewMessageArrayBufferViewType.Float64Array; + case 'BigInt64Array': return extHostProtocol.WebviewMessageArrayBufferViewType.BigInt64Array; + case 'BigUint64Array': return extHostProtocol.WebviewMessageArrayBufferViewType.BigUint64Array; + } + return undefined; +} + +export function deserializeWebviewMessage(jsonMessage: string, buffers: VSBuffer[]): { message: any, arrayBuffers: ArrayBuffer[] } { + const arrayBuffers: ArrayBuffer[] = buffers.map(buffer => { + const arrayBuffer = new ArrayBuffer(buffer.byteLength); + const uint8Array = new Uint8Array(arrayBuffer); + uint8Array.set(buffer.buffer); + return arrayBuffer; + }); + + const reviver = !buffers.length ? undefined : (_key: string, value: any) => { + if (typeof value === 'object' && (value as extHostProtocol.WebviewMessageArrayBufferReference).$$vscode_array_buffer_reference$$) { + const ref = value as extHostProtocol.WebviewMessageArrayBufferReference; + const { index } = ref; + const arrayBuffer = arrayBuffers[index]; + if (ref.view) { + switch (ref.view.type) { + case extHostProtocol.WebviewMessageArrayBufferViewType.Int8Array: return new Int8Array(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / Int8Array.BYTES_PER_ELEMENT); + case extHostProtocol.WebviewMessageArrayBufferViewType.Uint8Array: return new Uint8Array(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / Uint8Array.BYTES_PER_ELEMENT); + case extHostProtocol.WebviewMessageArrayBufferViewType.Uint8ClampedArray: return new Uint8ClampedArray(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / Uint8ClampedArray.BYTES_PER_ELEMENT); + case extHostProtocol.WebviewMessageArrayBufferViewType.Int16Array: return new Int16Array(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / Int16Array.BYTES_PER_ELEMENT); + case extHostProtocol.WebviewMessageArrayBufferViewType.Uint16Array: return new Uint16Array(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / Uint16Array.BYTES_PER_ELEMENT); + case extHostProtocol.WebviewMessageArrayBufferViewType.Int32Array: return new Int32Array(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / Int32Array.BYTES_PER_ELEMENT); + case extHostProtocol.WebviewMessageArrayBufferViewType.Uint32Array: return new Uint32Array(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / Uint32Array.BYTES_PER_ELEMENT); + case extHostProtocol.WebviewMessageArrayBufferViewType.Float32Array: return new Float32Array(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / Float32Array.BYTES_PER_ELEMENT); + case extHostProtocol.WebviewMessageArrayBufferViewType.Float64Array: return new Float64Array(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / Float64Array.BYTES_PER_ELEMENT); + case extHostProtocol.WebviewMessageArrayBufferViewType.BigInt64Array: return new BigInt64Array(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / BigInt64Array.BYTES_PER_ELEMENT); + case extHostProtocol.WebviewMessageArrayBufferViewType.BigUint64Array: return new BigUint64Array(arrayBuffer, ref.view.byteOffset, ref.view.byteLength / BigUint64Array.BYTES_PER_ELEMENT); + default: throw new Error('Unknown array buffer view type'); + } + } + return arrayBuffer; + } + return value; + }; + + const message = JSON.parse(jsonMessage, reviver); + return { message, arrayBuffers }; +} diff --git a/src/vs/workbench/api/common/extHostWebviewPanels.ts b/src/vs/workbench/api/common/extHostWebviewPanels.ts index 3e30cffcf47..ace45ef742b 100644 --- a/src/vs/workbench/api/common/extHostWebviewPanels.ts +++ b/src/vs/workbench/api/common/extHostWebviewPanels.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; -import { serializeWebviewOptions, ExtHostWebview, ExtHostWebviews, toExtensionData } from 'vs/workbench/api/common/extHostWebview'; +import { serializeWebviewOptions, ExtHostWebview, ExtHostWebviews, toExtensionData, shouldSerializeBuffersForPostMessage } from 'vs/workbench/api/common/extHostWebview'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { EditorGroupColumn } from 'vs/workbench/common/editor'; import type * as vscode from 'vscode'; @@ -199,11 +199,13 @@ export class ExtHostWebviewPanels implements extHostProtocol.ExtHostWebviewPanel preserveFocus: typeof showOptions === 'object' && !!showOptions.preserveFocus }; + const serializeBuffersForPostMessage = shouldSerializeBuffersForPostMessage(extension); const handle = ExtHostWebviewPanels.newHandle(); this._proxy.$createWebviewPanel(toExtensionData(extension), handle, viewType, { title, panelOptions: serializeWebviewPanelOptions(options), webviewOptions: serializeWebviewOptions(extension, this.workspace, options), + serializeBuffersForPostMessage, }, webviewShowOptions); const webview = this.webviews.createNewWebview(handle, options, extension); @@ -263,7 +265,9 @@ export class ExtHostWebviewPanels implements extHostProtocol.ExtHostWebviewPanel } this._serializers.set(viewType, { serializer, extension }); - this._proxy.$registerSerializer(viewType); + this._proxy.$registerSerializer(viewType, { + serializeBuffersForPostMessage: shouldSerializeBuffersForPostMessage(extension) + }); return new extHostTypes.Disposable(() => { this._serializers.delete(viewType); diff --git a/src/vs/workbench/api/common/extHostWebviewView.ts b/src/vs/workbench/api/common/extHostWebviewView.ts index 205ad24c09e..6f28d51b938 100644 --- a/src/vs/workbench/api/common/extHostWebviewView.ts +++ b/src/vs/workbench/api/common/extHostWebviewView.ts @@ -146,7 +146,10 @@ export class ExtHostWebviewViews implements extHostProtocol.ExtHostWebviewViewsS } this._viewProviders.set(viewType, { provider, extension }); - this._proxy.$registerWebviewViewProvider(toExtensionData(extension), viewType, webviewOptions); + this._proxy.$registerWebviewViewProvider(toExtensionData(extension), viewType, { + retainContextWhenHidden: webviewOptions?.retainContextWhenHidden, + serializeBuffersForPostMessage: false, + }); return new extHostTypes.Disposable(() => { this._viewProviders.delete(viewType); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 87649ab0baa..88988a7ec94 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -800,7 +800,8 @@ var requirejs = (function() { } })); - this._register(this.webview.onMessage((data: FromWebviewMessage | { readonly __vscode_notebook_message: undefined }) => { + this._register(this.webview.onMessage((message) => { + const data: FromWebviewMessage | { readonly __vscode_notebook_message: undefined } = message.message; if (this._disposed) { return; } @@ -1020,7 +1021,8 @@ var requirejs = (function() { resolveFunc = resolve; }); - const dispose = webview.onMessage((data: FromWebviewMessage) => { + const dispose = webview.onMessage((message) => { + const data: FromWebviewMessage = message.message; if (data.__vscode_notebook_message && data.type === 'initialized') { resolveFunc(); dispose.dispose(); @@ -1230,7 +1232,7 @@ var requirejs = (function() { // TODO: use proper handler const p = new Promise(resolve => { this.webview?.onMessage(e => { - if (e.type === 'initializedMarkdownPreview') { + if (e.message.type === 'initializedMarkdownPreview') { resolve(); } }); diff --git a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts index 2c3f6c87d3e..086e64311e5 100644 --- a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts @@ -13,7 +13,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; -import { areWebviewContentOptionsEqual, WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; +import { areWebviewContentOptionsEqual, WebviewContentOptions, WebviewExtensionDescription, WebviewMessageReceivedEvent, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export const enum WebviewMessageChannels { @@ -120,8 +120,11 @@ export abstract class BaseWebview extends Disposable { this._onDidClickLink.fire(uri); })); - this._register(this.on(WebviewMessageChannels.onmessage, (data: any) => { - this._onMessage.fire(data); + this._register(this.on(WebviewMessageChannels.onmessage, (data: { message: any, transfer?: ArrayBuffer[] }) => { + this._onMessage.fire({ + message: data.message, + transfer: data.transfer, + }); })); this._register(this.on(WebviewMessageChannels.didScroll, (scrollYPercentage: number) => { @@ -188,7 +191,7 @@ export abstract class BaseWebview extends Disposable { private readonly _onDidReload = this._register(new Emitter()); public readonly onDidReload = this._onDidReload.event; - private readonly _onMessage = this._register(new Emitter()); + private readonly _onMessage = this._register(new Emitter()); public readonly onMessage = this._onMessage.event; private readonly _onDidScroll = this._register(new Emitter<{ readonly scrollYPercentage: number; }>()); @@ -209,8 +212,8 @@ export abstract class BaseWebview extends Disposable { private readonly _onDidDispose = this._register(new Emitter()); public readonly onDidDispose = this._onDidDispose.event; - public postMessage(data: any): void { - this._send('message', data); + public postMessage(message: any, transfer?: ArrayBuffer[]): void { + this._send('message', { message, transfer }); } protected _send(channel: string, data?: any): void { diff --git a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts index 102f7132e9c..1a9d6c2f159 100644 --- a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts +++ b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts @@ -7,12 +7,12 @@ import { Dimension } from 'vs/base/browser/dom'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { memoize } from 'vs/base/common/decorators'; import { Emitter, Event } from 'vs/base/common/event'; -import { URI } from 'vs/base/common/uri'; import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { IWebviewService, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_ENABLED, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE, Webview, WebviewContentOptions, WebviewElement, WebviewExtensionDescription, WebviewOptions, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; +import { IWebviewService, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_ENABLED, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE, Webview, WebviewContentOptions, WebviewElement, WebviewExtensionDescription, WebviewMessageReceivedEvent, WebviewOptions, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; /** * Webview editor overlay that creates and destroys the underlying webview as needed. @@ -22,7 +22,7 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv private readonly _onDidWheel = this._register(new Emitter()); public readonly onDidWheel = this._onDidWheel.event; - private readonly _pendingMessages = new Set(); + private readonly _pendingMessages = new Set<{ readonly message: any, readonly transfer?: readonly ArrayBuffer[] }>(); private readonly _webview = this._register(new MutableDisposable()); private readonly _webviewEvents = this._register(new DisposableStore()); @@ -182,7 +182,7 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv this._onDidUpdateState.fire(state); })); - this._pendingMessages.forEach(msg => webview.postMessage(msg)); + this._pendingMessages.forEach(msg => webview.postMessage(msg.message, msg.transfer)); this._pendingMessages.clear(); } @@ -244,17 +244,17 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv private readonly _onDidUpdateState = this._register(new Emitter()); public readonly onDidUpdateState: Event = this._onDidUpdateState.event; - private readonly _onMessage = this._register(new Emitter()); - public readonly onMessage: Event = this._onMessage.event; + private readonly _onMessage = this._register(new Emitter()); + public readonly onMessage = this._onMessage.event; private readonly _onMissingCsp = this._register(new Emitter()); public readonly onMissingCsp: Event = this._onMissingCsp.event; - postMessage(data: any): void { + public postMessage(message: any, transfer?: readonly ArrayBuffer[]): void { if (this._webview.value) { - this._webview.value.postMessage(data); + this._webview.value.postMessage(message, transfer); } else { - this._pendingMessages.add(data); + this._pendingMessages.add({ message, transfer }); } } diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index e7fb4afe174..6224340b658 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -150,13 +150,13 @@ */ function getVsCodeApiScript(allowMultipleAPIAcquire, useParentPostMessage, state) { const encodedState = state ? encodeURIComponent(state) : undefined; - return ` + return /* js */` globalThis.acquireVsCodeApi = (function() { const originalPostMessage = window.parent['${useParentPostMessage ? 'postMessage' : vscodePostMessageFuncName}'].bind(window.parent); - const doPostMessage = (channel, data) => { + const doPostMessage = (channel, data, transfer) => { ${useParentPostMessage - ? `originalPostMessage({ command: channel, data: data }, '*');` - : `originalPostMessage(channel, data);` + ? `originalPostMessage({ command: channel, data: data }, '*', transfer);` + : `originalPostMessage(channel, data, transfer);` } }; @@ -170,8 +170,8 @@ } acquired = true; return Object.freeze({ - postMessage: function(msg) { - doPostMessage('onmessage', msg); + postMessage: function(message, transfer) { + doPostMessage('onmessage', { message, transfer }, transfer); }, setState: function(newState) { state = newState; @@ -197,6 +197,7 @@ // state let firstLoad = true; let loadTimeout; + /** @type {Array<{ readonly message: any, transfer?: ArrayBuffer[] }>} */ let pendingMessages = []; const initData = { @@ -622,8 +623,8 @@ contentWindow.focus(); } - pendingMessages.forEach((data) => { - contentWindow.postMessage(data, '*'); + pendingMessages.forEach((message) => { + contentWindow.postMessage(message.message, '*', message.transfer); }); pendingMessages = []; } @@ -678,12 +679,12 @@ }); // Forward message to the embedded iframe - host.onMessage('message', (_event, data) => { + host.onMessage('message', (_event, /** @type {{message: any, transfer?: ArrayBuffer[] }} */ data) => { const pending = getPendingFrame(); if (!pending) { const target = getActiveFrame(); if (target) { - target.contentWindow.postMessage(data, '*'); + target.contentWindow.postMessage(data.message, '*', data.transfer); return; } } @@ -707,7 +708,7 @@ onBlur: () => host.postMessage('did-blur') }); - (/** @type {any} */ (window))[vscodePostMessageFuncName] = (command, data) => { + (/** @type {any} */ (window))[vscodePostMessageFuncName] = (command, data, transfer) => { switch (command) { case 'onmessage': case 'do-update-state': diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index 63b8303ad67..5764a055c11 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -101,6 +101,11 @@ export interface IDataLinkClickEvent { downloadName?: string; } +export interface WebviewMessageReceivedEvent { + readonly message: any; + readonly transfer?: readonly ArrayBuffer[]; +} + export interface Webview extends IDisposable { readonly id: string; @@ -123,10 +128,10 @@ export interface Webview extends IDisposable { readonly onDidWheel: Event; readonly onDidUpdateState: Event; readonly onDidReload: Event; - readonly onMessage: Event; + readonly onMessage: Event; readonly onMissingCsp: Event; - postMessage(data: any): void; + postMessage(message: any, transfer?: readonly ArrayBuffer[]): void; focus(): void; reload(): void; diff --git a/src/vs/workbench/contrib/webview/electron-browser/pre/electron-index.js b/src/vs/workbench/contrib/webview/electron-browser/pre/electron-index.js index bac642ca74c..5c6a14cbd4b 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/pre/electron-index.js +++ b/src/vs/workbench/contrib/webview/electron-browser/pre/electron-index.js @@ -65,8 +65,8 @@ document.addEventListener('DOMContentLoaded', e => { // Forward messages from the embedded iframe - window.onmessage = (message) => { - ipcRenderer.sendToHost(message.data.command, message.data.data); + window.onmessage = (/** @type {MessageEvent} */ event) => { + ipcRenderer.sendToHost(event.data.command, event.data.data); }; });