From 23fe7a39de1cf6eff8044b8ea1ff30d692b98f6f Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 26 Aug 2020 16:44:53 -0700 Subject: [PATCH] Split extHost webview panels to own file --- .../workbench/api/common/extHost.api.impl.ts | 9 +- .../api/common/extHostCustomEditors.ts | 6 +- src/vs/workbench/api/common/extHostWebview.ts | 283 +---------------- .../api/common/extHostWebviewPanels.ts | 299 ++++++++++++++++++ .../test/browser/api/extHostWebview.test.ts | 23 +- 5 files changed, 330 insertions(+), 290 deletions(-) create mode 100644 src/vs/workbench/api/common/extHostWebviewPanels.ts diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 0785eb62371..864c337d345 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -78,6 +78,7 @@ import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePa import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer'; import { ExtHostWebviewViews } from 'vs/workbench/api/common/extHostWebviewView'; import { ExtHostCustomEditors } from 'vs/workbench/api/common/extHostCustomEditors'; +import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels'; export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -144,8 +145,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol)); const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol, extHostCommands)); const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, initData.environment, extHostWorkspace, extHostLogService, extHostApiDeprecation)); - rpcProtocol.set(ExtHostContext.ExtHostWebviewPanels, extHostWebviews); - const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews)); + const extHostWebviewPanels = rpcProtocol.set(ExtHostContext.ExtHostWebviewPanels, new ExtHostWebviewPanels(rpcProtocol, extHostWebviews, extHostWorkspace)); + const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews, extHostWebviewPanels)); const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews)); // Check that no named customers are missing @@ -569,7 +570,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostOutputService.createOutputChannel(name); }, createWebviewPanel(viewType: string, title: string, showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, options?: vscode.WebviewPanelOptions & vscode.WebviewOptions): vscode.WebviewPanel { - return extHostWebviews.createWebviewPanel(extension, viewType, title, showOptions, options); + return extHostWebviewPanels.createWebviewPanel(extension, viewType, title, showOptions, options); }, createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): vscode.WebviewEditorInset { checkProposedApiEnabled(extension); @@ -594,7 +595,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostTreeViews.createTreeView(viewId, options, extension); }, registerWebviewPanelSerializer: (viewType: string, serializer: vscode.WebviewPanelSerializer) => { - return extHostWebviews.registerWebviewPanelSerializer(extension, viewType, serializer); + return extHostWebviewPanels.registerWebviewPanelSerializer(extension, viewType, serializer); }, registerCustomEditorProvider: (viewType: string, provider: vscode.CustomTextEditorProvider | vscode.CustomReadonlyEditorProvider, options: { webviewOptions?: vscode.WebviewPanelOptions, supportsMultipleEditorsPerDocument?: boolean } = {}) => { return extHostCustomEditors.registerCustomEditorProvider(extension, viewType, provider, options); diff --git a/src/vs/workbench/api/common/extHostCustomEditors.ts b/src/vs/workbench/api/common/extHostCustomEditors.ts index 779075db693..f3b3cbd8438 100644 --- a/src/vs/workbench/api/common/extHostCustomEditors.ts +++ b/src/vs/workbench/api/common/extHostCustomEditors.ts @@ -14,6 +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 { ExtHostWebviews, toExtensionData } from 'vs/workbench/api/common/extHostWebview'; +import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels'; import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; import type * as vscode from 'vscode'; import { Cache } from './cache'; @@ -165,6 +166,7 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor private readonly _extHostDocuments: ExtHostDocuments, private readonly _extensionStoragePaths: IExtensionStoragePaths | undefined, private readonly _extHostWebview: ExtHostWebviews, + private readonly _extHostWebviewPanels: ExtHostWebviewPanels, ) { this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadCustomEditors); } @@ -260,7 +262,7 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor } const webview = this._extHostWebview.createNewWebview(handle, options, entry.extension); - const panel = this._extHostWebview.createNewWebviewPanel(handle, viewType, title, position, options, webview); + const panel = this._extHostWebviewPanels.createNewWebviewPanel(handle, viewType, title, position, options, webview); const revivedResource = URI.revive(resource); @@ -297,7 +299,7 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor throw new Error(`Provider does not implement move '${viewType}'`); } - const webview = this._extHostWebview.getWebviewPanel(handle); + const webview = this._extHostWebviewPanels.getWebviewPanel(handle); if (!webview) { throw new Error(`No webview found`); } diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index 3d2673c7142..e5dc3c670d5 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -4,20 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; import * as modes from 'vs/editor/common/modes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; -import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import type * as vscode from 'vscode'; import * as extHostProtocol from './extHost.protocol'; -import * as extHostTypes from './extHostTypes'; export class ExtHostWebview implements vscode.Webview { @@ -116,169 +111,11 @@ export class ExtHostWebview implements vscode.Webview { } } -type IconPath = URI | { light: URI, dark: URI }; +export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { - -class ExtHostWebviewPanel extends Disposable implements vscode.WebviewPanel { - - readonly #handle: extHostProtocol.WebviewHandle; - readonly #proxy: extHostProtocol.MainThreadWebviewPanelsShape; - readonly #viewType: string; - - readonly #webview: ExtHostWebview; - readonly #options: vscode.WebviewPanelOptions; - - #title: string; - #iconPath?: IconPath; - #viewColumn: vscode.ViewColumn | undefined = undefined; - #visible: boolean = true; - #active: boolean = true; - #isDisposed: boolean = false; - - readonly #onDidDispose = this._register(new Emitter()); - public readonly onDidDispose = this.#onDidDispose.event; - - readonly #onDidChangeViewState = this._register(new Emitter()); - public readonly onDidChangeViewState = this.#onDidChangeViewState.event; - - constructor( - handle: extHostProtocol.WebviewHandle, - proxy: extHostProtocol.MainThreadWebviewPanelsShape, - viewType: string, - title: string, - viewColumn: vscode.ViewColumn | undefined, - editorOptions: vscode.WebviewPanelOptions, - webview: ExtHostWebview - ) { - super(); - this.#handle = handle; - this.#proxy = proxy; - this.#viewType = viewType; - this.#options = editorOptions; - this.#viewColumn = viewColumn; - this.#title = title; - this.#webview = webview; - } - - public dispose() { - if (this.#isDisposed) { - return; - } - - this.#isDisposed = true; - this.#onDidDispose.fire(); - - this.#proxy.$disposeWebview(this.#handle); - this.#webview.dispose(); - - super.dispose(); - } - - get webview() { - this.assertNotDisposed(); - return this.#webview; - } - - get viewType(): string { - this.assertNotDisposed(); - return this.#viewType; - } - - get title(): string { - this.assertNotDisposed(); - return this.#title; - } - - set title(value: string) { - this.assertNotDisposed(); - if (this.#title !== value) { - this.#title = value; - this.#proxy.$setTitle(this.#handle, value); - } - } - - get iconPath(): IconPath | undefined { - this.assertNotDisposed(); - return this.#iconPath; - } - - set iconPath(value: IconPath | undefined) { - this.assertNotDisposed(); - if (this.#iconPath !== value) { - this.#iconPath = value; - - this.#proxy.$setIconPath(this.#handle, URI.isUri(value) ? { light: value, dark: value } : value); - } - } - - get options() { - return this.#options; - } - - get viewColumn(): vscode.ViewColumn | undefined { - this.assertNotDisposed(); - if (typeof this.#viewColumn === 'number' && this.#viewColumn < 0) { - // We are using a symbolic view column - // Return undefined instead to indicate that the real view column is currently unknown but will be resolved. - return undefined; - } - return this.#viewColumn; - } - - public get active(): boolean { - this.assertNotDisposed(); - return this.#active; - } - - public get visible(): boolean { - this.assertNotDisposed(); - return this.#visible; - } - - _updateViewState(newState: { active: boolean; visible: boolean; viewColumn: vscode.ViewColumn; }) { - if (this.#isDisposed) { - return; - } - - if (this.active !== newState.active || this.visible !== newState.visible || this.viewColumn !== newState.viewColumn) { - this.#active = newState.active; - this.#visible = newState.visible; - this.#viewColumn = newState.viewColumn; - this.#onDidChangeViewState.fire({ webviewPanel: this }); - } - } - - public reveal(viewColumn?: vscode.ViewColumn, preserveFocus?: boolean): void { - this.assertNotDisposed(); - this.#proxy.$reveal(this.#handle, { - viewColumn: viewColumn ? typeConverters.ViewColumn.from(viewColumn) : undefined, - preserveFocus: !!preserveFocus - }); - } - - private assertNotDisposed() { - if (this.#isDisposed) { - throw new Error('Webview is disposed'); - } - } -} - -export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape, extHostProtocol.ExtHostWebviewPanelsShape { - - private static newHandle(): extHostProtocol.WebviewHandle { - return generateUuid(); - } - - private readonly _proxy: extHostProtocol.MainThreadWebviewPanelsShape; private readonly _webviewProxy: extHostProtocol.MainThreadWebviewsShape; private readonly _webviews = new Map(); - private readonly _webviewPanels = new Map(); - - private readonly _serializers = new Map(); constructor( mainContext: extHostProtocol.IMainContext, @@ -287,32 +124,9 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape, ex private readonly _logService: ILogService, private readonly _deprecationService: IExtHostApiDeprecationService, ) { - this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadWebviewPanels); this._webviewProxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadWebviews); } - public createWebviewPanel( - extension: IExtensionDescription, - viewType: string, - title: string, - showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, - options: (vscode.WebviewPanelOptions & vscode.WebviewOptions) = {}, - ): vscode.WebviewPanel { - const viewColumn = typeof showOptions === 'object' ? showOptions.viewColumn : showOptions; - const webviewShowOptions = { - viewColumn: typeConverters.ViewColumn.from(viewColumn), - preserveFocus: typeof showOptions === 'object' && !!showOptions.preserveFocus - }; - - const handle = ExtHostWebviews.newHandle(); - this._proxy.$createWebviewPanel(toExtensionData(extension), handle, viewType, title, webviewShowOptions, convertWebviewOptions(extension, this.workspace, options)); - - const webview = this.createNewWebview(handle, options, extension); - const panel = this.createNewWebviewPanel(handle, viewType, title, viewColumn, options, webview); - - return panel; - } - public $onMessage( handle: extHostProtocol.WebviewHandle, message: any @@ -330,91 +144,6 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape, ex this._logService.warn(`${extensionId} created a webview without a content security policy: https://aka.ms/vscode-webview-missing-csp`); } - public $onDidChangeWebviewPanelViewStates(newStates: extHostProtocol.WebviewPanelViewStateData): void { - const handles = Object.keys(newStates); - // Notify webviews of state changes in the following order: - // - Non-visible - // - Visible - // - Active - handles.sort((a, b) => { - const stateA = newStates[a]; - const stateB = newStates[b]; - if (stateA.active) { - return 1; - } - if (stateB.active) { - return -1; - } - return (+stateA.visible) - (+stateB.visible); - }); - - for (const handle of handles) { - const panel = this.getWebviewPanel(handle); - if (!panel) { - continue; - } - - const newState = newStates[handle]; - panel._updateViewState({ - active: newState.active, - visible: newState.visible, - viewColumn: typeConverters.ViewColumn.to(newState.position), - }); - } - } - - async $onDidDisposeWebviewPanel(handle: extHostProtocol.WebviewHandle): Promise { - const panel = this.getWebviewPanel(handle); - panel?.dispose(); - - this._webviewPanels.delete(handle); - this._webviews.delete(handle); - } - - - public registerWebviewPanelSerializer( - extension: IExtensionDescription, - viewType: string, - serializer: vscode.WebviewPanelSerializer - ): vscode.Disposable { - if (this._serializers.has(viewType)) { - throw new Error(`Serializer for '${viewType}' already registered`); - } - - this._serializers.set(viewType, { serializer, extension }); - this._proxy.$registerSerializer(viewType); - - return new extHostTypes.Disposable(() => { - this._serializers.delete(viewType); - this._proxy.$unregisterSerializer(viewType); - }); - } - - async $deserializeWebviewPanel( - webviewHandle: extHostProtocol.WebviewHandle, - viewType: string, - title: string, - state: any, - position: EditorViewColumn, - options: modes.IWebviewOptions & modes.IWebviewPanelOptions - ): Promise { - const entry = this._serializers.get(viewType); - if (!entry) { - throw new Error(`No serializer found for '${viewType}'`); - } - const { serializer, extension } = entry; - - const webview = this.createNewWebview(webviewHandle, options, extension); - const revivedPanel = this.createNewWebviewPanel(webviewHandle, viewType, title, position, options, webview); - await serializer.deserializeWebviewPanel(revivedPanel, state); - } - - public createNewWebviewPanel(webviewHandle: string, viewType: string, title: string, position: number, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, webview: ExtHostWebview) { - const panel = new ExtHostWebviewPanel(webviewHandle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview); - this._webviewPanels.set(webviewHandle, panel); - return panel; - } - public createNewWebview(handle: string, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, extension: IExtensionDescription): ExtHostWebview { const webview = new ExtHostWebview(handle, this._webviewProxy, reviveOptions(options), this.initData, this.workspace, extension, this._deprecationService); this._webviews.set(handle, webview); @@ -424,12 +153,12 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape, ex return webview; } - private getWebview(handle: extHostProtocol.WebviewHandle): ExtHostWebview | undefined { - return this._webviews.get(handle); + public deleteWebview(handle: string) { + this._webviews.delete(handle); } - public getWebviewPanel(handle: extHostProtocol.WebviewHandle): ExtHostWebviewPanel | undefined { - return this._webviewPanels.get(handle); + private getWebview(handle: extHostProtocol.WebviewHandle): ExtHostWebview | undefined { + return this._webviews.get(handle); } } @@ -437,7 +166,7 @@ export function toExtensionData(extension: IExtensionDescription): extHostProtoc return { id: extension.identifier, location: extension.extensionLocation }; } -function convertWebviewOptions( +export function convertWebviewOptions( extension: IExtensionDescription, workspace: IExtHostWorkspace | undefined, options: vscode.WebviewPanelOptions & vscode.WebviewOptions, diff --git a/src/vs/workbench/api/common/extHostWebviewPanels.ts b/src/vs/workbench/api/common/extHostWebviewPanels.ts new file mode 100644 index 00000000000..d29fdc631fa --- /dev/null +++ b/src/vs/workbench/api/common/extHostWebviewPanels.ts @@ -0,0 +1,299 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import * as modes from 'vs/editor/common/modes'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; +import { convertWebviewOptions, ExtHostWebview, ExtHostWebviews, toExtensionData } from 'vs/workbench/api/common/extHostWebview'; +import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; +import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; +import type * as vscode from 'vscode'; +import * as extHostProtocol from './extHost.protocol'; +import * as extHostTypes from './extHostTypes'; + + +type IconPath = URI | { light: URI, dark: URI }; + +class ExtHostWebviewPanel extends Disposable implements vscode.WebviewPanel { + + readonly #handle: extHostProtocol.WebviewHandle; + readonly #proxy: extHostProtocol.MainThreadWebviewPanelsShape; + readonly #viewType: string; + + readonly #webview: ExtHostWebview; + readonly #options: vscode.WebviewPanelOptions; + + #title: string; + #iconPath?: IconPath; + #viewColumn: vscode.ViewColumn | undefined = undefined; + #visible: boolean = true; + #active: boolean = true; + #isDisposed: boolean = false; + + readonly #onDidDispose = this._register(new Emitter()); + public readonly onDidDispose = this.#onDidDispose.event; + + readonly #onDidChangeViewState = this._register(new Emitter()); + public readonly onDidChangeViewState = this.#onDidChangeViewState.event; + + constructor( + handle: extHostProtocol.WebviewHandle, + proxy: extHostProtocol.MainThreadWebviewPanelsShape, + viewType: string, + title: string, + viewColumn: vscode.ViewColumn | undefined, + editorOptions: vscode.WebviewPanelOptions, + webview: ExtHostWebview + ) { + super(); + this.#handle = handle; + this.#proxy = proxy; + this.#viewType = viewType; + this.#options = editorOptions; + this.#viewColumn = viewColumn; + this.#title = title; + this.#webview = webview; + } + + public dispose() { + if (this.#isDisposed) { + return; + } + + this.#isDisposed = true; + this.#onDidDispose.fire(); + + this.#proxy.$disposeWebview(this.#handle); + this.#webview.dispose(); + + super.dispose(); + } + + get webview() { + this.assertNotDisposed(); + return this.#webview; + } + + get viewType(): string { + this.assertNotDisposed(); + return this.#viewType; + } + + get title(): string { + this.assertNotDisposed(); + return this.#title; + } + + set title(value: string) { + this.assertNotDisposed(); + if (this.#title !== value) { + this.#title = value; + this.#proxy.$setTitle(this.#handle, value); + } + } + + get iconPath(): IconPath | undefined { + this.assertNotDisposed(); + return this.#iconPath; + } + + set iconPath(value: IconPath | undefined) { + this.assertNotDisposed(); + if (this.#iconPath !== value) { + this.#iconPath = value; + + this.#proxy.$setIconPath(this.#handle, URI.isUri(value) ? { light: value, dark: value } : value); + } + } + + get options() { + return this.#options; + } + + get viewColumn(): vscode.ViewColumn | undefined { + this.assertNotDisposed(); + if (typeof this.#viewColumn === 'number' && this.#viewColumn < 0) { + // We are using a symbolic view column + // Return undefined instead to indicate that the real view column is currently unknown but will be resolved. + return undefined; + } + return this.#viewColumn; + } + + public get active(): boolean { + this.assertNotDisposed(); + return this.#active; + } + + public get visible(): boolean { + this.assertNotDisposed(); + return this.#visible; + } + + _updateViewState(newState: { active: boolean; visible: boolean; viewColumn: vscode.ViewColumn; }) { + if (this.#isDisposed) { + return; + } + + if (this.active !== newState.active || this.visible !== newState.visible || this.viewColumn !== newState.viewColumn) { + this.#active = newState.active; + this.#visible = newState.visible; + this.#viewColumn = newState.viewColumn; + this.#onDidChangeViewState.fire({ webviewPanel: this }); + } + } + + public reveal(viewColumn?: vscode.ViewColumn, preserveFocus?: boolean): void { + this.assertNotDisposed(); + this.#proxy.$reveal(this.#handle, { + viewColumn: viewColumn ? typeConverters.ViewColumn.from(viewColumn) : undefined, + preserveFocus: !!preserveFocus + }); + } + + private assertNotDisposed() { + if (this.#isDisposed) { + throw new Error('Webview is disposed'); + } + } +} + +export class ExtHostWebviewPanels implements extHostProtocol.ExtHostWebviewPanelsShape { + + private static newHandle(): extHostProtocol.WebviewHandle { + return generateUuid(); + } + + private readonly _proxy: extHostProtocol.MainThreadWebviewPanelsShape; + + private readonly _webviewPanels = new Map(); + + private readonly _serializers = new Map(); + + constructor( + mainContext: extHostProtocol.IMainContext, + private readonly webviews: ExtHostWebviews, + private readonly workspace: IExtHostWorkspace | undefined, + ) { + this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadWebviewPanels); + } + + public createWebviewPanel( + extension: IExtensionDescription, + viewType: string, + title: string, + showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, + options: (vscode.WebviewPanelOptions & vscode.WebviewOptions) = {}, + ): vscode.WebviewPanel { + const viewColumn = typeof showOptions === 'object' ? showOptions.viewColumn : showOptions; + const webviewShowOptions = { + viewColumn: typeConverters.ViewColumn.from(viewColumn), + preserveFocus: typeof showOptions === 'object' && !!showOptions.preserveFocus + }; + + const handle = ExtHostWebviewPanels.newHandle(); + this._proxy.$createWebviewPanel(toExtensionData(extension), handle, viewType, title, webviewShowOptions, convertWebviewOptions(extension, this.workspace, options)); + + const webview = this.webviews.createNewWebview(handle, options, extension); + const panel = this.createNewWebviewPanel(handle, viewType, title, viewColumn, options, webview); + + return panel; + } + + public $onDidChangeWebviewPanelViewStates(newStates: extHostProtocol.WebviewPanelViewStateData): void { + const handles = Object.keys(newStates); + // Notify webviews of state changes in the following order: + // - Non-visible + // - Visible + // - Active + handles.sort((a, b) => { + const stateA = newStates[a]; + const stateB = newStates[b]; + if (stateA.active) { + return 1; + } + if (stateB.active) { + return -1; + } + return (+stateA.visible) - (+stateB.visible); + }); + + for (const handle of handles) { + const panel = this.getWebviewPanel(handle); + if (!panel) { + continue; + } + + const newState = newStates[handle]; + panel._updateViewState({ + active: newState.active, + visible: newState.visible, + viewColumn: typeConverters.ViewColumn.to(newState.position), + }); + } + } + + async $onDidDisposeWebviewPanel(handle: extHostProtocol.WebviewHandle): Promise { + const panel = this.getWebviewPanel(handle); + panel?.dispose(); + + this._webviewPanels.delete(handle); + this.webviews.deleteWebview(handle); + } + + public registerWebviewPanelSerializer( + extension: IExtensionDescription, + viewType: string, + serializer: vscode.WebviewPanelSerializer + ): vscode.Disposable { + if (this._serializers.has(viewType)) { + throw new Error(`Serializer for '${viewType}' already registered`); + } + + this._serializers.set(viewType, { serializer, extension }); + this._proxy.$registerSerializer(viewType); + + return new extHostTypes.Disposable(() => { + this._serializers.delete(viewType); + this._proxy.$unregisterSerializer(viewType); + }); + } + + async $deserializeWebviewPanel( + webviewHandle: extHostProtocol.WebviewHandle, + viewType: string, + title: string, + state: any, + position: EditorViewColumn, + options: modes.IWebviewOptions & modes.IWebviewPanelOptions + ): Promise { + const entry = this._serializers.get(viewType); + if (!entry) { + throw new Error(`No serializer found for '${viewType}'`); + } + const { serializer, extension } = entry; + + const webview = this.webviews.createNewWebview(webviewHandle, options, extension); + const revivedPanel = this.createNewWebviewPanel(webviewHandle, viewType, title, position, options, webview); + await serializer.deserializeWebviewPanel(revivedPanel, state); + } + + public createNewWebviewPanel(webviewHandle: string, viewType: string, title: string, position: number, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, webview: ExtHostWebview) { + const panel = new ExtHostWebviewPanel(webviewHandle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview); + this._webviewPanels.set(webviewHandle, panel); + return panel; + } + + public getWebviewPanel(handle: extHostProtocol.WebviewHandle): ExtHostWebviewPanel | undefined { + return this._webviewPanels.get(handle); + } +} diff --git a/src/vs/workbench/test/browser/api/extHostWebview.test.ts b/src/vs/workbench/test/browser/api/extHostWebview.test.ts index 9e6194a99c7..577b61c1747 100644 --- a/src/vs/workbench/test/browser/api/extHostWebview.test.ts +++ b/src/vs/workbench/test/browser/api/extHostWebview.test.ts @@ -13,6 +13,7 @@ import { IExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; import { NullApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview'; +import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels'; import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; import type * as vscode from 'vscode'; import { SingleProxyRPCProtocol } from './testRPCProtocol'; @@ -35,6 +36,8 @@ suite('ExtHostWebview', () => { isExtensionDevelopmentDebug: false, }, undefined, new NullLogService(), NullApiDeprecationService); + const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); + let lastInvokedDeserializer: vscode.WebviewPanelSerializer | undefined = undefined; class NoopSerializer implements vscode.WebviewPanelSerializer { @@ -48,20 +51,20 @@ suite('ExtHostWebview', () => { const serializerA = new NoopSerializer(); const serializerB = new NoopSerializer(); - const serializerARegistration = extHostWebviews.registerWebviewPanelSerializer(extension, viewType, serializerA); + const serializerARegistration = extHostWebviewPanels.registerWebviewPanelSerializer(extension, viewType, serializerA); - await extHostWebviews.$deserializeWebviewPanel('x', viewType, 'title', {}, 0 as EditorViewColumn, {}); + await extHostWebviewPanels.$deserializeWebviewPanel('x', viewType, 'title', {}, 0 as EditorViewColumn, {}); assert.strictEqual(lastInvokedDeserializer, serializerA); assert.throws( - () => extHostWebviews.registerWebviewPanelSerializer(extension, viewType, serializerB), + () => extHostWebviewPanels.registerWebviewPanelSerializer(extension, viewType, serializerB), 'Should throw when registering two serializers for the same view'); serializerARegistration.dispose(); - extHostWebviews.registerWebviewPanelSerializer(extension, viewType, serializerB); + extHostWebviewPanels.registerWebviewPanelSerializer(extension, viewType, serializerB); - await extHostWebviews.$deserializeWebviewPanel('x', viewType, 'title', {}, 0 as EditorViewColumn, {}); + await extHostWebviewPanels.$deserializeWebviewPanel('x', viewType, 'title', {}, 0 as EditorViewColumn, {}); assert.strictEqual(lastInvokedDeserializer, serializerB); }); @@ -71,7 +74,10 @@ suite('ExtHostWebview', () => { webviewResourceRoot: 'vscode-resource://{{resource}}', isExtensionDevelopmentDebug: false, }, undefined, new NullLogService(), NullApiDeprecationService); - const webview = extHostWebviews.createWebviewPanel({} as any, 'type', 'title', 1, {}); + + const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); + + const webview = extHostWebviewPanels.createWebviewPanel({} as any, 'type', 'title', 1, {}); assert.strictEqual( webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString(), @@ -110,7 +116,10 @@ suite('ExtHostWebview', () => { webviewResourceRoot: `https://{{uuid}}.webview.contoso.com/commit/{{resource}}`, isExtensionDevelopmentDebug: false, }, undefined, new NullLogService(), NullApiDeprecationService); - const webview = extHostWebviews.createWebviewPanel({} as any, 'type', 'title', 1, {}); + + const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); + + const webview = extHostWebviewPanels.createWebviewPanel({} as any, 'type', 'title', 1, {}); function stripEndpointUuid(input: string) { return input.replace(/^https:\/\/[^\.]+?\./, '');