From 2038b8fc7f0e771aa5f015124b301c4e3a19c3f4 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 26 Feb 2018 10:06:50 -0800 Subject: [PATCH] Webview API prototype 3 (#44307) * Webview API prototype 3 Part of #43713 Third try at refining the webview api. This pass reworks #44165. 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 - Adds an `onDispose` event ot webviews. This is fired when a webview is closed by the user - Perist webview state when the editor group changes. This is enabled for all webviews, not just those with keep alive. * Throw error when trying to access disposed webview * Improving webview documentation * Clean up dispose management * Throw if we receive a bad handle * Move more event handling to input * Simplify input updating * Remove extra container property * Fixing md security alert button * Remove extra update container call * Restore syncing of preview to active editor * Fixing posting to webview * Debounce preview updates * Remove previewUri * Enable direct window.postMessage instead of window.parent.postMessage * Fixing scroll position not preserved when updating previews * Revert parent.postMessage change. Old behavior was correct * Properly hide webview container on tab switch * Make sure we only handle scroll events for the correct document * Don't try setting negative scroll * Revert vs code whitespace change --- extensions/markdown/media/csp.js | 9 +- extensions/markdown/media/main.js | 17 +- .../markdown/src/commands/refreshPreview.ts | 16 +- .../markdown/src/commands/showPreview.ts | 17 +- extensions/markdown/src/extension.ts | 29 +- .../src/features/previewContentProvider.ts | 205 +++++--- extensions/markdown/src/security.ts | 13 +- src/vs/vscode.proposed.d.ts | 56 ++- .../api/electron-browser/mainThreadWebview.ts | 438 ++++++++++++------ src/vs/workbench/api/node/extHost.api.impl.ts | 11 +- src/vs/workbench/api/node/extHost.protocol.ts | 26 +- .../api/node/extHostDocumentsAndEditors.ts | 30 +- .../workbench/api/node/extHostTextEditor.ts | 2 +- src/vs/workbench/api/node/extHostWebview.ts | 110 +++-- src/vs/workbench/browser/composite.ts | 24 +- .../parts/editor/editorGroupsControl.ts | 8 +- .../parts/html/browser/webview-pre.js | 20 +- .../workbench/parts/html/browser/webview.ts | 4 + 18 files changed, 680 insertions(+), 355 deletions(-) diff --git a/extensions/markdown/media/csp.js b/extensions/markdown/media/csp.js index 83015600f51..079ef567dcd 100644 --- a/extensions/markdown/media/csp.js +++ b/extensions/markdown/media/csp.js @@ -16,7 +16,6 @@ return; } didShow = true; - const args = [settings.previewUri]; const notification = document.createElement('a'); notification.innerText = strings.cspAlertMessageText; @@ -25,8 +24,12 @@ notification.setAttribute('role', 'button'); notification.setAttribute('aria-label', strings.cspAlertMessageLabel); - notification.setAttribute('href', `command:markdown.showPreviewSecuritySelector?${encodeURIComponent(JSON.stringify(args))}`); - + notification.onclick = () => { + window.parent.postMessage({ + command: 'markdown.showPreviewSecuritySelector', + args: [settings.source] + }, '*'); + }; document.body.appendChild(notification); }; diff --git a/extensions/markdown/media/main.js b/extensions/markdown/media/main.js index 42bac764d7a..dc925038685 100644 --- a/extensions/markdown/media/main.js +++ b/extensions/markdown/media/main.js @@ -146,7 +146,7 @@ } else { scrollTo = previous.element.getBoundingClientRect().top; } - window.scroll(0, window.scrollY + scrollTo + getSourceRevealAddedOffset()); + window.scroll(0, Math.max(1, window.scrollY + scrollTo + getSourceRevealAddedOffset())); } } @@ -193,13 +193,13 @@ function onLoad() { if (settings.scrollPreviewWithEditorSelection) { - const initialLine = +settings.line; - if (!isNaN(initialLine)) { - setTimeout(() => { + setTimeout(() => { + const initialLine = +settings.line; + if (!isNaN(initialLine)) { scrollDisabled = true; scrollToRevealSourceLine(initialLine); - }, 0); - } + } + }, 0); } } @@ -220,8 +220,13 @@ scrollToRevealSourceLine(line); }, 50); return event => { + if (event.data.source !== settings.source) { + return; + } + const line = +event.data.line; if (!isNaN(line)) { + settings.line = line; doScroll(line); } }; diff --git a/extensions/markdown/src/commands/refreshPreview.ts b/extensions/markdown/src/commands/refreshPreview.ts index 107e565da28..bac5487a04c 100644 --- a/extensions/markdown/src/commands/refreshPreview.ts +++ b/extensions/markdown/src/commands/refreshPreview.ts @@ -3,25 +3,17 @@ * 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 { MarkdownPreviewManager } from '../features/previewContentProvider'; export class RefreshPreviewCommand implements Command { public readonly id = 'markdown.refreshPreview'; public constructor( - private readonly webviewManager: MarkdownPreviewWebviewManager + private readonly webviewManager: MarkdownPreviewManager ) { } - 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 5383c686496..6b354331344 100644 --- a/extensions/markdown/src/commands/showPreview.ts +++ b/extensions/markdown/src/commands/showPreview.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode'; import { Command } from '../commandManager'; -import { MarkdownPreviewWebviewManager, } from '../features/previewContentProvider'; +import { MarkdownPreviewManager, } from '../features/previewContentProvider'; import { TelemetryReporter } from '../telemetryReporter'; @@ -30,12 +30,12 @@ function getViewColumn(sideBySide: boolean): vscode.ViewColumn | undefined { return active.viewColumn; } -function showPreview( - webviewManager: MarkdownPreviewWebviewManager, +async function showPreview( + webviewManager: MarkdownPreviewManager, telemetryReporter: TelemetryReporter, uri?: vscode.Uri, sideBySide: boolean = false, -) { +): Promise { let resource = uri; if (!(resource instanceof vscode.Uri)) { if (vscode.window.activeTextEditor) { @@ -53,23 +53,22 @@ function showPreview( return; } - const view = webviewManager.create( + webviewManager.preview( resource, + (vscode.window.activeTextEditor && vscode.window.activeTextEditor.viewColumn) || vscode.ViewColumn.One, getViewColumn(sideBySide) || vscode.ViewColumn.Active); telemetryReporter.sendTelemetryEvent('openPreview', { where: sideBySide ? 'sideBySide' : 'inPlace', how: (uri instanceof vscode.Uri) ? 'action' : 'pallete' }); - - return view; } export class ShowPreviewCommand implements Command { public readonly id = 'markdown.showPreview'; public constructor( - private readonly webviewManager: MarkdownPreviewWebviewManager, + private readonly webviewManager: MarkdownPreviewManager, private readonly telemetryReporter: TelemetryReporter ) { } @@ -84,7 +83,7 @@ export class ShowPreviewToSideCommand implements Command { public readonly id = 'markdown.showPreviewToSide'; public constructor( - private readonly webviewManager: MarkdownPreviewWebviewManager, + private readonly webviewManager: MarkdownPreviewManager, private readonly telemetryReporter: TelemetryReporter ) { } diff --git a/extensions/markdown/src/extension.ts b/extensions/markdown/src/extension.ts index 524bbe3fcd1..2dc6071316a 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, MarkdownPreviewManager } from './features/previewContentProvider'; export function activate(context: vscode.ExtensionContext) { @@ -30,20 +30,20 @@ export function activate(context: vscode.ExtensionContext) { const contentProvider = new MarkdownContentProvider(engine, context, cspArbiter, logger); loadMarkdownExtensions(contentProvider, engine); - const webviewManager = new MarkdownPreviewWebviewManager(contentProvider); - context.subscriptions.push(webviewManager); + const previewManager = new MarkdownPreviewManager(contentProvider, logger); + context.subscriptions.push(previewManager); context.subscriptions.push(vscode.languages.registerDocumentSymbolProvider(selector, new MDDocumentSymbolProvider(engine))); context.subscriptions.push(vscode.languages.registerDocumentLinkProvider(selector, new LinkProvider())); - const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, webviewManager); + const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, previewManager); const commandManager = new CommandManager(); context.subscriptions.push(commandManager); - commandManager.register(new commands.ShowPreviewCommand(webviewManager, telemetryReporter)); - commandManager.register(new commands.ShowPreviewToSideCommand(webviewManager, telemetryReporter)); + commandManager.register(new commands.ShowPreviewCommand(previewManager, telemetryReporter)); + commandManager.register(new commands.ShowPreviewToSideCommand(previewManager, telemetryReporter)); commandManager.register(new commands.ShowSourceCommand()); - commandManager.register(new commands.RefreshPreviewCommand(webviewManager)); + commandManager.register(new commands.RefreshPreviewCommand(previewManager)); commandManager.register(new commands.RevealLineCommand(logger)); commandManager.register(new commands.MoveCursorToPositionCommand()); commandManager.register(new commands.ShowPreviewSecuritySelectorCommand(previewSecuritySelector)); @@ -53,19 +53,6 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => { 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 - }); - } + previewManager.updateConfiguration(); })); } diff --git a/extensions/markdown/src/features/previewContentProvider.ts b/extensions/markdown/src/features/previewContentProvider.ts index a942b4e16e6..142bae287af 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 = []; @@ -234,19 +219,13 @@ export class MarkdownContentProvider { public async provideTextDocumentContent( sourceUri: vscode.Uri, - previewConfigurations: PreviewConfigManager + previewConfigurations: PreviewConfigManager, + initialLine: number | undefined = undefined ): Promise { - let initialLine: number | undefined = undefined; - const editor = vscode.window.activeTextEditor; - if (editor && editor.document.uri.toString() === sourceUri.toString()) { - initialLine = editor.selection.active.line; - } - const document = await vscode.workspace.openTextDocument(sourceUri); const config = previewConfigurations.loadAndCacheConfiguration(sourceUri); const initialData = { - previewUri: sourceUri.toString(), // TODO source: sourceUri.toString(), line: initialLine, scrollPreviewWithEditorSelection: config.scrollPreviewWithEditorSelection, @@ -296,21 +275,99 @@ export class MarkdownContentProvider { } } -export class MarkdownPreviewWebviewManager { - private readonly webviews = new Map(); +class MarkdownPreview { + private throttleTimer: any; + private initialLine: number | undefined = undefined; + + constructor( + public resource: vscode.Uri, + public webview: vscode.Webview, + public ofColumn: vscode.ViewColumn, + private readonly contentProvider: MarkdownContentProvider, + private readonly previewConfigurations: PreviewConfigManager + ) { } + + public update(resource: vscode.Uri) { + const editor = vscode.window.activeTextEditor; + if (editor && editor.document.uri.fsPath === resource.fsPath) { + this.initialLine = editor.selection.active.line; + } else { + this.initialLine = undefined; + } + + // Schedule update + if (!this.throttleTimer) { + this.throttleTimer = setTimeout(() => this.doUpdate(), resource.fsPath === this.resource.fsPath ? 300 : 0); + } + + this.resource = resource; + } + + public updateForSelection(resource: vscode.Uri, line: number) { + if (this.resource.fsPath !== resource.fsPath) { + return; + } + + this.initialLine = line; + this.webview.postMessage({ line, source: resource.toString() }); + } + + private getPreviewTitle(resource: vscode.Uri): string { + return localize('previewTitle', 'Preview {0}', path.basename(resource.fsPath)); + } + + private doUpdate() { + const resource = this.resource; + this.throttleTimer = undefined; + + this.contentProvider.provideTextDocumentContent(resource, this.previewConfigurations, this.initialLine) + .then(content => { + if (this.resource === resource) { + this.webview.title = this.getPreviewTitle(this.resource); + this.webview.html = content; + } + }); + } +} + +export class MarkdownPreviewManager { + private static webviewId = vscode.Uri.parse('vscode-markdown-preview://preview'); + + private previews: MarkdownPreview[] = []; private readonly previewConfigurations = new PreviewConfigManager(); private readonly disposables: vscode.Disposable[] = []; public constructor( - private readonly contentProvider: MarkdownContentProvider + private readonly contentProvider: MarkdownContentProvider, + private readonly logger: Logger ) { - 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.uri.fsPath === MarkdownPreviewManager.webviewId.fsPath); + + if (editor && editor.editorType === 'texteditor') { + if (isMarkdownFile(editor.document)) { + for (const preview of this.previews.filter(preview => preview.ofColumn === editor.viewColumn)) { + preview.update(editor.document.uri); + } + } + } + }, null, this.disposables); + + vscode.window.onDidChangeTextEditorSelection(event => { + if (!isMarkdownFile(event.textEditor.document)) { + return; + } + + const resource = event.textEditor.document.uri; + for (const previewForResource of this.previews.filter(preview => preview.resource.fsPath === resource.fsPath)) { + this.logger.log('updatePreviewForSelection', { markdownFile: resource }); + previewForResource.updateForSelection(resource, event.selections[0].active.line); + } }, null, this.disposables); } @@ -321,65 +378,73 @@ 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); + public refresh() { + for (const preview of this.previews) { + preview.update(preview.resource); } } - public updateAll() { - for (const resource of this.webviews.keys()) { - const sourceUri = vscode.Uri.parse(resource); - this.update(sourceUri); + public updateConfiguration() { + for (const preview of this.previews) { + if (this.previewConfigurations.shouldUpdateConfiguration(preview.resource)) { + preview.update(preview.resource); + } } } - public updateConfiguration() { - for (const resource of this.webviews.keys()) { - const sourceUri = vscode.Uri.parse(resource); - if (this.previewConfigurations.shouldUpdateConfiguration(sourceUri)) { - this.update(sourceUri); + 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.update(document.uri); } } } - 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) + // Only allow a single markdown preview per column + let preview = this.previews.find(preview => preview.webview.viewColumn === previewColumn); + if (preview) { + preview.resource = resource; + preview.ofColumn = resourceColumn; + } else { + const webview = vscode.window.createWebview( + MarkdownPreviewManager.webviewId, + previewColumn, { + enableScripts: true, + localResourceRoots: this.getLocalResourceRoots(resource) + }); + + webview.onDispose(() => { + const existing = this.previews.findIndex(preview => preview.webview === webview); + if (existing >= 0) { + this.previews.splice(existing, 1); + } }); - this.contentProvider.provideTextDocumentContent(resource, this.previewConfigurations).then(x => view.html = x); - - view.onMessage(e => { - vscode.commands.executeCommand(e.command, ...e.args); - }); - - view.onBecameActive(() => { - vscode.commands.executeCommand('setContext', 'markdownPreview', true); - }); + webview.onMessage(e => { + vscode.commands.executeCommand(e.command, ...e.args); + }); - view.onBecameInactive(() => { - vscode.commands.executeCommand('setContext', 'markdownPreview', false); - }); + preview = new MarkdownPreview(resource, webview, resourceColumn, this.contentProvider, this.previewConfigurations); + this.previews.push(preview); + } - this.webviews.set(resource.fsPath, view); - return view; + preview.update(preview.resource); + return preview.webview; } - private getLocalResourceRoots( - resource: vscode.Uri - ): vscode.Uri[] { + private getLocalResourceRoots(resource: vscode.Uri): vscode.Uri[] { const folder = vscode.workspace.getWorkspaceFolder(resource); if (folder) { return [folder.uri]; diff --git a/extensions/markdown/src/security.ts b/extensions/markdown/src/security.ts index 5978a9102b9..fb46f12a389 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 { MarkdownPreviewManager } from './features/previewContentProvider'; import * as nls from 'vscode-nls'; @@ -90,7 +90,7 @@ export class PreviewSecuritySelector { public constructor( private cspArbiter: ContentSecurityPolicyArbiter, - private webviewManager: MarkdownPreviewWebviewManager + private webviewManager: MarkdownPreviewManager ) { } public async showSecutitySelectorForResource(resource: vscode.Uri): Promise { @@ -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.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 87f0d7c5e3d..ca867faa03d 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -513,19 +513,31 @@ declare module 'vscode' { */ export interface Webview { /** - * Title of the webview. + * Type identifying the editor as a webview editor. */ - title: string; + readonly editorType: 'webview'; /** - * Contents of the webview. + * Unique identifer of the webview. */ - html: string; + readonly uri: Uri; /** * Content settings for the webview. */ - options: WebviewOptions; + readonly options: WebviewOptions; + + /** + * Title of the webview shown in UI. + */ + title: string; + + /** + * Contents of the webview. + * + * Should be a complete html document. + */ + html: string; /** * The column in which the webview is showing. @@ -538,14 +550,14 @@ declare module 'vscode' { readonly onMessage: Event; /** - * Fired when the webview becomes the active editor. + * Fired when the webview is disposed. */ - readonly onBecameActive: Event; + readonly onDispose: Event; /** - * Fired when the webview stops being the active editor + * Fired when the webview's view column changes. */ - readonly onBecameInactive: Event; + readonly onDidChangeViewColumn: Event; /** * Post a message to the webview content. @@ -554,23 +566,39 @@ declare module 'vscode' { * * @param message Body of the message. */ - postMessage(message: any): Thenable; + postMessage(message: any): Thenable; /** - * Dispose the webview. + * Dispose of the the webview. + * + * This closes the webview if it showing and disposes of the resources owned by the webview. + * Webview are also disposed when the user closes the webview editor. Both cases fire `onDispose` + * event. Trying to use the webview after it has been disposed throws an exception. */ dispose(): any; } + export interface TextEditor { + /** + * Type identifying the editor as a text editor. + */ + readonly editorType: 'texteditor'; + } + namespace window { /** * Create and show a new webview. * - * @param title Title of the webview. + * @param uri Unique identifier for the webview. * @param column Editor column to show the new webview in. - * @param options Webview content options. + * @param options Content settings for the webview. + */ + export function createWebview(uri: Uri, column: ViewColumn, options: WebviewOptions): Webview; + + /** + * Event fired when the active editor changes. */ - export function createWebview(title: string, column: ViewColumn, options: WebviewOptions): Webview; + export const onDidChangeActiveEditor: Event; } export namespace window { diff --git a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts index 6cbfd960fae..267c2290fe7 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts @@ -6,8 +6,8 @@ import * as map from 'vs/base/common/map'; import { TPromise } from 'vs/base/common/winjs.base'; -import { MainThreadWebviewShape, MainContext, IExtHostContext, ExtHostContext, ExtHostWebviewsShape } from 'vs/workbench/api/node/extHost.protocol'; -import { IDisposable, dispose, toDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { MainThreadWebviewsShape, MainContext, IExtHostContext, ExtHostContext, ExtHostWebviewsShape, WebviewHandle } from 'vs/workbench/api/node/extHost.protocol'; +import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { extHostNamedCustomer } from './extHostCustomers'; import { EditorInput, EditorModel, EditorOptions } from 'vs/workbench/common/editor'; import { IEditorModel, Position } from 'vs/platform/editor/common/editor'; @@ -28,32 +28,84 @@ import { localize } from 'vs/nls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import * as vscode from 'vscode'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import URI from 'vs/base/common/uri'; +import DOM = require('vs/base/browser/dom'); +import Event, { Emitter } from 'vs/base/common/event'; + interface WebviewEvents { onMessage(message: any): void; - onFocus(): void; - onBlur(): void; + onDidChangePosition(newPosition: Position): void; + onDispose(): void; + onDidClickLink(link: URI, options: vscode.WebviewOptions): void; } class WebviewInput extends EditorInput { + private static handlePool = 0; + private _name: string; private _options: vscode.WebviewOptions; private _html: string; + private _events: WebviewEvents | undefined; + private _container: HTMLElement; + private _webview: Webview | undefined; + private _webviewOwner: any; + private _webviewDisposables: IDisposable[] = []; + + public static create( + name: string, + options: vscode.WebviewOptions, + html: string, + events: WebviewEvents, + partService: IPartService + ): WebviewInput { + const id = WebviewInput.handlePool++; + const webviewContainer = document.createElement('div'); + webviewContainer.id = `webview-${id}`; + + partService.getContainer(Parts.EDITOR_PART).appendChild(webviewContainer); + + return new WebviewInput(name, options, html, events, webviewContainer, undefined); + } constructor( name: string, options: vscode.WebviewOptions, html: string, - public readonly events: WebviewEvents + events: WebviewEvents, + container: HTMLElement, + webview: Webview | undefined ) { super(); this._name = name; this._options = options; this._html = html; + this._events = events; + + this._container = container; + this._webview = webview; + } + + public getTypeId(): string { + return 'webview'; + } + + public dispose() { + this.disposeWebview(); + + if (this._container) { + this._container.remove(); + this._container = undefined; + } + + if (this._events) { + this._events.onDispose(); + this._events = undefined; + } + + super.dispose(); } public getName(): string { @@ -69,8 +121,12 @@ class WebviewInput extends EditorInput { return this._html; } - public set html(value: string) { + public setHtml(value: string): void { this._html = value; + + if (this._webview) { + this._webview.contents = value; + } } public get options(): vscode.WebviewOptions { @@ -81,71 +137,116 @@ class WebviewInput extends EditorInput { this._options = value; } - public getTypeId(): string { - return 'webview'; - } - public resolve(refresh?: boolean): TPromise { return TPromise.as(new EditorModel()); } + + public supportsSplitEditor() { + return false; + } + + public get container(): HTMLElement { + return this._container; + } + + public get webview(): Webview | undefined { + return this._webview; + } + + public set webview(value: Webview) { + this._webviewDisposables = dispose(this._webviewDisposables); + + this._webview = value; + + this._webview.onDidClickLink(link => { + if (this._events) { + this._events.onDidClickLink(link, this._options); + } + }, null, this._webviewDisposables); + + this._webview.onMessage(message => { + if (this._events) { + this._events.onMessage(message); + } + }, null, this._webviewDisposables); + } + + public claimWebview(owner: any) { + this._webviewOwner = owner; + } + + public releaseWebview(owner: any) { + if (this._webviewOwner === owner) { + this._webviewOwner = undefined; + if (!this._options.keepAlive) { + this.disposeWebview(); + } + } + } + + public disposeWebview() { + // The input owns the webview and its parent + if (this._webview) { + this._webview.dispose(); + this._webview = undefined; + } + + this._webviewDisposables = dispose(this._webviewDisposables); + + this._webviewOwner = undefined; + this.container.style.visibility = 'hidden'; + } + + public onDidChangePosition(position: Position) { + if (this._events) { + this._events.onDidChangePosition(position); + } + } } class WebviewEditor extends BaseWebviewEditor { - private static webviewIndex = 0; public static readonly ID = 'WebviewEditor'; - private static readonly standardSupportedLinkSchemes = ['http', 'https', 'mailto']; - - private frame: HTMLElement; - private container: HTMLElement; - private webviewContent: HTMLDivElement; - - private _contentDisposables: IDisposable[] = []; + private editorFrame: HTMLElement; + private webviewContent: HTMLElement; + private _onDidFocusWebview: Emitter; + private _webviewFocusTracker?: DOM.IFocusTracker; + private _webviewFocusListenerDisposable?: IDisposable; constructor( @ITelemetryService telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, - @IContextKeyService private _contextKeyService: IContextKeyService, @IThemeService themeService: IThemeService, + @IContextKeyService private _contextKeyService: IContextKeyService, @IPartService private readonly _partService: IPartService, @IContextViewService private readonly _contextViewService: IContextViewService, @IEnvironmentService private readonly _environmentService: IEnvironmentService, - @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, - @IOpenerService private readonly _openerService: IOpenerService + @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService ) { super(WebviewEditor.ID, telemetryService, themeService, storageService, _contextKeyService); + + this._onDidFocusWebview = new Emitter(); } protected createEditor(parent: Builder): void { - this.frame = parent.getHTMLElement(); - this.container = this._partService.getContainer(Parts.EDITOR_PART); - - this.webviewContent = document.createElement('div'); - this.webviewContent.id = `webview-${WebviewEditor.webviewIndex++}`; - this._contextKeyService = this._contextKeyService.createScoped(this.webviewContent); - this.contextKey = KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS.bindTo(this._contextKeyService); - this.findInputFocusContextKey = KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED.bindTo(this._contextKeyService); - this.findWidgetVisible = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE.bindTo(this._contextKeyService); - - this.container.appendChild(this.webviewContent); - + this.editorFrame = parent.getHTMLElement(); this.content = document.createElement('div'); - this.content.setAttribute('aria-flowto', this.webviewContent.id); - parent.append(this.content); - this.doUpdateContainer(); } private doUpdateContainer() { - const frameRect = this.frame.getBoundingClientRect(); - const containerRect = this.container.getBoundingClientRect(); - - this.webviewContent.style.position = 'absolute'; - this.webviewContent.style.top = `${frameRect.top - containerRect.top}px`; - this.webviewContent.style.left = `${frameRect.left - containerRect.left}px`; - this.webviewContent.style.width = `${frameRect.width}px`; - this.webviewContent.style.height = `${frameRect.height}px`; + const webviewContainer = this.input && (this.input as WebviewInput).container; + if (webviewContainer) { + const frameRect = this.editorFrame.getBoundingClientRect(); + const containerRect = webviewContainer.parentElement.getBoundingClientRect(); + + webviewContainer.style.position = 'absolute'; + webviewContainer.style.top = `${frameRect.top - containerRect.top}px`; + webviewContainer.style.left = `${frameRect.left - containerRect.left}px`; + webviewContainer.style.width = `${frameRect.width}px`; + webviewContainer.style.height = `${frameRect.height}px`; + } } public layout(dimension: Dimension): void { @@ -156,7 +257,20 @@ class WebviewEditor extends BaseWebviewEditor { } public dispose(): void { - this._contentDisposables = dispose(this._contentDisposables); + // Let the editor input dispose of the webview. + this._webview = undefined; + this.webviewContent = undefined; + + this._onDidFocusWebview.dispose(); + + if (this._webviewFocusTracker) { + this._webviewFocusTracker.dispose(); + } + + if (this._webviewFocusListenerDisposable) { + this._webviewFocusListenerDisposable.dispose(); + } + super.dispose(); } @@ -166,32 +280,50 @@ class WebviewEditor extends BaseWebviewEditor { } } - public getFocusContainer(): Builder { - return new Builder(this.webviewContent, false); + public get onDidFocus(): Event { + return this._onDidFocusWebview.event; } protected setEditorVisible(visible: boolean, position?: Position): void { - if (visible) { - this.webviewContent.style.visibility = 'visible'; - this.doUpdateContainer(); - } else { - if (this._webview) { + if (this.input && this.input instanceof WebviewInput) { + if (visible) { + this.input.claimWebview(this); + } else { + this.input.releaseWebview(this); + } + + this.updateWebview(this.input as WebviewInput); + } + + if (this.webviewContent) { + if (visible) { + this.webviewContent.style.visibility = 'visible'; + this.doUpdateContainer(); + } else { this.webviewContent.style.visibility = 'hidden'; } } super.setEditorVisible(visible, position); } - public clearInput(): void { + public clearInput() { if (this.input && this.input instanceof WebviewInput) { - if (this.input.options.keepAlive) { - // Noop - return; - } + this.input.releaseWebview(this); } + + this._webview = undefined; + this.webviewContent = undefined; + super.clearInput(); } + public changePosition(position: Position): void { + if (this.input && this.input instanceof WebviewInput) { + this.input.onDidChangePosition(position); + } + super.changePosition(position); + } + async setInput(input: WebviewInput, options: EditorOptions): TPromise { if (this.input && this.input.matches(input)) { return undefined; @@ -199,120 +331,129 @@ class WebviewEditor extends BaseWebviewEditor { await super.setInput(input, options); - this.webview.options = { + this.updateWebview(input); + } + + private updateWebview(input: WebviewInput) { + const webview = this.getWebview(input); + input.claimWebview(this); + webview.options = { allowScripts: input.options.enableScripts, enableWrappedPostMessage: true, useSameOriginForRoot: false, localResourceRoots: (input && input.options.localResourceRoots) || this._contextService.getWorkspace().folders.map(x => x.uri) }; - this.webview.contents = input.html; - } - - private get webview(): Webview { - if (!this._webview) { - this._contentDisposables = dispose(this._contentDisposables); - - this._webview = new Webview( - this.webviewContent, - this._partService.getContainer(Parts.EDITOR_PART), - this.themeService, - this._environmentService, - this._contextViewService, - this.contextKey, - this.findInputFocusContextKey, - { - enableWrappedPostMessage: true, - useSameOriginForRoot: false - }); - - this._webview.onDidClickLink(this.onDidClickLink, this, this._contentDisposables); - - this._webview.onMessage(message => { - if (this.input) { - (this.input as WebviewInput).events.onMessage(message); - } - }, null, this._contentDisposables); - - this._contentDisposables.push(this._webview); - this._contentDisposables.push(toDisposable(() => this._webview = null)); - } - return this._webview; + webview.contents = input.html; + this.webviewContent.style.visibility = 'visible'; + this.doUpdateContainer(); } - private onDidClickLink(link: URI): void { - if (!link) { - return; + private getWebview(input: WebviewInput): Webview { + if (this._webview) { + return this._webview; } - const enableCommandUris = (this.input as WebviewInput).options.enableCommandUris; - if (WebviewEditor.standardSupportedLinkSchemes.indexOf(link.scheme) >= 0 || enableCommandUris && link.scheme === 'command') { - this._openerService.open(link); + this.webviewContent = input.container; + const existing = input.webview; + if (existing) { + this._webview = existing; + return existing; } + + this._webviewFocusTracker = DOM.trackFocus(this.webviewContent); + this._webviewFocusListenerDisposable = this._webviewFocusTracker.onDidFocus(() => { + this._onDidFocusWebview.fire(); + }); + + this._contextKeyService = this._contextKeyService.createScoped(this.webviewContent); + this.contextKey = KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS.bindTo(this._contextKeyService); + this.findInputFocusContextKey = KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED.bindTo(this._contextKeyService); + this.findWidgetVisible = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE.bindTo(this._contextKeyService); + + this._webview = new Webview( + this.webviewContent, + this._partService.getContainer(Parts.EDITOR_PART), + this.themeService, + this._environmentService, + this._contextViewService, + this.contextKey, + this.findInputFocusContextKey, + { + enableWrappedPostMessage: true, + useSameOriginForRoot: false + }); + input.webview = this._webview; + + this.content.setAttribute('aria-flowto', this.webviewContent.id); + + this.doUpdateContainer(); + return this._webview; } } -@extHostNamedCustomer(MainContext.MainThreadWebview) -export class MainThreadWebview implements MainThreadWebviewShape { - private readonly _toDispose: Disposable[] = []; +@extHostNamedCustomer(MainContext.MainThreadWebviews) +export class MainThreadWebviews implements MainThreadWebviewsShape { + private static readonly standardSupportedLinkSchemes = ['http', 'https', 'mailto']; + + private _toDispose: Disposable[] = []; private readonly _proxy: ExtHostWebviewsShape; - private readonly _webviews = new Map(); + private readonly _webviews = new Map(); + private _activeWebview: WebviewInput | undefined = undefined; constructor( context: IExtHostContext, @IEditorGroupService _editorGroupService: IEditorGroupService, + @IContextKeyService _contextKeyService: IContextKeyService, + @IPartService private readonly _partService: IPartService, @IWorkbenchEditorService private readonly _editorService: IWorkbenchEditorService, - @IInstantiationService private readonly _instantiationService: IInstantiationService + @IOpenerService private readonly _openerService: IOpenerService ) { this._proxy = context.getProxy(ExtHostContext.ExtHostWebviews); - _editorGroupService.onEditorsChanged(this.onEditorsChanged, this, this._toDispose); } dispose(): void { - dispose(this._toDispose); + this._toDispose = dispose(this._toDispose); } - $createWebview(handle: number): void { - const webview = new WebviewInput('', {}, '', { - onMessage: (message) => this._proxy.$onMessage(handle, message), - onFocus: () => this._proxy.$onBecameActive(handle), - onBlur: () => this._proxy.$onBecameInactive(handle) - }); - this._webviews.set(handle, webview); - } + $createWebview(handle: WebviewHandle, uri: URI, options: vscode.WebviewOptions): void { + const webviewInput = WebviewInput.create('', options, '', { + onMessage: message => this._proxy.$onMessage(handle, message), + onDidChangePosition: position => this._proxy.$onDidChangePosition(handle, position), + onDispose: () => this._proxy.$onDidDisposeWeview(handle), + onDidClickLink: (link, options) => this.onDidClickLink(link, options) + }, this._partService); - $disposeWebview(handle: number): void { - const webview = this._webviews.get(handle); - this._editorService.closeEditor(Position.ONE, webview); + this._webviews.set(handle, webviewInput); } - $setTitle(handle: number, value: string): void { - const webview = this._webviews.get(handle); - webview.setName(value); + $disposeWebview(handle: WebviewHandle): void { + const webview = this.getWebview(handle); + this._editorService.closeEditors({ positionOne: [webview], positionTwo: [webview], positionThree: [webview] }); } - $setHtml(handle: number, value: string): void { - this.updateInput(handle, existingInput => - this._instantiationService.createInstance(WebviewInput, existingInput.getName(), existingInput.options, value, existingInput.events)); + $setTitle(handle: WebviewHandle, value: string): void { + const webview = this.getWebview(handle); + webview.setName(value); } - $setOptions(handle: number, newOptions: vscode.WebviewOptions): void { - this.updateInput(handle, existingInput => - this._instantiationService.createInstance(WebviewInput, existingInput.getName(), newOptions, existingInput.html, existingInput.events)); + $setHtml(handle: WebviewHandle, value: string): void { + const webview = this.getWebview(handle); + webview.setHtml(value); } - $show(handle: number, column: Position): void { - const webviewInput = this._webviews.get(handle); + $show(handle: WebviewHandle, column: Position): void { + const webviewInput = this.getWebview(handle); this._editorService.openEditor(webviewInput, { pinned: true }, column); } - async $sendMessage(handle: number, message: any): Promise { - const webviewInput = this._webviews.get(handle); + async $sendMessage(handle: WebviewHandle, message: any): Promise { + const webviewInput = this.getWebview(handle); const editors = this._editorService.getVisibleEditors() - .filter(e => e instanceof WebviewInput) + .filter(e => e instanceof WebviewEditor) .map(e => e as WebviewEditor) .filter(e => e.input.matches(webviewInput)); @@ -323,41 +464,50 @@ export class MainThreadWebview implements MainThreadWebviewShape { return (editors.length > 0); } - private updateInput(handle: number, 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 }]); + private getWebview(handle: number): WebviewInput { + const webviewInput = this._webviews.get(handle); + if (!webviewInput) { + throw new Error('Unknown webview handle:' + handle); + } + return webviewInput; } private onEditorsChanged() { const activeEditor = this._editorService.getActiveEditor(); - let newActiveWebview: WebviewInput | undefined = undefined; - if (activeEditor && activeEditor.input instanceof WebviewInput) { + let newActiveWebview: { input: WebviewInput, handle: WebviewHandle } | 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; } } } + + private onDidClickLink(link: URI, options: vscode.WebviewOptions): void { + if (!link) { + return; + } + + const enableCommandUris = options.enableCommandUris; + if (MainThreadWebviews.standardSupportedLinkSchemes.indexOf(link.scheme) >= 0 || enableCommandUris && link.scheme === 'command') { + this._openerService.open(link); + } + } } (Registry.as(EditorExtensions.Editors)).registerEditor(new EditorDescriptor( diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 77cc180bb9b..a97a2d0a464 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]); @@ -402,8 +402,11 @@ 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); + createWebview: proposedApiFunction(extension, (uri: vscode.Uri, column: vscode.ViewColumn, options: vscode.WebviewOptions) => { + return extHostWebviews.createWebview(uri, column, options); + }), + onDidChangeActiveEditor: proposedApiFunction(extension, (listener, thisArg?, disposables?) => { + return extHostDocumentsAndEditors.onDidChangeActiveEditor(listener, thisArg, disposables); }) }; diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 653ed148986..636dc41d27a 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -342,19 +342,21 @@ export interface MainThreadTelemetryShape extends IDisposable { $publicLog(eventName: string, data?: any): void; } -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; +export type WebviewHandle = number; + +export interface MainThreadWebviewsShape extends IDisposable { + $createWebview(handle: WebviewHandle, uri: URI, options: vscode.WebviewOptions): void; + $disposeWebview(handle: WebviewHandle): void; + $show(handle: WebviewHandle, column: EditorPosition): void; + $setTitle(handle: WebviewHandle, value: string): void; + $setHtml(handle: WebviewHandle, value: string): void; + $sendMessage(handle: WebviewHandle, value: any): Thenable; } export interface ExtHostWebviewsShape { - $onMessage(handle: number, message: any): void; - $onBecameActive(handle: number): void; - $onBecameInactive(handle: number): void; + $onMessage(handle: WebviewHandle, message: any): void; + $onDidChangeActiveWeview(handle: WebviewHandle | undefined): void; + $onDidDisposeWeview(handle: WebviewHandle): void; + $onDidChangePosition(handle: WebviewHandle, newPosition: EditorPosition): void; } export interface MainThreadWorkspaceShape extends IDisposable { @@ -817,7 +819,7 @@ export const MainContext = { MainThreadStorage: createMainId('MainThreadStorage'), MainThreadTelemetry: createMainId('MainThreadTelemetry'), MainThreadTerminalService: createMainId('MainThreadTerminalService'), - MainThreadWebview: createMainId('MainThreadWebview'), + MainThreadWebviews: createMainId('MainThreadWebviews'), MainThreadWorkspace: createMainId('MainThreadWorkspace'), MainThreadFileSystem: createMainId('MainThreadFileSystem'), MainThreadExtensionService: createMainId('MainThreadExtensionService'), diff --git a/src/vs/workbench/api/node/extHostDocumentsAndEditors.ts b/src/vs/workbench/api/node/extHostDocumentsAndEditors.ts index 65be19d03aa..0eafdce9d72 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 6ebf86fad18..356e6f12d8b 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 97117f0b4d2..c16da9d06fa 100644 --- a/src/vs/workbench/api/node/extHostWebview.ts +++ b/src/vs/workbench/api/node/extHostWebview.ts @@ -3,49 +3,66 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MainContext, MainThreadWebviewShape, IMainContext, ExtHostWebviewsShape } from './extHost.protocol'; +import { MainContext, MainThreadWebviewsShape, IMainContext, ExtHostWebviewsShape, WebviewHandle } 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'; +import { Position } from 'vs/platform/editor/common/editor'; + +export class ExtHostWebview implements vscode.Webview { + public readonly editorType = 'webview'; -class ExtHostWebview implements vscode.Webview { private _title: string; private _html: string; private _options: vscode.WebviewOptions; private _isDisposed: boolean = false; private _viewColumn: vscode.ViewColumn; - public readonly onMessageEmitter = new Emitter(); - public readonly onMessage = this.onMessageEmitter.event; + public readonly onMessage: Event = this.onMessageEmitter.event; - public readonly onBecameActiveEmitter = new Emitter(); - public readonly onBecameActive = this.onBecameActiveEmitter.event; + public readonly onDisposeEmitter = new Emitter(); + public readonly onDispose: Event = this.onDisposeEmitter.event; - public readonly onBecameInactiveEmitter = new Emitter(); - public readonly onBecameInactive = this.onBecameInactiveEmitter.event; + public readonly onDidChangeViewColumnEmitter = new Emitter(); + public readonly onDidChangeViewColumn: Event = this.onDidChangeViewColumnEmitter.event; constructor( - private readonly _proxy: MainThreadWebviewShape, - private readonly _handle: number, - viewColumn: vscode.ViewColumn + private readonly _handle: WebviewHandle, + private readonly _proxy: MainThreadWebviewsShape, + private readonly _uri: vscode.Uri, + viewColumn: vscode.ViewColumn, + options: vscode.WebviewOptions ) { this._viewColumn = viewColumn; + this._options = options; } public dispose() { if (this._isDisposed) { return; } + this._isDisposed = true; this._proxy.$disposeWebview(this._handle); + + this.onDisposeEmitter.dispose(); + this.onMessageEmitter.dispose(); + this.onDidChangeViewColumnEmitter.dispose(); + } + + get uri(): vscode.Uri { + this.assertNotDisposed(); + return this._uri; } 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); @@ -53,10 +70,12 @@ class ExtHostWebview implements vscode.Webview { } get html(): string { + this.assertNotDisposed(); return this._html; } set html(value: string) { + this.assertNotDisposed(); if (this._html !== value) { this._html = value; this._proxy.$setHtml(this._handle, value); @@ -64,63 +83,88 @@ class ExtHostWebview implements vscode.Webview { } get options(): vscode.WebviewOptions { + this.assertNotDisposed(); return this._options; } - set options(value: vscode.WebviewOptions) { - this._proxy.$setOptions(this._handle, value); - } - get viewColumn(): vscode.ViewColumn { + this.assertNotDisposed(); return this._viewColumn; } + set viewColumn(value: vscode.ViewColumn) { + this.assertNotDisposed(); + this._viewColumn = value; + } + public postMessage(message: any): Thenable { return this._proxy.$sendMessage(this._handle, message); } + + private assertNotDisposed() { + if (this._isDisposed) { + throw new Error('Webview is disposed'); + } + } } export class ExtHostWebviews implements ExtHostWebviewsShape { - private static _handlePool = 0; + private static handlePool = 0; - private readonly _proxy: MainThreadWebviewShape; + private readonly _proxy: MainThreadWebviewsShape; - private readonly _webviews = new Map(); + private readonly _webviews = new Map(); constructor( mainContext: IMainContext ) { - this._proxy = mainContext.getProxy(MainContext.MainThreadWebview); + this._proxy = mainContext.getProxy(MainContext.MainThreadWebviews); } createWebview( - title: string, + uri: vscode.Uri, viewColumn: vscode.ViewColumn, options: vscode.WebviewOptions ): vscode.Webview { - const handle = ExtHostWebviews._handlePool++; - this._proxy.$createWebview(handle); + const handle = ExtHostWebviews.handlePool++; + if (!this._webviews.has(handle)) { + this._proxy.$createWebview(handle, uri, options); + + const webview = new ExtHostWebview(handle, this._proxy, uri, viewColumn, options); + 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: WebviewHandle, message: any): void { const webview = this._webviews.get(handle); + webview.onMessageEmitter.fire(message); } - $onBecameActive(handle: number): void { + $onDidChangeActiveWeview(handle: WebviewHandle | undefined): void { const webview = this._webviews.get(handle); - webview.onBecameActiveEmitter.fire(); + this._onDidChangeActiveWebview.fire(webview); } - $onBecameInactive(handle: number): void { + $onDidDisposeWeview(handle: WebviewHandle): void { const webview = this._webviews.get(handle); - webview.onBecameInactiveEmitter.fire(); + if (webview) { + webview.onDisposeEmitter.fire(); + } + } + + $onDidChangePosition(handle: WebviewHandle, newPosition: Position): void { + const webview = this._webviews.get(handle); + if (webview) { + const newViewColumn = typeConverters.toViewColumn(newPosition); + webview.viewColumn = newViewColumn; + webview.onDidChangeViewColumnEmitter.fire(newViewColumn); + } } + + private readonly _onDidChangeActiveWebview = new Emitter(); + public readonly onDidChangeActiveWebview = this._onDidChangeActiveWebview.event; } \ No newline at end of file diff --git a/src/vs/workbench/browser/composite.ts b/src/vs/workbench/browser/composite.ts index c213bacd720..c2ac268a2a5 100644 --- a/src/vs/workbench/browser/composite.ts +++ b/src/vs/workbench/browser/composite.ts @@ -14,6 +14,8 @@ import { IEditorControl } from 'vs/platform/editor/common/editor'; import Event, { Emitter } from 'vs/base/common/event'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConstructorSignature0, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import DOM = require('vs/base/browser/dom'); +import { IDisposable } from 'vs/base/common/lifecycle'; /** * Composites are layed out in the sidebar and panel part of the workbench. At a time only one composite @@ -27,6 +29,10 @@ import { IConstructorSignature0, IInstantiationService } from 'vs/platform/insta */ export abstract class Composite extends Component implements IComposite { private _onTitleAreaUpdate: Emitter; + private _onDidFocus: Emitter; + + private _focusTracker?: DOM.IFocusTracker; + private _focusListenerDisposable?: IDisposable; private visible: boolean; private parent: Builder; @@ -45,6 +51,7 @@ export abstract class Composite extends Component implements IComposite { this.visible = false; this._onTitleAreaUpdate = new Emitter(); + this._onDidFocus = new Emitter(); } public getTitle(): string { @@ -85,8 +92,12 @@ export abstract class Composite extends Component implements IComposite { return this.parent; } - public getFocusContainer(): Builder { - return this.getContainer(); + public get onDidFocus(): Event { + this._focusTracker = DOM.trackFocus(this.getContainer().getHTMLElement()); + this._focusListenerDisposable = this._focusTracker.onDidFocus(() => { + this._onDidFocus.fire(); + }); + return this._onDidFocus.event; } /** @@ -189,6 +200,15 @@ export abstract class Composite extends Component implements IComposite { public dispose(): void { this._onTitleAreaUpdate.dispose(); + this._onDidFocus.dispose(); + + if (this._focusTracker) { + this._focusTracker.dispose(); + } + + if (this._focusListenerDisposable) { + this._focusListenerDisposable.dispose(); + } super.dispose(); } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts b/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts index 7e09e42c93b..cd796c1bcb4 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts @@ -35,7 +35,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { editorBackground, contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { Themable, EDITOR_GROUP_HEADER_TABS_BACKGROUND, EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND, EDITOR_GROUP_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, EDITOR_GROUP_BACKGROUND, EDITOR_GROUP_HEADER_TABS_BORDER } from 'vs/workbench/common/theme'; import { attachProgressBarStyler } from 'vs/platform/theme/common/styler'; -import { IDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { ResourcesDropHandler, LocalSelectionTransfer, DraggedEditorIdentifier } from 'vs/workbench/browser/dnd'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IPartService } from 'vs/workbench/services/part/common/partService'; @@ -454,13 +454,9 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro this.visibleEditorFocusTrackerDisposable[position].dispose(); } - // Track focus on editor container - const focusTracker = DOM.trackFocus(editor.getFocusContainer().getHTMLElement()); - const listenerDispose = focusTracker.onDidFocus(() => { + this.visibleEditorFocusTrackerDisposable[position] = editor.onDidFocus(() => { this.onFocusGained(editor); }); - - this.visibleEditorFocusTrackerDisposable[position] = combinedDisposable([focusTracker, listenerDispose]); } private onFocusGained(editor: BaseEditor): void { diff --git a/src/vs/workbench/parts/html/browser/webview-pre.js b/src/vs/workbench/parts/html/browser/webview-pre.js index 5331fa75e39..8c05d2cbe8e 100644 --- a/src/vs/workbench/parts/html/browser/webview-pre.js +++ b/src/vs/workbench/parts/html/browser/webview-pre.js @@ -70,6 +70,16 @@ } } + function onMessage(message) { + if (enableWrappedPostMessage) { + // Modern webview. Forward wrapped message + ipcRenderer.sendToHost('onmessage', message.data); + } else { + // Old school webview. Forward exact message + ipcRenderer.sendToHost(message.data.command, message.data.data); + } + } + var isHandlingScroll = false; function handleInnerScroll(event) { if (isHandlingScroll) { @@ -337,15 +347,7 @@ }); // Forward messages from the embedded iframe - window.onmessage = function (message) { - if (enableWrappedPostMessage) { - // Modern webview. Forward wrapped message - ipcRenderer.sendToHost('onmessage', message.data); - } else { - // Old school webview. Forward exact message - ipcRenderer.sendToHost(message.data.command, message.data.data); - } - }; + window.onmessage = onMessage; // signal ready ipcRenderer.sendToHost('webview-ready', process.pid); diff --git a/src/vs/workbench/parts/html/browser/webview.ts b/src/vs/workbench/parts/html/browser/webview.ts index 00669ce6dad..0632e2f6033 100644 --- a/src/vs/workbench/parts/html/browser/webview.ts +++ b/src/vs/workbench/parts/html/browser/webview.ts @@ -234,6 +234,10 @@ export class Webview { } public set contents(value: string) { + if (this._contents === value) { + return; + } + this._contents = value; this._send('content', { contents: value, -- GitLab