From d4052420993b2cd37b81e85d9f93238233a65ba0 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 15 Feb 2018 19:24:41 -0800 Subject: [PATCH] Webview API prototype 2 Part of #43713 Second try at refining the webview api. This pass specifically looks at managing webviews. Major changes: - Adds an `id` field to webviews. The id is provided by the extension and identifies the webview. It is used with the new event handling apis - Adds a new `onDidChangeActiveEditor` api. This is similar to `onDidChangeActiveTextEditor` but is also fired when you change webviews. It replaces the old `onFocus` and `onBlur` events on the webview itself - Replaces `createWebview` with `getOrCreateWebview`. This new API uses the id and column together as a key. The idea is that only a single webview of id may exist in a given column - Adds an `onDidCloseWebview` api. This is fired when a webview is closed by the user The motivation for these changes is #27983, which tracks using the same markdown preview for any active markdown files. I believe this case is similar to how other extensions may use the webview. --- .../markdown/src/commands/refreshPreview.ts | 14 +- .../markdown/src/commands/showPreview.ts | 3 +- extensions/markdown/src/extension.ts | 15 +- .../src/features/previewContentProvider.ts | 128 ++++++++++-------- extensions/markdown/src/security.ts | 11 +- src/vs/vscode.d.ts | 4 + src/vs/vscode.proposed.d.ts | 37 +++-- .../api/electron-browser/mainThreadWebview.ts | 80 +++++++---- src/vs/workbench/api/node/extHost.api.impl.ts | 16 ++- src/vs/workbench/api/node/extHost.protocol.ts | 20 +-- .../api/node/extHostDocumentsAndEditors.ts | 30 +++- .../workbench/api/node/extHostTextEditor.ts | 2 +- src/vs/workbench/api/node/extHostWebview.ts | 65 +++++---- 13 files changed, 249 insertions(+), 176 deletions(-) diff --git a/extensions/markdown/src/commands/refreshPreview.ts b/extensions/markdown/src/commands/refreshPreview.ts index 107e565da2884..cb781af067ff3 100644 --- a/extensions/markdown/src/commands/refreshPreview.ts +++ b/extensions/markdown/src/commands/refreshPreview.ts @@ -3,9 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; import { Command } from '../commandManager'; -import { isMarkdownFile, getMarkdownUri, MarkdownPreviewWebviewManager } from '../features/previewContentProvider'; +import { MarkdownPreviewWebviewManager } from '../features/previewContentProvider'; export class RefreshPreviewCommand implements Command { public readonly id = 'markdown.refreshPreview'; @@ -14,14 +13,7 @@ export class RefreshPreviewCommand implements Command { private readonly webviewManager: MarkdownPreviewWebviewManager ) { } - public execute(resource: string | undefined) { - if (resource) { - const source = vscode.Uri.parse(resource); - this.webviewManager.update(source); - } else if (vscode.window.activeTextEditor && isMarkdownFile(vscode.window.activeTextEditor.document)) { - this.webviewManager.update(getMarkdownUri(vscode.window.activeTextEditor.document.uri)); - } else { - this.webviewManager.updateAll(); - } + public execute() { + this.webviewManager.refresh(); } } \ No newline at end of file diff --git a/extensions/markdown/src/commands/showPreview.ts b/extensions/markdown/src/commands/showPreview.ts index 5383c68649694..607e461a80467 100644 --- a/extensions/markdown/src/commands/showPreview.ts +++ b/extensions/markdown/src/commands/showPreview.ts @@ -53,8 +53,9 @@ function showPreview( return; } - const view = webviewManager.create( + const view = webviewManager.preview( resource, + (vscode.window.activeTextEditor && vscode.window.activeTextEditor.viewColumn) || vscode.ViewColumn.One, getViewColumn(sideBySide) || vscode.ViewColumn.Active); telemetryReporter.sendTelemetryEvent('openPreview', { diff --git a/extensions/markdown/src/extension.ts b/extensions/markdown/src/extension.ts index 524bbe3fcd10a..933937a410c6a 100644 --- a/extensions/markdown/src/extension.ts +++ b/extensions/markdown/src/extension.ts @@ -14,7 +14,7 @@ import { loadDefaultTelemetryReporter } from './telemetryReporter'; import { loadMarkdownExtensions } from './markdownExtensions'; import LinkProvider from './features/documentLinkProvider'; import MDDocumentSymbolProvider from './features/documentSymbolProvider'; -import { MarkdownContentProvider, getMarkdownUri, isMarkdownFile, MarkdownPreviewWebviewManager } from './features/previewContentProvider'; +import { MarkdownContentProvider, MarkdownPreviewWebviewManager } from './features/previewContentProvider'; export function activate(context: vscode.ExtensionContext) { @@ -55,17 +55,4 @@ export function activate(context: vscode.ExtensionContext) { logger.updateConfiguration(); webviewManager.updateConfiguration(); })); - - context.subscriptions.push(vscode.window.onDidChangeTextEditorSelection(event => { - if (isMarkdownFile(event.textEditor.document)) { - const markdownFile = getMarkdownUri(event.textEditor.document.uri); - logger.log('updatePreviewForSelection', { markdownFile: markdownFile.toString() }); - - vscode.commands.executeCommand('_workbench.htmlPreview.postMessage', - markdownFile, - { - line: event.selections[0].active.line - }); - } - })); } diff --git a/extensions/markdown/src/features/previewContentProvider.ts b/extensions/markdown/src/features/previewContentProvider.ts index a942b4e16e63c..0e9b9cbced8bf 100644 --- a/extensions/markdown/src/features/previewContentProvider.ts +++ b/extensions/markdown/src/features/previewContentProvider.ts @@ -19,20 +19,7 @@ const previewStrings = { }; export function isMarkdownFile(document: vscode.TextDocument) { - return document.languageId === 'markdown' - && document.uri.scheme !== MarkdownContentProvider.scheme; // prevent processing of own documents -} - -export function getMarkdownUri(uri: vscode.Uri) { - if (uri.scheme === MarkdownContentProvider.scheme) { - return uri; - } - - return uri.with({ - scheme: MarkdownContentProvider.scheme, - path: uri.path + '.rendered', - query: uri.toString() - }); + return document.languageId === 'markdown'; } export class MarkdownPreviewConfig { @@ -137,8 +124,6 @@ export class PreviewConfigManager { } export class MarkdownContentProvider { - public static readonly scheme = 'markdown'; - private extraStyles: Array = []; private extraScripts: Array = []; @@ -296,8 +281,16 @@ export class MarkdownContentProvider { } } +interface MarkdownPreview { + resource: vscode.Uri; + webview: vscode.Webview; + ofColumn: vscode.ViewColumn; +} + export class MarkdownPreviewWebviewManager { - private readonly webviews = new Map(); + private static webviewId = 'vscode-markdown-preview'; + + private previews: MarkdownPreview[] = []; private readonly previewConfigurations = new PreviewConfigManager(); private readonly disposables: vscode.Disposable[] = []; @@ -305,12 +298,25 @@ export class MarkdownPreviewWebviewManager { public constructor( private readonly contentProvider: MarkdownContentProvider ) { - vscode.workspace.onDidSaveTextDocument(document => { - this.update(document.uri); + vscode.workspace.onDidChangeTextDocument(event => { + this.update(event.document, undefined); }, null, this.disposables); - vscode.workspace.onDidChangeTextDocument(event => { - this.update(event.document.uri); + vscode.window.onDidChangeActiveEditor(editor => { + vscode.commands.executeCommand('setContext', 'markdownPreview', editor && editor.editorType === 'webview' && editor.id === MarkdownPreviewWebviewManager.webviewId); + + if (editor && editor.editorType === 'texteditor') { + this.update(editor.document, editor.viewColumn); + } + }, null, this.disposables); + + vscode.window.onDidCloseWebview(webview => { + if (webview.id === MarkdownPreviewWebviewManager.webviewId) { + const existing = this.previews.findIndex(preview => preview.webview === webview); + if (existing >= 0) { + this.previews.splice(existing, 1); + } + } }, null, this.disposables); } @@ -321,60 +327,66 @@ export class MarkdownPreviewWebviewManager { item.dispose(); } } - this.webviews.clear(); + this.previews = []; } - public update(uri: vscode.Uri) { - const webview = this.webviews.get(uri.fsPath); - if (webview) { - this.contentProvider.provideTextDocumentContent(uri, this.previewConfigurations).then(x => webview.html = x); + private update(document: vscode.TextDocument, viewColumn: vscode.ViewColumn | undefined) { + if (!isMarkdownFile(document)) { + return; + } + + for (const preview of this.previews) { + if (preview.resource.fsPath === document.uri.fsPath || viewColumn && preview.ofColumn === viewColumn) { + preview.webview.title = this.getPreviewTitle(document.uri); + preview.resource = document.uri; + this.contentProvider.provideTextDocumentContent(document.uri, this.previewConfigurations).then(x => preview.webview.html = x); + } } } - public updateAll() { - for (const resource of this.webviews.keys()) { - const sourceUri = vscode.Uri.parse(resource); - this.update(sourceUri); + public refresh() { + for (const preview of this.previews) { + this.contentProvider.provideTextDocumentContent(preview.resource, this.previewConfigurations).then(x => preview.webview.html = x); } } public updateConfiguration() { - for (const resource of this.webviews.keys()) { - const sourceUri = vscode.Uri.parse(resource); - if (this.previewConfigurations.shouldUpdateConfiguration(sourceUri)) { - this.update(sourceUri); + for (const preview of this.previews) { + if (this.previewConfigurations.shouldUpdateConfiguration(preview.resource)) { + this.contentProvider.provideTextDocumentContent(preview.resource, this.previewConfigurations).then(x => preview.webview.html = x); } } } - public create( + public preview( resource: vscode.Uri, - viewColumn: vscode.ViewColumn + resourceColumn: vscode.ViewColumn, + previewColumn: vscode.ViewColumn ) { - const view = vscode.window.createWebview( - localize('previewTitle', 'Preview {0}', path.basename(resource.fsPath)), - viewColumn, - { - enableScripts: true, - localResourceRoots: this.getLocalResourceRoots(resource) - }); + const webview: vscode.Webview = vscode.window.getOrCreateWebview( + MarkdownPreviewWebviewManager.webviewId, + previewColumn); - this.contentProvider.provideTextDocumentContent(resource, this.previewConfigurations).then(x => view.html = x); - - view.onMessage(e => { - vscode.commands.executeCommand(e.command, ...e.args); - }); + webview.options = { + enableScripts: true, + localResourceRoots: this.getLocalResourceRoots(resource) + }; + webview.title = this.getPreviewTitle(resource); + + const existing = this.previews.find(preview => preview.webview.viewColumn === webview.viewColumn); + if (existing) { + existing.resource = resource; + } else { + webview.onMessage(e => { + vscode.commands.executeCommand(e.command, ...e.args); + }); - view.onBecameActive(() => { - vscode.commands.executeCommand('setContext', 'markdownPreview', true); - }); + this.previews.push({ webview, resource, ofColumn: resourceColumn }); + } - view.onBecameInactive(() => { - vscode.commands.executeCommand('setContext', 'markdownPreview', false); - }); + this.contentProvider.provideTextDocumentContent(resource, this.previewConfigurations).then(x => webview.html = x); - this.webviews.set(resource.fsPath, view); - return view; + return webview; } private getLocalResourceRoots( @@ -391,4 +403,8 @@ export class MarkdownPreviewWebviewManager { return []; } + + private getPreviewTitle(resource: vscode.Uri): string { + return localize('previewTitle', 'Preview {0}', path.basename(resource.fsPath)); + } } \ No newline at end of file diff --git a/extensions/markdown/src/security.ts b/extensions/markdown/src/security.ts index 5978a9102b98a..5c14d9bbdcaa6 100644 --- a/extensions/markdown/src/security.ts +++ b/extensions/markdown/src/security.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; -import { getMarkdownUri, MarkdownPreviewWebviewManager } from './features/previewContentProvider'; +import { MarkdownPreviewWebviewManager } from './features/previewContentProvider'; import * as nls from 'vscode-nls'; @@ -143,15 +143,12 @@ export class PreviewSecuritySelector { return; } - const sourceUri = getMarkdownUri(resource); if (selection.type === 'toggle') { this.cspArbiter.setShouldDisableSecurityWarning(!this.cspArbiter.shouldDisableSecurityWarnings()); - this.webviewManager.update(sourceUri); return; + } else { + await this.cspArbiter.setSecurityLevelForResource(resource, selection.type); } - - await this.cspArbiter.setSecurityLevelForResource(resource, selection.type); - - this.webviewManager.update(sourceUri); + this.webviewManager.refresh(); } } diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 3ca7322116ddd..6654a59d5a8a4 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -1028,6 +1028,10 @@ declare module 'vscode' { * Represents an editor that is attached to a [document](#TextDocument). */ export interface TextEditor { + /** + * Type identifying the editor as a text editor. + */ + readonly editorType: 'texteditor'; /** * The document associated with this text editor. The document will be the same for the entire lifetime of this text editor. diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index a3be8f6188820..037356990ee62 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -439,7 +439,17 @@ declare module 'vscode' { */ export interface Webview { /** - * Title of the webview. + * Type identifying the editor as a webview editor. + */ + readonly editorType: 'webview'; + + /** + * Unique internal identifer of the webview. + */ + readonly id: string; + + /** + * Title of the webview shown in UI. */ title: string; @@ -463,16 +473,6 @@ declare module 'vscode' { */ readonly onMessage: Event; - /** - * Fired when the webview becomes the active editor. - */ - readonly onBecameActive: Event; - - /** - * Fired when the webview stops being the active editor - */ - readonly onBecameInactive: Event; - /** * Post a message to the webview content. * @@ -492,10 +492,19 @@ declare module 'vscode' { /** * Create and show a new webview. * - * @param title Title of the webview. + * @param id Unique identifier for the webview. * @param column Editor column to show the new webview in. - * @param options Webview content options. */ - export function createWebview(title: string, column: ViewColumn, options: WebviewOptions): Webview; + export function getOrCreateWebview(id: string, column: ViewColumn): Webview; + + /** + * Event fired when the active editor changes. + */ + export const onDidChangeActiveEditor: Event; + + /** + * Event fired when a webview is closed. + */ + export const onDidCloseWebview: Event; } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts index 6cbfd960fae5d..24e6751e00319 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts @@ -35,8 +35,6 @@ import URI from 'vs/base/common/uri'; interface WebviewEvents { onMessage(message: any): void; - onFocus(): void; - onBlur(): void; } class WebviewInput extends EditorInput { @@ -189,6 +187,12 @@ class WebviewEditor extends BaseWebviewEditor { return; } } + + if (this._webview) { + this._webview.dispose(); + this._webview = undefined; + } + super.clearInput(); } @@ -257,7 +261,9 @@ export class MainThreadWebview implements MainThreadWebviewShape { private readonly _toDispose: Disposable[] = []; private readonly _proxy: ExtHostWebviewsShape; - private readonly _webviews = new Map(); + private readonly _webviews = new Map(); + private readonly _disposeSubscriptions = new Map(); + private _activeWebview: WebviewInput | undefined = undefined; constructor( @@ -273,43 +279,52 @@ export class MainThreadWebview implements MainThreadWebviewShape { dispose(): void { dispose(this._toDispose); + + for (const sub of map.values(this._disposeSubscriptions)) { + sub.dispose(); + } + this._disposeSubscriptions.clear(); } - $createWebview(handle: number): void { + $createWebview(handle: string): void { const webview = new WebviewInput('', {}, '', { - onMessage: (message) => this._proxy.$onMessage(handle, message), - onFocus: () => this._proxy.$onBecameActive(handle), - onBlur: () => this._proxy.$onBecameInactive(handle) + onMessage: (message) => this._proxy.$onMessage(handle, message) }); + + this._disposeSubscriptions.set(handle, webview.onDispose(() => { + this._proxy.$onDidDisposeWeview(handle); + })); + this._webviews.set(handle, webview); } - $disposeWebview(handle: number): void { + $disposeWebview(handle: string): void { const webview = this._webviews.get(handle); this._editorService.closeEditor(Position.ONE, webview); } - $setTitle(handle: number, value: string): void { + $setTitle(handle: string, value: string): void { const webview = this._webviews.get(handle); webview.setName(value); } - $setHtml(handle: number, value: string): void { - this.updateInput(handle, existingInput => - this._instantiationService.createInstance(WebviewInput, existingInput.getName(), existingInput.options, value, existingInput.events)); + $setHtml(handle: string, value: string): void { + this.updateInput(handle, existingInput => { + return this._instantiationService.createInstance(WebviewInput, existingInput.getName(), existingInput.options, value, existingInput.events); + }); } - $setOptions(handle: number, newOptions: vscode.WebviewOptions): void { + $setOptions(handle: string, newOptions: vscode.WebviewOptions): void { this.updateInput(handle, existingInput => this._instantiationService.createInstance(WebviewInput, existingInput.getName(), newOptions, existingInput.html, existingInput.events)); } - $show(handle: number, column: Position): void { + $show(handle: string, column: Position): void { const webviewInput = this._webviews.get(handle); this._editorService.openEditor(webviewInput, { pinned: true }, column); } - async $sendMessage(handle: number, message: any): Promise { + async $sendMessage(handle: string, message: any): Promise { const webviewInput = this._webviews.get(handle); const editors = this._editorService.getVisibleEditors() .filter(e => e instanceof WebviewInput) @@ -323,37 +338,48 @@ export class MainThreadWebview implements MainThreadWebviewShape { return (editors.length > 0); } - private updateInput(handle: number, f: (existingInput: WebviewInput) => WebviewInput) { + private updateInput(handle: string, f: (existingInput: WebviewInput) => WebviewInput) { const existingInput = this._webviews.get(handle); const newInput = f(existingInput); this._webviews.set(handle, newInput); - this._editorService.replaceEditors([{ toReplace: existingInput, replaceWith: newInput }]); + + const existing = this._disposeSubscriptions.get(handle); + this._disposeSubscriptions.set(handle, newInput.onDispose(() => { + this._proxy.$onDidDisposeWeview(handle); + })); + + if (existing) { + existing.dispose(); + } + + this._editorService.replaceEditors([{ + toReplace: existingInput, + replaceWith: newInput, + options: { preserveFocus: true } + }]); } private onEditorsChanged() { const activeEditor = this._editorService.getActiveEditor(); - let newActiveWebview: WebviewInput | undefined = undefined; - if (activeEditor && activeEditor.input instanceof WebviewInput) { + let newActiveWebview: { input: WebviewInput, handle: string } | undefined = undefined; + if (activeEditor.input instanceof WebviewInput) { for (const handle of map.keys(this._webviews)) { const input = this._webviews.get(handle); if (input.matches(activeEditor.input)) { - newActiveWebview = input; + newActiveWebview = { input, handle }; break; } } } if (newActiveWebview) { - if (!this._activeWebview || !newActiveWebview.matches(this._activeWebview)) { - if (this._activeWebview) { - this._activeWebview.events.onBlur(); - } - newActiveWebview.events.onFocus(); - this._activeWebview = newActiveWebview; + if (!this._activeWebview || !newActiveWebview.input.matches(this._activeWebview)) { + this._proxy.$onDidChangeActiveWeview(newActiveWebview.handle); + this._activeWebview = newActiveWebview.input; } } else { if (this._activeWebview) { - this._activeWebview.events.onBlur(); + this._proxy.$onDidChangeActiveWeview(undefined); this._activeWebview = undefined; } } diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 466456f5bc3dd..7b2bf021266cb 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -97,7 +97,8 @@ export function createApiFactory( rpcProtocol.set(ExtHostContext.ExtHostLogService, extHostLogService); const extHostHeapService = rpcProtocol.set(ExtHostContext.ExtHostHeapService, new ExtHostHeapService()); const extHostDecorations = rpcProtocol.set(ExtHostContext.ExtHostDecorations, new ExtHostDecorations(rpcProtocol)); - const extHostDocumentsAndEditors = rpcProtocol.set(ExtHostContext.ExtHostDocumentsAndEditors, new ExtHostDocumentsAndEditors(rpcProtocol)); + const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol)); + const extHostDocumentsAndEditors = rpcProtocol.set(ExtHostContext.ExtHostDocumentsAndEditors, new ExtHostDocumentsAndEditors(rpcProtocol, extHostWebviews)); const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors)); const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors)); const extHostDocumentSaveParticipant = rpcProtocol.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostLogService, extHostDocuments, rpcProtocol.getProxy(MainContext.MainThreadTextEditors))); @@ -117,7 +118,6 @@ export function createApiFactory( const extHostTask = rpcProtocol.set(ExtHostContext.ExtHostTask, new ExtHostTask(rpcProtocol, extHostWorkspace)); const extHostWindow = rpcProtocol.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(rpcProtocol)); rpcProtocol.set(ExtHostContext.ExtHostExtensionService, extensionService); - const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol)); // Check that no named customers are missing const expected: ProxyIdentifier[] = Object.keys(ExtHostContext).map((key) => ExtHostContext[key]); @@ -399,9 +399,15 @@ export function createApiFactory( registerDecorationProvider: proposedApiFunction(extension, (provider: vscode.DecorationProvider) => { return extHostDecorations.registerDecorationProvider(provider, extension.id); }), - createWebview: proposedApiFunction(extension, (name: string, column: vscode.ViewColumn, options: vscode.WebviewOptions) => { - return extHostWebviews.createWebview(name, column, options); - }) + getOrCreateWebview: proposedApiFunction(extension, (id: string, column: vscode.ViewColumn) => { + return extHostWebviews.getOrCreateWebview(id, column); + }), + onDidChangeActiveEditor: proposedApiFunction(extension, (listener, thisArg?, disposables?) => { + return extHostDocumentsAndEditors.onDidChangeActiveEditor(listener, thisArg, disposables); + }), + onDidCloseWebview: proposedApiFunction(extension, (listener, thisArg?, disposables?) => { + return extHostWebviews.onDidDisposeWebview(listener, thisArg, disposables); + }), }; // namespace: workspace diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index b4715e17ff2c7..8dc06582605d1 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -346,18 +346,18 @@ export interface MainThreadTelemetryShape extends IDisposable { } export interface MainThreadWebviewShape extends IDisposable { - $createWebview(handle: number): void; - $disposeWebview(handle: number): void; - $show(handle: number, column: EditorPosition): void; - $setTitle(handle: number, value: string): void; - $setHtml(handle: number, value: string): void; - $setOptions(handle: number, value: vscode.WebviewOptions): void; - $sendMessage(handle: number, value: any): Thenable; + $createWebview(handle: string): void; + $disposeWebview(handle: string): void; + $show(handle: string, column: EditorPosition): void; + $setTitle(handle: string, value: string): void; + $setHtml(handle: string, value: string): void; + $setOptions(handle: string, value: vscode.WebviewOptions): void; + $sendMessage(handle: string, value: any): Thenable; } export interface ExtHostWebviewsShape { - $onMessage(handle: number, message: any): void; - $onBecameActive(handle: number): void; - $onBecameInactive(handle: number): void; + $onMessage(handle: string, message: any): void; + $onDidChangeActiveWeview(handle: string | undefined): void; + $onDidDisposeWeview(handle: string): void; } export interface MainThreadWorkspaceShape extends IDisposable { diff --git a/src/vs/workbench/api/node/extHostDocumentsAndEditors.ts b/src/vs/workbench/api/node/extHostDocumentsAndEditors.ts index 65be19d03aaf2..0eafdce9d7237 100644 --- a/src/vs/workbench/api/node/extHostDocumentsAndEditors.ts +++ b/src/vs/workbench/api/node/extHostDocumentsAndEditors.ts @@ -12,10 +12,16 @@ import { ExtHostTextEditor } from './extHostTextEditor'; import * as assert from 'assert'; import * as typeConverters from './extHostTypeConverters'; import URI from 'vs/base/common/uri'; +import { ExtHostWebview, ExtHostWebviews } from './extHostWebview'; +import { Disposable } from './extHostTypes'; export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsShape { + private _disposables: Disposable[] = []; + private _activeEditorId: string; + private _activeWebview: ExtHostWebview; + private readonly _editors = new Map(); private readonly _documents = new Map(); @@ -23,15 +29,34 @@ export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsSha private readonly _onDidRemoveDocuments = new Emitter(); private readonly _onDidChangeVisibleTextEditors = new Emitter(); private readonly _onDidChangeActiveTextEditor = new Emitter(); + private readonly _onDidChangeActiveEditor = new Emitter(); readonly onDidAddDocuments: Event = this._onDidAddDocuments.event; readonly onDidRemoveDocuments: Event = this._onDidRemoveDocuments.event; readonly onDidChangeVisibleTextEditors: Event = this._onDidChangeVisibleTextEditors.event; readonly onDidChangeActiveTextEditor: Event = this._onDidChangeActiveTextEditor.event; + readonly onDidChangeActiveEditor: Event = this._onDidChangeActiveEditor.event; constructor( - private readonly _mainContext: IMainContext + private readonly _mainContext: IMainContext, + _extHostWebviews?: ExtHostWebviews ) { + if (_extHostWebviews) { + _extHostWebviews.onDidChangeActiveWebview(webview => { + if (webview) { + if (webview !== this._activeWebview) { + this._onDidChangeActiveEditor.fire(webview); + this._activeWebview = webview; + } + } else { + this._activeWebview = webview; + } + }, this, this._disposables); + } + } + + dispose() { + this._disposables = dispose(this._disposables); } $acceptDocumentsAndEditorsDelta(delta: IDocumentsAndEditorsDelta): void { @@ -117,6 +142,9 @@ export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsSha } if (delta.newActiveEditor !== undefined) { this._onDidChangeActiveTextEditor.fire(this.activeEditor()); + + const activeEditor = this.activeEditor(); + this._onDidChangeActiveEditor.fire(activeEditor || this._activeWebview); } } diff --git a/src/vs/workbench/api/node/extHostTextEditor.ts b/src/vs/workbench/api/node/extHostTextEditor.ts index 6ebf86fad18b2..356e6f12d8b21 100644 --- a/src/vs/workbench/api/node/extHostTextEditor.ts +++ b/src/vs/workbench/api/node/extHostTextEditor.ts @@ -313,7 +313,7 @@ export class ExtHostTextEditorOptions implements vscode.TextEditorOptions { export class ExtHostTextEditor implements vscode.TextEditor { - public readonly type = 'texteditor'; + public readonly editorType = 'texteditor'; private readonly _proxy: MainThreadTextEditorsShape; private readonly _id: string; diff --git a/src/vs/workbench/api/node/extHostWebview.ts b/src/vs/workbench/api/node/extHostWebview.ts index 97117f0b4d281..634647485dba6 100644 --- a/src/vs/workbench/api/node/extHostWebview.ts +++ b/src/vs/workbench/api/node/extHostWebview.ts @@ -5,10 +5,12 @@ import { MainContext, MainThreadWebviewShape, IMainContext, ExtHostWebviewsShape } from './extHost.protocol'; import * as vscode from 'vscode'; -import { Emitter } from 'vs/base/common/event'; +import Event, { Emitter } from 'vs/base/common/event'; import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters'; -class ExtHostWebview implements vscode.Webview { +export class ExtHostWebview implements vscode.Webview { + public readonly editorType = 'webview'; + private _title: string; private _html: string; private _options: vscode.WebviewOptions; @@ -17,17 +19,12 @@ class ExtHostWebview implements vscode.Webview { public readonly onMessageEmitter = new Emitter(); - public readonly onMessage = this.onMessageEmitter.event; - - public readonly onBecameActiveEmitter = new Emitter(); - public readonly onBecameActive = this.onBecameActiveEmitter.event; - - public readonly onBecameInactiveEmitter = new Emitter(); - public readonly onBecameInactive = this.onBecameInactiveEmitter.event; + public readonly onMessage: Event = this.onMessageEmitter.event; constructor( + private readonly _id: string, private readonly _proxy: MainThreadWebviewShape, - private readonly _handle: number, + private readonly _handle: string, viewColumn: vscode.ViewColumn ) { this._viewColumn = viewColumn; @@ -41,6 +38,10 @@ class ExtHostWebview implements vscode.Webview { this._proxy.$disposeWebview(this._handle); } + get id(): string { + return this._id; + } + get title(): string { return this._title; } @@ -81,11 +82,9 @@ class ExtHostWebview implements vscode.Webview { } export class ExtHostWebviews implements ExtHostWebviewsShape { - private static _handlePool = 0; - private readonly _proxy: MainThreadWebviewShape; - private readonly _webviews = new Map(); + private readonly _webviews = new Map(); constructor( mainContext: IMainContext @@ -93,34 +92,42 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { this._proxy = mainContext.getProxy(MainContext.MainThreadWebview); } - createWebview( - title: string, - viewColumn: vscode.ViewColumn, - options: vscode.WebviewOptions + getOrCreateWebview( + id: string, + viewColumn: vscode.ViewColumn ): vscode.Webview { - const handle = ExtHostWebviews._handlePool++; - this._proxy.$createWebview(handle); + const handle = `webview-${id}-${viewColumn}`; + if (!this._webviews.has(handle)) { + this._proxy.$createWebview(handle); + + const webview = new ExtHostWebview(id, this._proxy, handle, viewColumn); + this._webviews.set(handle, webview); + } - const webview = new ExtHostWebview(this._proxy, handle, viewColumn); - this._webviews.set(handle, webview); - webview.title = title; - webview.options = options; this._proxy.$show(handle, typeConverters.fromViewColumn(viewColumn)); - return webview; + return this._webviews.get(handle); } - $onMessage(handle: number, message: any): void { + $onMessage(handle: string, message: any): void { const webview = this._webviews.get(handle); webview.onMessageEmitter.fire(message); } - $onBecameActive(handle: number): void { + $onDidChangeActiveWeview(handle: string | undefined): void { const webview = this._webviews.get(handle); - webview.onBecameActiveEmitter.fire(); + this._onDidChangeActiveWebview.fire(webview); } - $onBecameInactive(handle: number): void { + $onDidDisposeWeview(handle: string): void { const webview = this._webviews.get(handle); - webview.onBecameInactiveEmitter.fire(); + if (webview) { + this._onDidDisposeWebview.fire(webview); + } } + + private readonly _onDidChangeActiveWebview = new Emitter(); + public readonly onDidChangeActiveWebview = this._onDidChangeActiveWebview.event; + + private readonly _onDidDisposeWebview = new Emitter(); + public readonly onDidDisposeWebview = this._onDidDisposeWebview.event; } \ No newline at end of file