Move webview views into own ext host class

Also fixes message passing for webview views
This commit is contained in:
Matt Bierner 2020-08-20 15:19:14 -07:00
parent dc0150c61a
commit 4fd7f660a4
5 changed files with 236 additions and 174 deletions

View file

@ -118,6 +118,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
]);
private readonly _proxy: extHostProtocol.ExtHostWebviewsShape;
private readonly _viewsProxy: extHostProtocol.ExtHostWebviewViewsShape;
private readonly _webviewInputs = new WebviewInputStore();
private readonly _revivers = new Map<string, IDisposable>();
@ -146,6 +147,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
super();
this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviews);
this._viewsProxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviewViews);
this._register(_editorService.onDidActiveEditorChange(() => {
const activeInput = this._editorService.activeEditor;
@ -259,8 +261,8 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
}
public $setOptions(handle: extHostProtocol.WebviewPanelHandle, options: modes.IWebviewOptions): void {
const webview = this.getWebviewInput(handle);
webview.webview.contentOptions = reviveWebviewOptions(options);
const webview = this.getWebview(handle);
webview.contentOptions = reviveWebviewOptions(options);
}
public $reveal(handle: extHostProtocol.WebviewPanelHandle, showOptions: extHostProtocol.WebviewPanelShowOptions): void {
@ -276,8 +278,8 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
}
public async $postMessage(handle: extHostProtocol.WebviewPanelHandle, message: any): Promise<boolean> {
const webview = this.getWebviewInput(handle);
webview.webview.postMessage(message);
const webview = this.getWebview(handle);
webview.postMessage(message);
return true;
}
@ -356,15 +358,15 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
}
webviewView.onDidChangeVisibility(visible => {
this._proxy.$onDidChangeWebviewViewVisibility(handle, visible);
this._viewsProxy.$onDidChangeWebviewViewVisibility(handle, visible);
});
webviewView.onDispose(() => {
this._proxy.$disposeWebviewView(handle);
this._viewsProxy.$disposeWebviewView(handle);
});
try {
await this._proxy.$resolveWebviewView(handle, viewType, state, cancellation);
await this._viewsProxy.$resolveWebviewView(handle, viewType, state, cancellation);
} catch (error) {
onUnexpectedError(error);
webviewView.webview.html = MainThreadWebviews.getWebviewResolvedFailedContent(viewType);
@ -373,6 +375,16 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
});
}
public $unregisterWebviewViewProvider(viewType: string): void {
const provider = this._webviewViewProviders.get(viewType);
if (!provider) {
throw new Error(`No view provider for ${viewType} registered`);
}
provider.dispose();
this._webviewViewProviders.delete(viewType);
}
public $registerTextEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: extHostProtocol.CustomTextEditorCapabilities): void {
this.registerEditorProvider(ModelType.Text, extensionData, viewType, options, capabilities, true);
}

View file

@ -76,6 +76,7 @@ import { ExtHostTimeline } from 'vs/workbench/api/common/extHostTimeline';
import { ExtHostNotebookConcatDocument } from 'vs/workbench/api/common/extHostNotebookConcatDocument';
import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer';
import { ExtHostWebviewViews } from 'vs/workbench/api/common/extHostWebviewView';
export interface IExtensionApiFactory {
(extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode;
@ -142,6 +143,7 @@ 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, extHostDocuments, extensionStoragePaths));
const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews));
// Check that no named customers are missing
const expected: ProxyIdentifier<any>[] = values(ExtHostContext);
@ -619,7 +621,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
}
}) {
checkProposedApiEnabled(extension);
return extHostWebviews.registerWebviewViewProvider(extension, viewId, provider, options?.webviewOptions);
return extHostWebviewViews.registerWebviewViewProvider(extension, viewId, provider, options?.webviewOptions);
}
};

View file

