Compare commits

...

2 commits

Author SHA1 Message Date
Matt Bierner ba8fa699dd Serialize and restore typed arrays too
This also makes it so that if you pass the same ArrayBuffer twice in an object, we use a single object on the receiver side too
2021-03-17 20:20:52 -07:00
Matt Bierner 671ccf261c Improve passing of ArrayBuffers to and from webviews
Fixes #115807
2021-03-17 20:15:15 -07:00
20 changed files with 433 additions and 67 deletions

View file

@ -12,7 +12,7 @@
"activationEvents": [],
"main": "./out/extension",
"engines": {
"vscode": "^1.25.0"
"vscode": "^1.55.0"
},
"contributes": {
"configuration": {

View file

@ -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*/`
<script>
const vscode = acquireVsCodeApi();
window.addEventListener('message', (message) => {
switch (message.data.type) {
case 'add1':
const arrayBuffer = message.data.array;
const uint8Array = new Uint8Array(arrayBuffer);
for (let i = 0; i < uint8Array.length; ++i) {
uint8Array[i] = uint8Array[i] + 1;
}
vscode.postMessage({ array: arrayBuffer }, [arrayBuffer]);
break;
}
});
vscode.postMessage({ type: 'ready' });
</script>`);
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*/`
<script>
const vscode = acquireVsCodeApi();
window.addEventListener('message', (message) => {
switch (message.data.type) {
case 'add1':
const uint8Array = message.data.array1;
// This should update both buffers since they use the same ArrayBuffer storage
const uint16Array = message.data.array2;
for (let i = 0; i < uint16Array.length; ++i) {
uint16Array[i] = uint16Array[i] + 1;
}
vscode.postMessage({ array1: uint8Array, array2: uint16Array, }, [uint16Array.buffer]);
break;
}
});
vscode.postMessage({ type: 'ready' });
</script>`);
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 {

View file

@ -2692,6 +2692,27 @@ 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` in 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+, 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<boolean>;
}
//#endregion
//#region https://github.com/microsoft/vscode/issues/115616 @alexr00

View file

@ -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);
}

View file

@ -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;

View file

@ -137,9 +137,9 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc
public get webviewInputs(): Iterable<WebviewInput> { 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) {

View file

@ -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) {

View file

@ -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<boolean> {
public async $postMessage(handle: extHostProtocol.WebviewHandle, jsonMessage: string, ...buffers: VSBuffer[]): Promise<boolean> {
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(() => {

View file

@ -668,10 +668,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<boolean>
$postMessage(handle: WebviewHandle, value: any, ...buffers: VSBuffer[]): Promise<boolean>
}
export interface MainThreadWebviewPanelsShape extends IDisposable {
@ -683,6 +712,7 @@ export interface MainThreadWebviewPanelsShape extends IDisposable {
title: string;
webviewOptions: IWebviewOptions;
panelOptions: IWebviewPanelOptions;
serializeBuffersForPostMessage: boolean;
},
showOptions: WebviewPanelShowOptions,
): void;
@ -691,13 +721,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;
@ -705,7 +735,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;
@ -723,7 +753,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;
}

View file

@ -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(

View file

@ -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 >= 55;
} 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 <extHostProtocol.WebviewMessageArrayBufferReference>{
$$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);
}
}

View file

@ -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 <extHostProtocol.WebviewMessageArrayBufferReference>{
$$vscode_array_buffer_reference$$: true,
index,
};
} else if (ArrayBuffer.isView(value)) {
const type = getTypedArrayType(value);
if (type) {
const index = arrayBuffers.add(value.buffer);
return <extHostProtocol.WebviewMessageArrayBufferReference>{
$$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 };
}

View file

@ -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);

View file

@ -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);

View file

@ -789,7 +789,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;
}
@ -1009,7 +1010,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();
@ -1219,7 +1221,7 @@ var requirejs = (function() {
// TODO: use proper handler
const p = new Promise<void>(resolve => {
this.webview?.onMessage(e => {
if (e.type === 'initializedMarkdownPreview') {
if (e.message.type === 'initializedMarkdownPreview') {
resolve();
}
});

View file

@ -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<T extends HTMLElement> 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<T extends HTMLElement> extends Disposable {
private readonly _onDidReload = this._register(new Emitter<void>());
public readonly onDidReload = this._onDidReload.event;
private readonly _onMessage = this._register(new Emitter<any>());
private readonly _onMessage = this._register(new Emitter<WebviewMessageReceivedEvent>());
public readonly onMessage = this._onMessage.event;
private readonly _onDidScroll = this._register(new Emitter<{ readonly scrollYPercentage: number; }>());
@ -209,8 +212,8 @@ export abstract class BaseWebview<T extends HTMLElement> extends Disposable {
private readonly _onDidDispose = this._register(new Emitter<void>());
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 {

View file

@ -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<IMouseWheelEvent>());
public readonly onDidWheel = this._onDidWheel.event;
private readonly _pendingMessages = new Set<any>();
private readonly _pendingMessages = new Set<{ readonly message: any, readonly transfer?: readonly ArrayBuffer[] }>();
private readonly _webview = this._register(new MutableDisposable<WebviewElement>());
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<string | undefined>());
public readonly onDidUpdateState: Event<string | undefined> = this._onDidUpdateState.event;
private readonly _onMessage = this._register(new Emitter<any>());
public readonly onMessage: Event<any> = this._onMessage.event;
private readonly _onMessage = this._register(new Emitter<WebviewMessageReceivedEvent>());
public readonly onMessage = this._onMessage.event;
private readonly _onMissingCsp = this._register(new Emitter<ExtensionIdentifier>());
public readonly onMissingCsp: Event<any> = 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 });
}
}

View file

@ -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':

View file

@ -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<IMouseWheelEvent>;
readonly onDidUpdateState: Event<string | undefined>;
readonly onDidReload: Event<void>;
readonly onMessage: Event<any>;
readonly onMessage: Event<WebviewMessageReceivedEvent>;
readonly onMissingCsp: Event<ExtensionIdentifier>;
postMessage(data: any): void;
postMessage(message: any, transfer?: readonly ArrayBuffer[]): void;
focus(): void;
reload(): void;

View file

@ -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);
};
});