@ -631,6 +631,7 @@ export interface MainThreadWebviewsShape extends IDisposable {
$onContentChange(resource: UriComponents, viewType: string): void;
$registerWebviewViewProvider(viewType: string, options?: { retainContextWhenHidden?: boolean }): void;
$unregisterWebviewViewProvider(viewType: string): void;
$setWebviewViewTitle(handle: WebviewPanelHandle, value: string | undefined): void;
}
@ -666,9 +667,13 @@ export interface ExtHostWebviewsShape {
$backup(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise<string>;
$onMoveCustomEditor(handle: WebviewPanelHandle, newResource: UriComponents, viewType: string): Promise<void>;
}
export interface ExtHostWebviewViewsShape {
$resolveWebviewView(webviewHandle: WebviewPanelHandle, viewType: string, state: any, cancellation: CancellationToken): Promise<void>;
$onDidChangeWebviewViewVisibility(webviewHandle: WebviewPanelHandle, visible: boolean): void;
$disposeWebviewView(webviewHandle: WebviewPanelHandle): void;
}
@ -1744,6 +1749,7 @@ export const ExtHostContext = {
ExtHostWorkspace: createExtId<ExtHostWorkspaceShape>('ExtHostWorkspace'),
ExtHostWindow: createExtId<ExtHostWindowShape>('ExtHostWindow'),
ExtHostWebviews: createExtId<ExtHostWebviewsShape>('ExtHostWebviews'),
ExtHostWebviewViews: createExtId<ExtHostWebviewViewsShape>('ExtHostWebviewViews'),
ExtHostEditorInsets: createExtId<ExtHostEditorInsetsShape>('ExtHostEditorInsets'),
ExtHostProgress: createMainId<ExtHostProgressShape>('ExtHostProgress'),
ExtHostComments: createMainId<ExtHostCommentsShape>('ExtHostComments'),

View file

@ -64,7 +64,13 @@ export class ExtHostWebview implements vscode.Webview {
/* internal */ readonly _onMessageEmitter = new Emitter<any>();
public readonly onDidReceiveMessage: Event<any> = this._onMessageEmitter.event;
readonly #onDidDisposeEmitter = new Emitter<void>();
/* internal */ readonly _onDidDispose: Event<void> = this.#onDidDisposeEmitter.event;
public dispose() {
this.#onDidDisposeEmitter.fire();
this.#onDidDisposeEmitter.dispose();
this._onMessageEmitter.dispose();
}
@ -167,6 +173,7 @@ export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPa
this.#isDisposed = true;
this.#onDidDispose.fire();
this.#proxy.$disposeWebview(this.#handle);
this.#webview.dispose();
@ -267,92 +274,6 @@ export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPa
}
}
export class ExtHostWebviewView extends Disposable implements vscode.WebviewView {
readonly #handle: extHostProtocol.WebviewPanelHandle;
readonly #proxy: extHostProtocol.MainThreadWebviewsShape;
readonly #viewType: string;
readonly #webview: ExtHostWebview;
#isDisposed = false;
#isVisible: boolean;
#title: string | undefined;
constructor(
handle: extHostProtocol.WebviewPanelHandle,
proxy: extHostProtocol.MainThreadWebviewsShape,
viewType: string,
webview: ExtHostWebview,
isVisible: boolean,
) {
super();
this.#viewType = viewType;
this.#handle = handle;
this.#proxy = proxy;
this.#webview = webview;
this.#isVisible = isVisible;
}
public dispose() {
if (this.#isDisposed) {
return;
}
this.#isDisposed = true;
this.#onDidDispose.fire();
super.dispose();
}
readonly #onDidChangeVisibility = this._register(new Emitter<void>());
public readonly onDidChangeVisibility = this.#onDidChangeVisibility.event;
readonly #onDidDispose = this._register(new Emitter<void>());
public readonly onDidDispose = this.#onDidDispose.event;
get title(): string | undefined {
this.assertNotDisposed();
return this.#title;
}
set title(value: string | undefined) {
this.assertNotDisposed();
if (this.#title !== value) {
this.#title = value;
this.#proxy.$setWebviewViewTitle(this.#handle, value);
}
}
get visible() {
return this.#isVisible;
}
get webview() {
return this.#webview;
}
get viewType(): string {
return this.#viewType;
}
_setVisible(visible: boolean) {
if (visible === this.#isVisible) {
return;
}
this.#isVisible = visible;
this.#onDidChangeVisibility.fire();
}
private assertNotDisposed() {
if (this.#isDisposed) {
throw new Error('Webview is disposed');
}
}
}
class CustomDocumentStoreEntry {
private _backupCounter = 1;
@ -491,6 +412,8 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
}
private readonly _proxy: extHostProtocol.MainThreadWebviewsShape;
private readonly _webviews = new Map<extHostProtocol.WebviewPanelHandle, ExtHostWebview>();
private readonly _webviewPanels = new Map<extHostProtocol.WebviewPanelHandle, ExtHostWebviewEditor>();
private readonly _serializers = new Map<string, {
@ -498,13 +421,7 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
readonly extension: IExtensionDescription;
}>();
private readonly _viewProviders = new Map<string, {
readonly provider: vscode.WebviewViewProvider;
readonly extension: IExtensionDescription;
}>();
private readonly _editorProviders = new EditorProviderStore();
private readonly _webviewViews = new Map<extHostProtocol.WebviewPanelHandle, ExtHostWebviewView>();
private readonly _documents = new CustomDocumentStore();
@ -536,7 +453,7 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
const handle = ExtHostWebviews.newHandle();
this._proxy.$createWebviewPanel(toExtensionData(extension), handle, viewType, title, webviewShowOptions, convertWebviewOptions(extension, this.workspace, options));
const webview = new ExtHostWebview(handle, this._proxy, options, this.initData, this.workspace, extension, this._deprecationService);
const webview = this.createNewWebview(handle, options, extension);
const panel = new ExtHostWebviewEditor(handle, this._proxy, viewType, title, viewColumn, options, webview);
this._webviewPanels.set(handle, panel);
return panel;
@ -560,27 +477,6 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
});
}
public registerWebviewViewProvider(
extension: IExtensionDescription,
viewType: string,
provider: vscode.WebviewViewProvider,
webviewOptions?: {
retainContextWhenHidden?: boolean
},
): vscode.Disposable {
if (this._viewProviders.has(viewType)) {
throw new Error(`View provider for '${viewType}' already registered`);
}
this._viewProviders.set(viewType, { provider, extension });
this._proxy.$registerWebviewViewProvider(viewType, webviewOptions);
return new extHostTypes.Disposable(() => {
this._viewProviders.delete(viewType);
this._proxy.$unregisterSerializer(viewType);
});
}
public registerCustomEditorProvider(
extension: IExtensionDescription,
viewType: string,
@ -622,9 +518,9 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
handle: extHostProtocol.WebviewPanelHandle,
message: any
): void {
const panel = this.getWebviewPanel(handle);
if (panel) {
panel.webview._onMessageEmitter.fire(message);
const webview = this.getWebview(handle);
if (webview) {
webview._onMessageEmitter.fire(message);
}
}
@ -670,10 +566,10 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
async $onDidDisposeWebviewPanel(handle: extHostProtocol.WebviewPanelHandle): Promise<void> {
const panel = this.getWebviewPanel(handle);
if (panel) {
panel.dispose();
this._webviewPanels.delete(handle);
}
panel?.dispose();
this._webviewPanels.delete(handle);
this._webviews.delete(handle);
}
async $deserializeWebviewPanel(
@ -690,47 +586,12 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
}
const { serializer, extension } = entry;
const webview = new ExtHostWebview(webviewHandle, this._proxy, reviveOptions(options), this.initData, this.workspace, extension, this._deprecationService);
const webview = this.createNewWebview(webviewHandle, options, extension);
const revivedPanel = new ExtHostWebviewEditor(webviewHandle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview);
this._webviewPanels.set(webviewHandle, revivedPanel);
await serializer.deserializeWebviewPanel(revivedPanel, state);
}
async $resolveWebviewView(
webviewHandle: string,
viewType: string,
state: any,
cancellation: CancellationToken,
): Promise<void> {
const entry = this._viewProviders.get(viewType);
if (!entry) {
throw new Error(`No view provider found for '${viewType}'`);
}
const { provider, extension } = entry;
const webview = new ExtHostWebview(webviewHandle, this._proxy, reviveOptions({ /* todo */ }), this.initData, this.workspace, extension, this._deprecationService);
const revivedView = new ExtHostWebviewView(webviewHandle, this._proxy, viewType, webview, true);
this._webviewViews.set(webviewHandle, revivedView);
await provider.resolveWebviewView(revivedView, { state }, cancellation);
}
async $onDidChangeWebviewViewVisibility(
webviewHandle: string,
visible: boolean
) {
const webviewView = this.getWebviewView(webviewHandle);
webviewView._setVisible(visible);
}
async $disposeWebviewView(webviewHandle: string) {
const webviewView = this.getWebviewView(webviewHandle);
this._webviewViews.delete(webviewHandle);
webviewView.dispose();
}
async $createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, cancellation: CancellationToken) {
const entry = this._editorProviders.get(viewType);
if (!entry) {
@ -783,7 +644,7 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
throw new Error(`No provider found for '${viewType}'`);
}
const webview = new ExtHostWebview(handle, this._proxy, reviveOptions(options), this.initData, this.workspace, entry.extension, this._deprecationService);
const webview = this.createNewWebview(handle, options, entry.extension);
const revivedPanel = new ExtHostWebviewEditor(handle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview);
this._webviewPanels.set(handle, revivedPanel);
@ -873,6 +734,19 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
return backup.id;
}
public createNewWebview(handle: string, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, extension: IExtensionDescription): ExtHostWebview {
const webview = new ExtHostWebview(handle, this._proxy, reviveOptions(options), this.initData, this.workspace, extension, this._deprecationService);
this._webviews.set(handle, webview);
webview._onDidDispose(() => { this._webviews.delete(handle); });
return webview;
}
private getWebview(handle: extHostProtocol.WebviewPanelHandle): ExtHostWebview | undefined {
return this._webviews.get(handle);
}
private getWebviewPanel(handle: extHostProtocol.WebviewPanelHandle): ExtHostWebviewEditor | undefined {
return this._webviewPanels.get(handle);
}
@ -894,14 +768,6 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
return provider;
}
private getWebviewView(handle: string): ExtHostWebviewView {
const entry = this._webviewViews.get(handle);
if (!entry) {
throw new Error('Custom document is not editable');
}
return entry;
}
private supportEditing(
provider: vscode.CustomTextEditorProvider | vscode.CustomEditorProvider | vscode.CustomReadonlyEditorProvider
): provider is vscode.CustomEditorProvider {

View file

@ -0,0 +1,176 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ExtHostWebview, ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview';
import type * as vscode from 'vscode';
import * as extHostProtocol from './extHost.protocol';
import * as extHostTypes from './extHostTypes';
class ExtHostWebviewView extends Disposable implements vscode.WebviewView {
readonly #handle: extHostProtocol.WebviewPanelHandle;
readonly #proxy: extHostProtocol.MainThreadWebviewsShape;
readonly #viewType: string;
readonly #webview: ExtHostWebview;
#isDisposed = false;
#isVisible: boolean;
#title: string | undefined;
constructor(
handle: extHostProtocol.WebviewPanelHandle,
proxy: extHostProtocol.MainThreadWebviewsShape,
viewType: string,
webview: ExtHostWebview,
isVisible: boolean,
) {
super();
this.#viewType = viewType;
this.#handle = handle;
this.#proxy = proxy;
this.#webview = webview;
this.#isVisible = isVisible;
}
public dispose() {
if (this.#isDisposed) {
return;
}
this.#isDisposed = true;
this.#onDidDispose.fire();
super.dispose();
}
readonly #onDidChangeVisibility = this._register(new Emitter<void>());
public readonly onDidChangeVisibility = this.#onDidChangeVisibility.event;
readonly #onDidDispose = this._register(new Emitter<void>());
public readonly onDidDispose = this.#onDidDispose.event;
public get title(): string | undefined {
this.assertNotDisposed();
return this.#title;
}
public set title(value: string | undefined) {
this.assertNotDisposed();
if (this.#title !== value) {
this.#title = value;
this.#proxy.$setWebviewViewTitle(this.#handle, value);
}
}
public get visible(): boolean { return this.#isVisible; }
public get webview(): vscode.Webview { return this.#webview; }
public get viewType(): string { return this.#viewType; }
/* internal */ _setVisible(visible: boolean) {
if (visible === this.#isVisible) {
return;
}
this.#isVisible = visible;
this.#onDidChangeVisibility.fire();
}
private assertNotDisposed() {
if (this.#isDisposed) {
throw new Error('Webview is disposed');
}
}
}
export class ExtHostWebviewViews implements extHostProtocol.ExtHostWebviewViewsShape {
private readonly _proxy: extHostProtocol.MainThreadWebviewsShape;
private readonly _viewProviders = new Map<string, {
readonly provider: vscode.WebviewViewProvider;
readonly extension: IExtensionDescription;
}>();
private readonly _webviewViews = new Map<extHostProtocol.WebviewPanelHandle, ExtHostWebviewView>();
constructor(
mainContext: extHostProtocol.IMainContext,
private readonly _extHostWebview: ExtHostWebviews,
) {
this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadWebviews);
}
public registerWebviewViewProvider(
extension: IExtensionDescription,
viewType: string,
provider: vscode.WebviewViewProvider,
webviewOptions?: {
retainContextWhenHidden?: boolean
},
): vscode.Disposable {
if (this._viewProviders.has(viewType)) {
throw new Error(`View provider for '${viewType}' already registered`);
}
this._viewProviders.set(viewType, { provider, extension });
this._proxy.$registerWebviewViewProvider(viewType, webviewOptions);
return new extHostTypes.Disposable(() => {
this._viewProviders.delete(viewType);
this._proxy.$unregisterWebviewViewProvider(viewType);
});
}
async $resolveWebviewView(
webviewHandle: string,
viewType: string,
state: any,
cancellation: CancellationToken,
): Promise<void> {
const entry = this._viewProviders.get(viewType);
if (!entry) {
throw new Error(`No view provider found for '${viewType}'`);
}
const { provider, extension } = entry;
const webview = this._extHostWebview.createNewWebview(webviewHandle, { /* todo */ }, extension);
const revivedView = new ExtHostWebviewView(webviewHandle, this._proxy, viewType, webview, true);
this._webviewViews.set(webviewHandle, revivedView);
await provider.resolveWebviewView(revivedView, { state }, cancellation);
}
async $onDidChangeWebviewViewVisibility(
webviewHandle: string,
visible: boolean
) {
const webviewView = this.getWebviewView(webviewHandle);
webviewView._setVisible(visible);
}
async $disposeWebviewView(webviewHandle: string) {
const webviewView = this.getWebviewView(webviewHandle);
this._webviewViews.delete(webviewHandle);
webviewView.dispose();
}
private getWebviewView(handle: string): ExtHostWebviewView {
const entry = this._webviewViews.get(handle);
if (!entry) {
throw new Error('Custom document is not editable');
}
return entry;
}
}