From 7aa99827c86bfcc07565539ef569b8d1d92818ec Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 21 Mar 2018 21:25:14 -0700 Subject: [PATCH 1/6] First attempt at webview restoration api From #45994 Adds an experimental API that allows extensions to persist webviews and restore them even after vscode restarts. Uses this api to restore markdown previews. This is done by: - Adding a new `state` field on webviews. This is a json serializable blob of data - Adds a new `WebviewReviver` interface (name will probably change). This binds a specific viewType to a provider that can restore a webview's contents from its `state` - In VS Code core, persist webview editors. When we restart and need to show a webview, activate all extensions that may have reviviers for it using the `onView:viewType` activation event. Current implementation is sort of a mess. Will try to clean things up --- .../markdown-language-features/package.json | 3 +- .../src/features/preview.ts | 93 ++++++++-- .../src/features/previewManager.ts | 40 +++- src/vs/vscode.proposed.d.ts | 36 +++- .../api/electron-browser/mainThreadWebview.ts | 120 ++++++++---- src/vs/workbench/api/node/extHost.api.impl.ts | 3 + src/vs/workbench/api/node/extHost.protocol.ts | 8 +- src/vs/workbench/api/node/extHostWebview.ts | 55 +++++- .../electron-browser/releaseNotesEditor.ts | 61 +++--- .../electron-browser/webview.contribution.ts | 14 +- .../webview/electron-browser/webviewEditor.ts | 32 ++-- .../webview/electron-browser/webviewInput.ts | 74 ++++---- .../electron-browser/webviewInputFactory.ts | 54 ++++++ .../electron-browser/webviewService.ts | 175 ++++++++++++++++++ 14 files changed, 616 insertions(+), 152 deletions(-) create mode 100644 src/vs/workbench/parts/webview/electron-browser/webviewInputFactory.ts create mode 100644 src/vs/workbench/parts/webview/electron-browser/webviewService.ts diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index e02fb758ba27f..15f4c4b0ebbdd 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -22,7 +22,8 @@ "onCommand:markdown.showPreviewToSide", "onCommand:markdown.showLockedPreviewToSide", "onCommand:markdown.showSource", - "onCommand:markdown.showPreviewSecuritySelector" + "onCommand:markdown.showPreviewSecuritySelector", + "onView:markdown.preview" ], "contributes": { "commands": [ diff --git a/extensions/markdown-language-features/src/features/preview.ts b/extensions/markdown-language-features/src/features/preview.ts index 1002bcf0fc59a..df97c35023ea5 100644 --- a/extensions/markdown-language-features/src/features/preview.ts +++ b/extensions/markdown-language-features/src/features/preview.ts @@ -18,7 +18,7 @@ const localize = nls.loadMessageBundle(); export class MarkdownPreview { - public static previewViewType = 'markdown.preview'; + public static viewType = 'markdown.preview'; private readonly webview: vscode.Webview; private throttleTimer: any; @@ -29,26 +29,67 @@ export class MarkdownPreview { private forceUpdate = false; private isScrolling = false; - constructor( - private _resource: vscode.Uri, + public static revive( + webview: vscode.Webview, + contentProvider: MarkdownContentProvider, + previewConfigurations: MarkdownPreviewConfigurationManager, + logger: Logger, + topmostLineMonitor: MarkdownFileTopmostLineMonitor + ): MarkdownPreview { + const resource = vscode.Uri.parse(webview.state.resource); + const locked = webview.state.locked; + + return new MarkdownPreview( + webview, + resource, + locked, + contentProvider, + previewConfigurations, + logger, + topmostLineMonitor); + } + + public static create( + resource: vscode.Uri, previewColumn: vscode.ViewColumn, - public locked: boolean, - private readonly contentProvider: MarkdownContentProvider, - private readonly previewConfigurations: MarkdownPreviewConfigurationManager, - private readonly logger: Logger, + locked: boolean, + contentProvider: MarkdownContentProvider, + previewConfigurations: MarkdownPreviewConfigurationManager, + logger: Logger, topmostLineMonitor: MarkdownFileTopmostLineMonitor, - private readonly contributions: MarkdownContributions - ) { - this.webview = vscode.window.createWebview( - MarkdownPreview.previewViewType, - this.getPreviewTitle(this._resource), + contributions: MarkdownContributions + ): MarkdownPreview { + const webview = vscode.window.createWebview( + MarkdownPreview.viewType, + MarkdownPreview.getPreviewTitle(resource, locked), previewColumn, { enableScripts: true, enableCommandUris: true, enableFindWidget: true, - localResourceRoots: this.getLocalResourceRoots(_resource) + localResourceRoots: MarkdownPreview.getLocalResourceRoots(resource, contributions) }); + return new MarkdownPreview( + webview, + resource, + locked, + contentProvider, + previewConfigurations, + logger, + topmostLineMonitor); + } + + private constructor( + webview: vscode.Webview, + private _resource: vscode.Uri, + public locked: boolean, + private readonly contentProvider: MarkdownContentProvider, + private readonly previewConfigurations: MarkdownPreviewConfigurationManager, + private readonly logger: Logger, + topmostLineMonitor: MarkdownFileTopmostLineMonitor + ) { + this.webview = webview; + this.webview.onDidDispose(() => { this.dispose(); }, null, this.disposables); @@ -99,6 +140,8 @@ export class MarkdownPreview { }); } }, null, this.disposables); + + this.updateState(); } private readonly _onDisposeEmitter = new vscode.EventEmitter(); @@ -195,11 +238,12 @@ export class MarkdownPreview { public toggleLock() { this.locked = !this.locked; - this.webview.title = this.getPreviewTitle(this._resource); + this.webview.title = MarkdownPreview.getPreviewTitle(this._resource, this.locked); + this.updateState(); } - private getPreviewTitle(resource: vscode.Uri): string { - return this.locked + private static getPreviewTitle(resource: vscode.Uri, locked: boolean): string { + return locked ? localize('lockedPreviewTitle', '[Preview] {0}', path.basename(resource.fsPath)) : localize('previewTitle', 'Preview {0}', path.basename(resource.fsPath)); } @@ -244,14 +288,18 @@ export class MarkdownPreview { this.contentProvider.provideTextDocumentContent(document, this.previewConfigurations, this.initialLine) .then(content => { if (this._resource === resource) { - this.webview.title = this.getPreviewTitle(this._resource); + this.webview.title = MarkdownPreview.getPreviewTitle(this._resource, this.locked); this.webview.html = content; + this.updateState(); } }); } - private getLocalResourceRoots(resource: vscode.Uri): vscode.Uri[] { - const baseRoots = this.contributions.previewResourceRoots; + private static getLocalResourceRoots( + resource: vscode.Uri, + contributions: MarkdownContributions + ): vscode.Uri[] { + const baseRoots = contributions.previewResourceRoots; const folder = vscode.workspace.getWorkspaceFolder(resource); if (folder) { @@ -292,6 +340,13 @@ export class MarkdownPreview { } } } + + private updateState() { + this.webview.state = { + resource: this.resource.toString(), + locked: this.locked + }; + } } export interface PreviewSettings { diff --git a/extensions/markdown-language-features/src/features/previewManager.ts b/extensions/markdown-language-features/src/features/previewManager.ts index 61fee517b43ad..ce76312999651 100644 --- a/extensions/markdown-language-features/src/features/previewManager.ts +++ b/extensions/markdown-language-features/src/features/previewManager.ts @@ -14,7 +14,7 @@ import { isMarkdownFile } from '../util/file'; import { MarkdownPreviewConfigurationManager } from './previewConfig'; import { MarkdownContributions } from '../markdownExtensions'; -export class MarkdownPreviewManager { +export class MarkdownPreviewManager implements vscode.WebviewReviver { private static readonly markdownPreviewActiveContextKey = 'markdownPreviewFocus'; private readonly topmostLineMonitor = new MarkdownFileTopmostLineMonitor(); @@ -29,15 +29,14 @@ export class MarkdownPreviewManager { private readonly contributions: MarkdownContributions ) { vscode.window.onDidChangeActiveTextEditor(editor => { - if (editor) { - if (isMarkdownFile(editor.document)) { - for (const preview of this.previews.filter(preview => !preview.locked)) { - preview.update(editor.document.uri); - } + if (editor && isMarkdownFile(editor.document)) { + for (const preview of this.previews.filter(preview => !preview.locked)) { + preview.update(editor.document.uri); } } }, null, this.disposables); + this.disposables.push(vscode.window.registerWebviewReviver(MarkdownPreview.viewType, this)); } public dispose(): void { @@ -66,7 +65,6 @@ export class MarkdownPreviewManager { preview.reveal(previewSettings.previewColumn); } else { preview = this.createNewPreview(resource, previewSettings); - this.previews.push(preview); } preview.update(resource); @@ -90,6 +88,22 @@ export class MarkdownPreviewManager { } } + public reviveWebview( + webview: vscode.Webview + ): void { + console.log('It\'s close to midnight...'); + + const preview = MarkdownPreview.revive( + webview, + this.contentProvider, + this.previewConfigurations, + this.logger, + this.topmostLineMonitor); + + this.registerPreview(preview); + preview.refresh(); + } + private getExistingPreview( resource: vscode.Uri, previewSettings: PreviewSettings @@ -101,8 +115,8 @@ export class MarkdownPreviewManager { private createNewPreview( resource: vscode.Uri, previewSettings: PreviewSettings - ) { - const preview = new MarkdownPreview( + ): MarkdownPreview { + const preview = MarkdownPreview.create( resource, previewSettings.previewColumn, previewSettings.locked, @@ -112,6 +126,14 @@ export class MarkdownPreviewManager { this.topmostLineMonitor, this.contributions); + return this.registerPreview(preview); + } + + private registerPreview( + preview: MarkdownPreview + ): MarkdownPreview { + this.previews.push(preview); + preview.onDispose(() => { const existing = this.previews.indexOf(preview!); if (existing >= 0) { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 5d0f2e25d8613..bbcc77a82dfeb 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -568,7 +568,7 @@ declare module 'vscode' { */ export interface Webview { /** - * The type of the webview, such as `'markdownw.preview'` + * The type of the webview, such as `'markdown.preview'` */ readonly viewType: string; @@ -589,6 +589,11 @@ declare module 'vscode' { */ html: string; + /** + * JSON serializable blob of data saved on webviews for revival. + */ + state: any; + /** * The column in which the webview is showing. */ @@ -636,16 +641,43 @@ declare module 'vscode' { dispose(): any; } + /** + * Restores webviews that have been persisted when vscode shuts down. + */ + interface WebviewReviver { + /** + * Restore a webview's `html` from its `state`. + * + * Called when a serialized webview first becomes active. + * + * @param webview Webview to revive. + */ + reviveWebview(webview: Webview): void; + } + namespace window { /** * Create and show a new webview. * - * @param viewType Identifier the type of the webview. + * @param viewType Identifies the type of the webview. * @param title Title of the webview. * @param column Editor column to show the new webview in. * @param options Content settings for the webview. */ export function createWebview(viewType: string, title: string, column: ViewColumn, options: WebviewOptions): Webview; + + /** + * Registers a webview reviver. + * + * Extensions that support reviving should have an `"onView:viewType"` activation method and + * make sure that `registerWebviewReviver` is called during activation. + * + * Only a single reviver may be registered at a time for a given `viewType`. + * + * @param viewType Type of the webview that can be revived. + * @param reviver Webview revivier. + */ + export function registerWebviewReviver(viewType: string, reviver: WebviewReviver): Disposable; } //#endregion diff --git a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts index aba0f0c1621d9..5348c211a2f54 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts @@ -10,36 +10,45 @@ import { extHostNamedCustomer } from './extHostCustomers'; import { Position } from 'vs/platform/editor/common/editor'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IPartService } from 'vs/workbench/services/part/common/partService'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; import * as vscode from 'vscode'; -import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; -import URI from 'vs/base/common/uri'; -import { WebviewInput } from 'vs/workbench/parts/webview/electron-browser/webviewInput'; +import { WebviewEditorInput } from 'vs/workbench/parts/webview/electron-browser/webviewInput'; import { WebviewEditor } from 'vs/workbench/parts/webview/electron-browser/webviewEditor'; - +import { IWebviewService, WebviewReviver } from 'vs/workbench/parts/webview/electron-browser/webviewService'; +import { IEditorGroupService } from '../../services/group/common/groupService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import URI from 'vs/base/common/uri'; +import { IExtensionService } from '../../services/extensions/common/extensions'; @extHostNamedCustomer(MainContext.MainThreadWebviews) -export class MainThreadWebviews implements MainThreadWebviewsShape { +export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviver { + + private static readonly viewType = 'mainThreadWebview'; + private static readonly standardSupportedLinkSchemes = ['http', 'https', 'mailto']; + private static revivalPool = 0; + private _toDispose: Disposable[] = []; private readonly _proxy: ExtHostWebviewsShape; - private readonly _webviews = new Map(); + private readonly _webviews = new Map(); + private readonly _revivers = new Set(); - private _activeWebview: WebviewInput | undefined = undefined; + private _activeWebview: WebviewEditorInput | undefined = undefined; constructor( context: IExtHostContext, @IContextKeyService _contextKeyService: IContextKeyService, - @IPartService private readonly _partService: IPartService, + @IEditorGroupService _editorGroupService: IEditorGroupService, @IWorkbenchEditorService private readonly _editorService: IWorkbenchEditorService, - @IEditorGroupService private readonly _editorGroupService: IEditorGroupService, - @IOpenerService private readonly _openerService: IOpenerService + @IWebviewService private readonly _webviewService: IWebviewService, + @IOpenerService private readonly _openerService: IOpenerService, + @IExtensionService private readonly _extensionService: IExtensionService ) { this._proxy = context.getProxy(ExtHostContext.ExtHostWebviews); _editorGroupService.onEditorsChanged(this.onEditorsChanged, this, this._toDispose); + + _webviewService.registerReviver(MainThreadWebviews.viewType, this); } dispose(): void { @@ -54,27 +63,28 @@ export class MainThreadWebviews implements MainThreadWebviewsShape { options: vscode.WebviewOptions, extensionFolderPath: string ): void { - const webviewInput = new WebviewInput(title, options, '', { + const webview = this._webviewService.createWebview(MainThreadWebviews.viewType, title, column, options, extensionFolderPath, { + onDidClickLink: uri => this.onDidClickLink(uri, webview.options), onMessage: message => this._proxy.$onMessage(handle, message), onDidChangePosition: position => this._proxy.$onDidChangePosition(handle, position), onDispose: () => { this._proxy.$onDidDisposeWeview(handle).then(() => { this._webviews.delete(handle); }); - }, - onDidClickLink: (link, options) => this.onDidClickLink(link, options) - }, this._partService); + } + }); - this._webviews.set(handle, webviewInput); + webview.state = { + viewType: viewType, + state: undefined + }; - this._editorService.openEditor(webviewInput, { pinned: true }, column); + this._webviews.set(handle, webview); } $disposeWebview(handle: WebviewHandle): void { const webview = this.getWebview(handle); - if (webview) { - this._editorService.closeEditor(webview.position, webview); - } + webview.dispose(); } $setTitle(handle: WebviewHandle, value: string): void { @@ -84,24 +94,25 @@ export class MainThreadWebviews implements MainThreadWebviewsShape { $setHtml(handle: WebviewHandle, value: string): void { const webview = this.getWebview(handle); - webview.setHtml(value); + webview.html = value; + } + + $setState(handle: WebviewHandle, value: string): void { + const webview = this.getWebview(handle); + webview.state.state = value; } $reveal(handle: WebviewHandle, column: Position): void { - const webviewInput = this.getWebview(handle); - if (webviewInput.position === column) { - this._editorService.openEditor(webviewInput, { preserveFocus: true }, column); - } else { - this._editorGroupService.moveEditor(webviewInput, webviewInput.position, column, { preserveFocus: true }); - } + const webview = this.getWebview(handle); + this._webviewService.revealWebview(webview, column); } async $sendMessage(handle: WebviewHandle, message: any): Promise { - const webviewInput = this.getWebview(handle); + const webview = this.getWebview(handle); const editors = this._editorService.getVisibleEditors() .filter(e => e instanceof WebviewEditor) .map(e => e as WebviewEditor) - .filter(e => e.input.matches(webviewInput)); + .filter(e => e.input.matches(webview)); for (const editor of editors) { editor.sendMessage(message); @@ -110,18 +121,52 @@ export class MainThreadWebviews implements MainThreadWebviewsShape { return (editors.length > 0); } - private getWebview(handle: number): WebviewInput { - const webviewInput = this._webviews.get(handle); - if (!webviewInput) { + $registerReviver(viewType: string): void { + this._revivers.add(viewType); + } + + $unregisterReviver(viewType: string): void { + this._revivers.delete(viewType); + } + + reviveWebview(webview: WebviewEditorInput): boolean { + this._extensionService.activateByEvent(`onView:${webview.state.viewType}`).then(() => { + const handle = 'revival-' + MainThreadWebviews.revivalPool++; + this._webviews.set(handle, webview); + + webview._events = { + onDidClickLink: uri => this.onDidClickLink(uri, webview.options), + onMessage: message => this._proxy.$onMessage(handle, message), + onDidChangePosition: position => this._proxy.$onDidChangePosition(handle, position), + onDispose: () => { + this._proxy.$onDidDisposeWeview(handle).then(() => { + this._webviews.delete(handle); + }); + } + }; + + this._proxy.$reviveWebview(handle, webview.state.viewType, webview.state.state, webview.position, webview.options); + }); + + return true; + } + + canRevive(webview: WebviewEditorInput): boolean { + return this._revivers.has(webview.viewType) || webview.reviver !== null; + } + + private getWebview(handle: WebviewHandle): WebviewEditorInput { + const webview = this._webviews.get(handle); + if (!webview) { throw new Error('Unknown webview handle:' + handle); } - return webviewInput; + return webview; } private onEditorsChanged() { const activeEditor = this._editorService.getActiveEditor(); - let newActiveWebview: { input: WebviewInput, handle: WebviewHandle } | undefined = undefined; - if (activeEditor && activeEditor.input instanceof WebviewInput) { + let newActiveWebview: { input: WebviewEditorInput, handle: WebviewHandle } | undefined = undefined; + if (activeEditor && activeEditor.input instanceof WebviewEditorInput) { for (const handle of map.keys(this._webviews)) { const input = this._webviews.get(handle); if (input.matches(activeEditor.input)) { @@ -132,7 +177,7 @@ export class MainThreadWebviews implements MainThreadWebviewsShape { } if (newActiveWebview) { - if (!this._activeWebview || !newActiveWebview.input.matches(this._activeWebview)) { + if (!this._activeWebview || newActiveWebview.input !== this._activeWebview) { this._proxy.$onDidChangeActiveWeview(newActiveWebview.handle); this._activeWebview = newActiveWebview.input; } @@ -154,4 +199,5 @@ export class MainThreadWebviews implements MainThreadWebviewsShape { this._openerService.open(link); } } + } diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index de10ae5c3cfd5..8f9accef0d195 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -418,6 +418,9 @@ export function createApiFactory( }), createWebview: proposedApiFunction(extension, (viewType: string, title: string, column: vscode.ViewColumn, options: vscode.WebviewOptions) => { return extHostWebviews.createWebview(viewType, title, column, options, extension.extensionFolderPath); + }), + registerWebviewReviver: proposedApiFunction(extension, (viewType: string, reviver: vscode.WebviewReviver) => { + return extHostWebviews.registerWebviewReviver(viewType, reviver); }) }; diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 1a1336e336739..31857a9c07cb3 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -347,7 +347,7 @@ export interface MainThreadTelemetryShape extends IDisposable { $publicLog(eventName: string, data?: any): void; } -export type WebviewHandle = number; +export type WebviewHandle = string; export interface MainThreadWebviewsShape extends IDisposable { $createWebview(handle: WebviewHandle, viewType: string, title: string, column: EditorPosition, options: vscode.WebviewOptions, extensionFolderPath: string): void; @@ -355,13 +355,19 @@ export interface MainThreadWebviewsShape extends IDisposable { $reveal(handle: WebviewHandle, column: EditorPosition): void; $setTitle(handle: WebviewHandle, value: string): void; $setHtml(handle: WebviewHandle, value: string): void; + $setState(handle: WebviewHandle, value: any): void; $sendMessage(handle: WebviewHandle, value: any): Thenable; + + $registerReviver(viewType: string): void; + $unregisterReviver(viewType: string): void; } + export interface ExtHostWebviewsShape { $onMessage(handle: WebviewHandle, message: any): void; $onDidChangeActiveWeview(handle: WebviewHandle | undefined): void; $onDidDisposeWeview(handle: WebviewHandle): Thenable; $onDidChangePosition(handle: WebviewHandle, newPosition: EditorPosition): void; + $reviveWebview(newWebviewHandle: WebviewHandle, viewType: string, state: any, position: EditorPosition, options: vscode.WebviewOptions): void; } export interface MainThreadWorkspaceShape extends IDisposable { diff --git a/src/vs/workbench/api/node/extHostWebview.ts b/src/vs/workbench/api/node/extHostWebview.ts index 27f69d0ec1dc8..0e2dc76a39d69 100644 --- a/src/vs/workbench/api/node/extHostWebview.ts +++ b/src/vs/workbench/api/node/extHostWebview.ts @@ -9,6 +9,7 @@ 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'; import { TPromise } from 'vs/base/common/winjs.base'; +import { Disposable } from './extHostTypes'; export class ExtHostWebview implements vscode.Webview { @@ -19,6 +20,7 @@ export class ExtHostWebview implements vscode.Webview { private _isDisposed: boolean = false; private _viewColumn: vscode.ViewColumn; private _active: boolean; + private _state: any; public readonly onMessageEmitter = new Emitter(); public readonly onDidReceiveMessage: Event = this.onMessageEmitter.event; @@ -85,6 +87,17 @@ export class ExtHostWebview implements vscode.Webview { } } + get state(): any { + this.assertNotDisposed(); + return this._state; + } + + set state(value: any) { + this.assertNotDisposed(); + this._state = value; + this._proxy.$setState(this._handle, value); + } + get options(): vscode.WebviewOptions { this.assertNotDisposed(); return this._options; @@ -128,11 +141,12 @@ export class ExtHostWebview implements vscode.Webview { } export class ExtHostWebviews implements ExtHostWebviewsShape { - private static handlePool = 1; + private static webviewHandlePool = 1; private readonly _proxy: MainThreadWebviewsShape; private readonly _webviews = new Map(); + private readonly _revivers = new Map(); private _activeWebview: ExtHostWebview | undefined; @@ -149,7 +163,7 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { options: vscode.WebviewOptions, extensionFolderPath: string ): vscode.Webview { - const handle = ExtHostWebviews.handlePool++; + const handle = ExtHostWebviews.webviewHandlePool++ + ''; this._proxy.$createWebview(handle, viewType, title, typeConverters.fromViewColumn(viewColumn), options, extensionFolderPath); const webview = new ExtHostWebview(handle, this._proxy, viewType, viewColumn, options); @@ -157,6 +171,23 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { return webview; } + registerWebviewReviver( + viewType: string, + reviver: vscode.WebviewReviver + ): vscode.Disposable { + if (this._revivers.has(viewType)) { + throw new Error(`Reviver for '${viewType}' already registered`); + } + + this._revivers.set(viewType, reviver); + this._proxy.$registerReviver(viewType); + + return new Disposable(() => { + this._revivers.delete(viewType); + this._proxy.$unregisterReviver(viewType); + }); + } + $onMessage(handle: WebviewHandle, message: any): void { const webview = this.getWebview(handle); if (webview) { @@ -206,8 +237,24 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { } } - private readonly _onDidChangeActiveWebview = new Emitter(); - public readonly onDidChangeActiveWebview = this._onDidChangeActiveWebview.event; + $reviveWebview( + webviewHandle: WebviewHandle, + viewType: string, + state: any, + position: Position, + options: vscode.WebviewOptions + ): void { + const reviver = this._revivers.get(viewType); + if (!reviver) { + return; + } + + const webview = new ExtHostWebview(webviewHandle, this._proxy, viewType, typeConverters.toViewColumn(position), options); + webview.state = state; + + this._webviews.set(webviewHandle, webview); + reviver.reviveWebview(webview); + } private getWebview(handle: WebviewHandle) { return this._webviews.get(handle); diff --git a/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts b/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts index bf6f7c9feca59..12978917cf6d1 100644 --- a/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts +++ b/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts @@ -5,29 +5,29 @@ 'use strict'; -import { TPromise } from 'vs/base/common/winjs.base'; +import { onUnexpectedError } from 'vs/base/common/errors'; import { marked } from 'vs/base/common/marked/marked'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { tokenizeToString } from 'vs/editor/common/modes/textToHtmlTokenizer'; +import { OS } from 'vs/base/common/platform'; +import URI from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { asText } from 'vs/base/node/request'; import { IMode, TokenizationRegistry } from 'vs/editor/common/modes'; import { generateTokensCSSForColorMap } from 'vs/editor/common/modes/supports/tokenization'; -import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { tokenizeToString } from 'vs/editor/common/modes/textToHtmlTokenizer'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import * as nls from 'vs/nls'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { KeybindingIO } from 'vs/workbench/services/keybinding/common/keybindingIO'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IRequestService } from 'vs/platform/request/node/request'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IPartService } from 'vs/workbench/services/part/common/partService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IRequestService } from 'vs/platform/request/node/request'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { WebviewInput } from 'vs/workbench/parts/webview/electron-browser/webviewInput'; -import { onUnexpectedError } from 'vs/base/common/errors'; import { addGAParameters } from 'vs/platform/telemetry/node/telemetryNodeUtils'; -import URI from 'vs/base/common/uri'; -import { asText } from 'vs/base/node/request'; -import * as nls from 'vs/nls'; -import { OS } from 'vs/base/common/platform'; -import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; +import { IWebviewService } from 'vs/workbench/parts/webview/electron-browser/webviewService'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { KeybindingIO } from 'vs/workbench/services/keybinding/common/keybindingIO'; +import { Position } from 'vs/platform/editor/common/editor'; +import { WebviewEditorInput } from 'vs/workbench/parts/webview/electron-browser/webviewInput'; function renderBody( body: string, @@ -51,18 +51,17 @@ export class ReleaseNotesManager { private _releaseNotesCache: { [version: string]: TPromise; } = Object.create(null); - private _currentReleaseNotes: WebviewInput | undefined = undefined; + private _currentReleaseNotes: WebviewEditorInput | undefined = undefined; public constructor( - @IEditorGroupService private readonly _editorGroupService: IEditorGroupService, @IEnvironmentService private readonly _environmentService: IEnvironmentService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @IModeService private readonly _modeService: IModeService, @IOpenerService private readonly _openerService: IOpenerService, - @IPartService private readonly _partService: IPartService, @IRequestService private readonly _requestService: IRequestService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IWorkbenchEditorService private readonly _editorService: IWorkbenchEditorService, + @IWebviewService private readonly _webviewService: IWebviewService, ) { } public async show( @@ -73,21 +72,23 @@ export class ReleaseNotesManager { const html = await this.renderBody(releaseNoteText); const title = nls.localize('releaseNotesInputName', "Release Notes: {0}", version); + const activeEditor = this._editorService.getActiveEditor(); if (this._currentReleaseNotes) { this._currentReleaseNotes.setName(title); - this._currentReleaseNotes.setHtml(html); - const activeEditor = this._editorService.getActiveEditor(); - if (activeEditor && activeEditor.position !== this._currentReleaseNotes.position) { - this._editorGroupService.moveEditor(this._currentReleaseNotes, this._currentReleaseNotes.position, activeEditor.position, { preserveFocus: true }); - } else { - this._editorService.openEditor(this._currentReleaseNotes, { preserveFocus: true }); - } + this._currentReleaseNotes.html = html; + this._webviewService.revealWebview(this._currentReleaseNotes, activeEditor ? activeEditor.position : undefined); } else { - this._currentReleaseNotes = new WebviewInput(title, { tryRestoreScrollPosition: true, enableFindWidget: true }, html, { - onDidClickLink: uri => this.onDidClickLink(uri), - onDispose: () => { this._currentReleaseNotes = undefined; } - }, this._partService); - await this._editorService.openEditor(this._currentReleaseNotes, { pinned: true }); + this._currentReleaseNotes = this._webviewService.createWebview( + 'releaseNotes', + title, + activeEditor ? activeEditor.position : Position.ONE, + { tryRestoreScrollPosition: true, enableFindWidget: true }, + undefined, { + onDidClickLink: uri => this.onDidClickLink(uri), + onDispose: () => { this._currentReleaseNotes = undefined; } + }); + + this._currentReleaseNotes.html = html; } return true; diff --git a/src/vs/workbench/parts/webview/electron-browser/webview.contribution.ts b/src/vs/workbench/parts/webview/electron-browser/webview.contribution.ts index 4203fa20dffde..2b98fd80e6d5b 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webview.contribution.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webview.contribution.ts @@ -7,11 +7,21 @@ import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } fro import { WebviewEditor } from './webviewEditor'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Registry } from 'vs/platform/registry/common/platform'; -import { WebviewInput } from './webviewInput'; +import { WebviewEditorInput } from './webviewInput'; import { localize } from 'vs/nls'; +import { IEditorInputFactoryRegistry, Extensions as EditorInputExtensions } from 'vs/workbench/common/editor'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IWebviewService, WebviewService } from './webviewService'; +import { WebviewInputFactory } from 'vs/workbench/parts/webview/electron-browser/webviewInputFactory'; (Registry.as(EditorExtensions.Editors)).registerEditor(new EditorDescriptor( WebviewEditor, WebviewEditor.ID, localize('webview.editor.label', "webview editor")), - [new SyncDescriptor(WebviewInput)]); \ No newline at end of file + [new SyncDescriptor(WebviewEditorInput)]); + +Registry.as(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory( + WebviewInputFactory.ID, + WebviewInputFactory); + +registerSingleton(IWebviewService, WebviewService); diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewEditor.ts b/src/vs/workbench/parts/webview/electron-browser/webviewEditor.ts index ddbd2bc080d9c..37c601723af55 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webviewEditor.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webviewEditor.ts @@ -19,7 +19,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import * as DOM from 'vs/base/browser/dom'; import { Event, Emitter } from 'vs/base/common/event'; -import { WebviewInput } from 'vs/workbench/parts/webview/electron-browser/webviewInput'; +import { WebviewEditorInput } from 'vs/workbench/parts/webview/electron-browser/webviewInput'; import URI from 'vs/base/common/uri'; export class WebviewEditor extends BaseWebviewEditor { @@ -29,10 +29,12 @@ export class WebviewEditor extends BaseWebviewEditor { private editorFrame: HTMLElement; private content: HTMLElement; private webviewContent: HTMLElement | undefined; - private readonly _onDidFocusWebview: Emitter; + private _webviewFocusTracker?: DOM.IFocusTracker; private _webviewFocusListenerDisposable?: IDisposable; + private readonly _onDidFocusWebview = new Emitter(); + constructor( @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @@ -43,8 +45,6 @@ export class WebviewEditor extends BaseWebviewEditor { @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService ) { super(WebviewEditor.ID, telemetryService, themeService, _contextKeyService); - - this._onDidFocusWebview = new Emitter(); } protected createEditor(parent: Builder): void { @@ -54,7 +54,7 @@ export class WebviewEditor extends BaseWebviewEditor { } private doUpdateContainer() { - const webviewContainer = this.input && (this.input as WebviewInput).container; + const webviewContainer = this.input && (this.input as WebviewEditorInput).container; if (webviewContainer && webviewContainer.parentElement) { const frameRect = this.editorFrame.getBoundingClientRect(); const containerRect = webviewContainer.parentElement.getBoundingClientRect(); @@ -103,14 +103,14 @@ export class WebviewEditor extends BaseWebviewEditor { } protected setEditorVisible(visible: boolean, position?: Position): void { - if (this.input && this.input instanceof WebviewInput) { + if (this.input && this.input instanceof WebviewEditorInput) { if (visible) { this.input.claimWebview(this); } else { this.input.releaseWebview(this); } - this.updateWebview(this.input as WebviewInput); + this.updateWebview(this.input as WebviewEditorInput); } if (this.webviewContent) { @@ -126,7 +126,7 @@ export class WebviewEditor extends BaseWebviewEditor { } public clearInput() { - if (this.input && this.input instanceof WebviewInput) { + if (this.input && this.input instanceof WebviewEditorInput) { this.input.releaseWebview(this); } @@ -136,24 +136,24 @@ export class WebviewEditor extends BaseWebviewEditor { super.clearInput(); } - async setInput(input: WebviewInput, options: EditorOptions): TPromise { + async setInput(input: WebviewEditorInput, options: EditorOptions): TPromise { if (this.input && this.input.matches(input)) { return undefined; } if (this.input) { - (this.input as WebviewInput).releaseWebview(this); + (this.input as WebviewEditorInput).releaseWebview(this); this._webview = undefined; this.webviewContent = undefined; } await super.setInput(input, options); - input.onDidChangePosition(this.position); + input.onBecameActive(this.position); this.updateWebview(input); } - private updateWebview(input: WebviewInput) { + private updateWebview(input: WebviewEditorInput) { const webview = this.getWebview(input); input.claimWebview(this); webview.options = { @@ -163,7 +163,7 @@ export class WebviewEditor extends BaseWebviewEditor { useSameOriginForRoot: false, localResourceRoots: input.options.localResourceRoots || this.getDefaultLocalResourceRoots() }; - input.setHtml(input.html); + input.html = input.html; if (this.webviewContent) { this.webviewContent.style.visibility = 'visible'; @@ -174,13 +174,13 @@ export class WebviewEditor extends BaseWebviewEditor { private getDefaultLocalResourceRoots(): URI[] { const rootPaths = this._contextService.getWorkspace().folders.map(x => x.uri); - if ((this.input as WebviewInput).extensionFolderPath) { - rootPaths.push((this.input as WebviewInput).extensionFolderPath); + if ((this.input as WebviewEditorInput).extensionFolderPath) { + rootPaths.push((this.input as WebviewEditorInput).extensionFolderPath); } return rootPaths; } - private getWebview(input: WebviewInput): Webview { + private getWebview(input: WebviewEditorInput): Webview { if (this._webview) { return this._webview; } diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewInput.ts b/src/vs/workbench/parts/webview/electron-browser/webviewInput.ts index 0d925f614b739..1bd9535aeaf06 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webviewInput.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webviewInput.ts @@ -5,69 +5,61 @@ 'use strict'; -import { TPromise } from 'vs/base/common/winjs.base'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import URI from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IEditorInput, IEditorModel, Position } from 'vs/platform/editor/common/editor'; import { EditorInput, EditorModel } from 'vs/workbench/common/editor'; -import { IEditorModel, Position, IEditorInput } from 'vs/platform/editor/common/editor'; import { Webview } from 'vs/workbench/parts/html/electron-browser/webview'; import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; -import * as vscode from 'vscode'; -import URI from 'vs/base/common/uri'; +import { WebviewEvents, WebviewInputOptions, WebviewReviver } from './webviewService'; -export interface WebviewEvents { - onMessage?(message: any): void; - onDidChangePosition?(newPosition: Position): void; - onDispose?(): void; - onDidClickLink?(link: URI, options: vscode.WebviewOptions): void; -} -export interface WebviewInputOptions extends vscode.WebviewOptions { - tryRestoreScrollPosition?: boolean; -} - -export class WebviewInput extends EditorInput { +export class WebviewEditorInput extends EditorInput { private static handlePool = 0; + public static readonly typeId = 'workbench.editors.webviewInput'; + private _name: string; private _options: WebviewInputOptions; - private _html: string; + private _html: string = ''; private _currentWebviewHtml: string = ''; - private _events: WebviewEvents | undefined; + public _events: WebviewEvents | undefined; private _container: HTMLElement; private _webview: Webview | undefined; private _webviewOwner: any; private _webviewDisposables: IDisposable[] = []; private _position?: Position; private _scrollYPercentage: number = 0; + private _state: any; + + private _revived: boolean = false; + public readonly extensionFolderPath: URI | undefined; constructor( + public readonly viewType: string, name: string, options: WebviewInputOptions, - html: string, + state: any, events: WebviewEvents, - partService: IPartService, - extensionFolderPath?: string + extensionFolderPath: string | undefined, + public readonly reviver: WebviewReviver | undefined, + @IPartService private readonly _partService: IPartService, ) { super(); this._name = name; this._options = options; - this._html = html; this._events = events; + this._state = state; if (extensionFolderPath) { this.extensionFolderPath = URI.file(extensionFolderPath); } - - const id = WebviewInput.handlePool++; - this._container = document.createElement('div'); - this._container.id = `webview-${id}`; - - partService.getContainer(Parts.EDITOR_PART).appendChild(this._container); } public getTypeId(): string { - return 'webview'; + return WebviewEditorInput.typeId; } public dispose() { @@ -119,7 +111,7 @@ export class WebviewInput extends EditorInput { return this._html; } - public setHtml(value: string): void { + public set html(value: string) { if (value === this._currentWebviewHtml) { return; } @@ -132,6 +124,14 @@ export class WebviewInput extends EditorInput { } } + public get state(): any { + return this._state; + } + + public set state(value: any) { + this._state = value; + } + public get options(): WebviewInputOptions { return this._options; } @@ -149,6 +149,12 @@ export class WebviewInput extends EditorInput { } public get container(): HTMLElement { + if (!this._container) { + const id = WebviewEditorInput.handlePool++; + this._container = document.createElement('div'); + this._container.id = `webview-${id}`; + this._partService.getContainer(Parts.EDITOR_PART).appendChild(this._container); + } return this._container; } @@ -215,10 +221,16 @@ export class WebviewInput extends EditorInput { this._currentWebviewHtml = ''; } - public onDidChangePosition(position: Position) { + public onBecameActive(position: Position) { + this._position = position; + if (this._events && this._events.onDidChangePosition) { this._events.onDidChangePosition(position); } - this._position = position; + + if (this.reviver && !this._revived) { + this._revived = true; + this.reviver.reviveWebview(this); + } } } diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewInputFactory.ts b/src/vs/workbench/parts/webview/electron-browser/webviewInputFactory.ts new file mode 100644 index 0000000000000..824907dca95d5 --- /dev/null +++ b/src/vs/workbench/parts/webview/electron-browser/webviewInputFactory.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IEditorInputFactory } from 'vs/workbench/common/editor'; +import { IWebviewService, WebviewInputOptions } from './webviewService'; +import { WebviewEditorInput } from './webviewInput'; + +interface SerializedWebview { + readonly viewType: string; + readonly title: string; + readonly options: WebviewInputOptions; + readonly extensionFolderPath: string; + readonly state: any; +} + +export class WebviewInputFactory implements IEditorInputFactory { + + public static readonly ID = WebviewEditorInput.typeId; + + public constructor( + @IWebviewService private readonly _webviewService: IWebviewService + ) { } + + public serialize( + input: WebviewEditorInput + ): string { + // Only attempt revival if we may have a reviver + if (!this._webviewService.canRevive(input) && !input.reviver) { + return null; + } + + const data: SerializedWebview = { + viewType: input.viewType, + title: input.getName(), + options: input.options, + extensionFolderPath: input.extensionFolderPath.fsPath, + state: input.state + }; + return JSON.stringify(data); + } + + public deserialize( + instantiationService: IInstantiationService, + serializedEditorInput: string + ): WebviewEditorInput { + const data: SerializedWebview = JSON.parse(serializedEditorInput); + return this._webviewService.createRevivableWebview(data.viewType, data.title, data.state, data.options, data.extensionFolderPath); + } +} diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewService.ts b/src/vs/workbench/parts/webview/electron-browser/webviewService.ts new file mode 100644 index 0000000000000..fc2c3c789506b --- /dev/null +++ b/src/vs/workbench/parts/webview/electron-browser/webviewService.ts @@ -0,0 +1,175 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import URI from 'vs/base/common/uri'; +import { Position } from 'vs/platform/editor/common/editor'; +import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; +import * as vscode from 'vscode'; +import { WebviewEditorInput } from './webviewInput'; + +export const IWebviewService = createDecorator('webviewService'); + +export interface IWebviewService { + _serviceBrand: any; + + createWebview( + viewType: string, + title: string, + column: Position, + options: WebviewInputOptions, + extensionFolderPath: string, + events: WebviewEvents + ): WebviewEditorInput; + + createRevivableWebview( + viewType: string, + title: string, + state: any, + options: WebviewInputOptions, + extensionFolderPath: string + ): WebviewEditorInput; + + revealWebview( + webview: WebviewEditorInput, + column: Position | undefined + ): void; + + registerReviver( + viewType: string, + reviver: WebviewReviver + ): IDisposable; + + canRevive( + input: WebviewEditorInput + ): boolean; +} + +export interface WebviewReviver { + canRevive( + webview: WebviewEditorInput + ): boolean; + + reviveWebview( + webview: WebviewEditorInput + ): boolean; +} + +export interface WebviewEvents { + onMessage?(message: any): void; + onDidChangePosition?(newPosition: Position): void; + onDispose?(): void; + onDidClickLink?(link: URI, options: vscode.WebviewOptions): void; +} + +export interface WebviewInputOptions extends vscode.WebviewOptions { + tryRestoreScrollPosition?: boolean; +} + +export class WebviewService implements IWebviewService { + _serviceBrand: any; + + private readonly _revivers = new Map(); + private readonly _needingRevival = new Map(); + + constructor( + @IWorkbenchEditorService private readonly _editorService: IWorkbenchEditorService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IEditorGroupService private readonly _editorGroupService: IEditorGroupService, + ) { } + + createWebview( + viewType: string, + title: string, + column: Position, + options: vscode.WebviewOptions, + extensionFolderPath: string, + events: WebviewEvents + ): WebviewEditorInput { + const webviewInput = this._instantiationService.createInstance(WebviewEditorInput, viewType, title, options, {}, events, extensionFolderPath, undefined); + this._editorService.openEditor(webviewInput, { pinned: true }, column); + return webviewInput; + } + + revealWebview( + webview: WebviewEditorInput, + column: Position | undefined + ): void { + if (typeof column === 'undefined') { + column = webview.position; + } + + if (webview.position === column) { + this._editorService.openEditor(webview, { preserveFocus: true }, column); + } else { + this._editorGroupService.moveEditor(webview, webview.position, column, { preserveFocus: true }); + } + } + + createRevivableWebview( + viewType: string, + title: string, + state: any, + options: WebviewInputOptions, + extensionFolderPath: string + ): WebviewEditorInput { + const webviewInput = this._instantiationService.createInstance(WebviewEditorInput, viewType, title, options, state, {}, extensionFolderPath, { + canRevive: (webview) => { + return true; + }, + reviveWebview: (webview) => { + if (!this._needingRevival.has(viewType)) { + this._needingRevival.set(viewType, []); + } + this._needingRevival.get(viewType).push(webviewInput); + this.tryRevive(viewType); + } + }); + + return webviewInput; + } + + registerReviver( + viewType: string, + reviver: WebviewReviver + ): IDisposable { + if (this._revivers.has(viewType)) { + throw new Error(`Reveriver for 'viewType' already registered`); + } + + this._revivers.set(viewType, reviver); + this.tryRevive(viewType); + + return toDisposable(() => { + this._revivers.delete(viewType); + }); + } + + canRevive( + webview: WebviewEditorInput + ): boolean { + const viewType = webview.viewType; + return this._revivers.has(viewType) && this._revivers.get(viewType).canRevive(webview); + } + + tryRevive( + viewType: string + ) { + const reviver = this._revivers.get(viewType); + if (!reviver) { + return; + } + + const toRevive = this._needingRevival.get(viewType); + if (!toRevive) { + return; + } + + for (const webview of toRevive) { + reviver.reviveWebview(webview); + } + } +} \ No newline at end of file From e7fc1fe13c7f27c640a55909a937640d21623259 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 22 Mar 2018 19:10:19 -0700 Subject: [PATCH 2/6] reviveWebview return void --- src/vs/workbench/api/electron-browser/mainThreadWebview.ts | 5 +---- .../parts/webview/electron-browser/webviewService.ts | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts index 5348c211a2f54..bb8d2dd4dd1fd 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts @@ -129,7 +129,7 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv this._revivers.delete(viewType); } - reviveWebview(webview: WebviewEditorInput): boolean { + reviveWebview(webview: WebviewEditorInput) { this._extensionService.activateByEvent(`onView:${webview.state.viewType}`).then(() => { const handle = 'revival-' + MainThreadWebviews.revivalPool++; this._webviews.set(handle, webview); @@ -147,8 +147,6 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv this._proxy.$reviveWebview(handle, webview.state.viewType, webview.state.state, webview.position, webview.options); }); - - return true; } canRevive(webview: WebviewEditorInput): boolean { @@ -199,5 +197,4 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv this._openerService.open(link); } } - } diff --git a/src/vs/workbench/parts/webview/electron-browser/webviewService.ts b/src/vs/workbench/parts/webview/electron-browser/webviewService.ts index fc2c3c789506b..e9bead7bb4230 100644 --- a/src/vs/workbench/parts/webview/electron-browser/webviewService.ts +++ b/src/vs/workbench/parts/webview/electron-browser/webviewService.ts @@ -55,7 +55,7 @@ export interface WebviewReviver { reviveWebview( webview: WebviewEditorInput - ): boolean; + ): void; } export interface WebviewEvents { From 685d85a4719276cb200fc441be3ced612f7df7c8 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Fri, 23 Mar 2018 12:47:21 -0700 Subject: [PATCH 3/6] Also restore line in preview --- .../src/features/preview.ts | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/extensions/markdown-language-features/src/features/preview.ts b/extensions/markdown-language-features/src/features/preview.ts index df97c35023ea5..d9d0863a6d333 100644 --- a/extensions/markdown-language-features/src/features/preview.ts +++ b/extensions/markdown-language-features/src/features/preview.ts @@ -22,7 +22,7 @@ export class MarkdownPreview { private readonly webview: vscode.Webview; private throttleTimer: any; - private initialLine: number | undefined = undefined; + private line: number | undefined = undefined; private readonly disposables: vscode.Disposable[] = []; private firstUpdate = true; private currentVersion?: { resource: vscode.Uri, version: number }; @@ -38,8 +38,9 @@ export class MarkdownPreview { ): MarkdownPreview { const resource = vscode.Uri.parse(webview.state.resource); const locked = webview.state.locked; + const line = webview.state.line; - return new MarkdownPreview( + const preview = new MarkdownPreview( webview, resource, locked, @@ -47,6 +48,11 @@ export class MarkdownPreview { previewConfigurations, logger, topmostLineMonitor); + + if (!isNaN(line)) { + preview.line = line; + } + return preview; } public static create( @@ -167,9 +173,7 @@ export class MarkdownPreview { public update(resource: vscode.Uri) { const editor = vscode.window.activeTextEditor; if (editor && editor.document.uri.fsPath === resource.fsPath) { - this.initialLine = getVisibleLine(editor); - } else { - this.initialLine = undefined; + this.line = getVisibleLine(editor); } // If we have changed resources, cancel any pending updates @@ -260,7 +264,7 @@ export class MarkdownPreview { if (typeof topLine === 'number') { this.logger.log('updateForView', { markdownFile: resource }); - this.initialLine = topLine; + this.line = topLine; this.webview.postMessage({ type: 'updateView', line: topLine, @@ -277,15 +281,15 @@ export class MarkdownPreview { const document = await vscode.workspace.openTextDocument(resource); if (!this.forceUpdate && this.currentVersion && this.currentVersion.resource.fsPath === resource.fsPath && this.currentVersion.version === document.version) { - if (this.initialLine) { - this.updateForView(resource, this.initialLine); + if (this.line) { + this.updateForView(resource, this.line); } return; } this.forceUpdate = false; this.currentVersion = { resource, version: document.version }; - this.contentProvider.provideTextDocumentContent(document, this.previewConfigurations, this.initialLine) + this.contentProvider.provideTextDocumentContent(document, this.previewConfigurations, this.line) .then(content => { if (this._resource === resource) { this.webview.title = MarkdownPreview.getPreviewTitle(this._resource, this.locked); @@ -314,6 +318,7 @@ export class MarkdownPreview { } private onDidScrollPreview(line: number) { + this.line = line; for (const editor of vscode.window.visibleTextEditors) { if (!this.isPreviewOf(editor.document.uri)) { continue; @@ -328,6 +333,7 @@ export class MarkdownPreview { new vscode.Range(sourceLine, start, sourceLine + 1, 0), vscode.TextEditorRevealType.AtTop); } + this.updateState(); } private async onDidClickPreview(line: number): Promise { @@ -344,7 +350,8 @@ export class MarkdownPreview { private updateState() { this.webview.state = { resource: this.resource.toString(), - locked: this.locked + locked: this.locked, + line: this.line }; } } From 0bfbbc88858d7d8dc54218864a0d11f67ea525a9 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 2 Apr 2018 17:54:51 -0700 Subject: [PATCH 4/6] Switch to serializer based approach --- .../src/features/preview.ts | 32 ++++++------ .../src/features/previewManager.ts | 17 ++++-- src/vs/vscode.proposed.d.ts | 28 ++++++---- .../api/electron-browser/mainThreadWebview.ts | 52 ++++++++++++++----- src/vs/workbench/api/node/extHost.api.impl.ts | 4 +- src/vs/workbench/api/node/extHost.protocol.ts | 8 +-- src/vs/workbench/api/node/extHostWebview.ts | 47 +++++++++-------- 7 files changed, 117 insertions(+), 71 deletions(-) diff --git a/extensions/markdown-language-features/src/features/preview.ts b/extensions/markdown-language-features/src/features/preview.ts index d9d0863a6d333..5a3ca7443d429 100644 --- a/extensions/markdown-language-features/src/features/preview.ts +++ b/extensions/markdown-language-features/src/features/preview.ts @@ -31,14 +31,15 @@ export class MarkdownPreview { public static revive( webview: vscode.Webview, + state: any, contentProvider: MarkdownContentProvider, previewConfigurations: MarkdownPreviewConfigurationManager, logger: Logger, topmostLineMonitor: MarkdownFileTopmostLineMonitor ): MarkdownPreview { - const resource = vscode.Uri.parse(webview.state.resource); - const locked = webview.state.locked; - const line = webview.state.line; + const resource = vscode.Uri.parse(state.resource); + const locked = state.locked; + const line = state.line; const preview = new MarkdownPreview( webview, @@ -146,8 +147,6 @@ export class MarkdownPreview { }); } }, null, this.disposables); - - this.updateState(); } private readonly _onDisposeEmitter = new vscode.EventEmitter(); @@ -160,6 +159,14 @@ export class MarkdownPreview { return this._resource; } + public get state() { + return { + resource: this.resource.toString(), + locked: this.locked, + line: this.line + }; + } + public dispose() { this._onDisposeEmitter.fire(); @@ -216,6 +223,10 @@ export class MarkdownPreview { return this._resource.fsPath === resource.fsPath; } + public isWebviewOf(webview: vscode.Webview): boolean { + return this.webview === webview; + } + public matchesResource( otherResource: vscode.Uri, otherViewColumn: vscode.ViewColumn | undefined, @@ -243,7 +254,6 @@ export class MarkdownPreview { public toggleLock() { this.locked = !this.locked; this.webview.title = MarkdownPreview.getPreviewTitle(this._resource, this.locked); - this.updateState(); } private static getPreviewTitle(resource: vscode.Uri, locked: boolean): string { @@ -294,7 +304,6 @@ export class MarkdownPreview { if (this._resource === resource) { this.webview.title = MarkdownPreview.getPreviewTitle(this._resource, this.locked); this.webview.html = content; - this.updateState(); } }); } @@ -333,7 +342,6 @@ export class MarkdownPreview { new vscode.Range(sourceLine, start, sourceLine + 1, 0), vscode.TextEditorRevealType.AtTop); } - this.updateState(); } private async onDidClickPreview(line: number): Promise { @@ -346,14 +354,6 @@ export class MarkdownPreview { } } } - - private updateState() { - this.webview.state = { - resource: this.resource.toString(), - locked: this.locked, - line: this.line - }; - } } export interface PreviewSettings { diff --git a/extensions/markdown-language-features/src/features/previewManager.ts b/extensions/markdown-language-features/src/features/previewManager.ts index ce76312999651..b52505f5450eb 100644 --- a/extensions/markdown-language-features/src/features/previewManager.ts +++ b/extensions/markdown-language-features/src/features/previewManager.ts @@ -14,7 +14,7 @@ import { isMarkdownFile } from '../util/file'; import { MarkdownPreviewConfigurationManager } from './previewConfig'; import { MarkdownContributions } from '../markdownExtensions'; -export class MarkdownPreviewManager implements vscode.WebviewReviver { +export class MarkdownPreviewManager implements vscode.WebviewSerializer { private static readonly markdownPreviewActiveContextKey = 'markdownPreviewFocus'; private readonly topmostLineMonitor = new MarkdownFileTopmostLineMonitor(); @@ -36,7 +36,7 @@ export class MarkdownPreviewManager implements vscode.WebviewReviver { } }, null, this.disposables); - this.disposables.push(vscode.window.registerWebviewReviver(MarkdownPreview.viewType, this)); + this.disposables.push(vscode.window.registerWebviewSerializer(MarkdownPreview.viewType, this)); } public dispose(): void { @@ -88,13 +88,15 @@ export class MarkdownPreviewManager implements vscode.WebviewReviver { } } - public reviveWebview( - webview: vscode.Webview + public deserializeWebview( + webview: vscode.Webview, + state: any ): void { console.log('It\'s close to midnight...'); const preview = MarkdownPreview.revive( webview, + state, this.contentProvider, this.previewConfigurations, this.logger, @@ -104,6 +106,13 @@ export class MarkdownPreviewManager implements vscode.WebviewReviver { preview.refresh(); } + public serializeWebview( + webview: vscode.Webview, + ): any { + const preview = this.previews.find(preview => preview.isWebviewOf(webview)); + return preview ? preview.state : {}; + } + private getExistingPreview( resource: vscode.Uri, previewSettings: PreviewSettings diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index bbcc77a82dfeb..35357e5cd08f2 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -589,11 +589,6 @@ declare module 'vscode' { */ html: string; - /** - * JSON serializable blob of data saved on webviews for revival. - */ - state: any; - /** * The column in which the webview is showing. */ @@ -644,15 +639,28 @@ declare module 'vscode' { /** * Restores webviews that have been persisted when vscode shuts down. */ - interface WebviewReviver { + interface WebviewSerializer { + /** + * Save a webview's `state`. + * + * Called when a webview is about to be hidden + * + * @param webview Webview to revive. + * + * @returns state JSON serializable blob. + */ + serializeWebview(webview: Webview): any; + + /** * Restore a webview's `html` from its `state`. * * Called when a serialized webview first becomes active. * * @param webview Webview to revive. + * @param state Persisted state. */ - reviveWebview(webview: Webview): void; + deserializeWebview(webview: Webview, state: any): void; } namespace window { @@ -667,17 +675,17 @@ declare module 'vscode' { export function createWebview(viewType: string, title: string, column: ViewColumn, options: WebviewOptions): Webview; /** - * Registers a webview reviver. + * Registers a webview serializer. * * Extensions that support reviving should have an `"onView:viewType"` activation method and - * make sure that `registerWebviewReviver` is called during activation. + * make sure that `registerWebviewSerializer` is called during activation. * * Only a single reviver may be registered at a time for a given `viewType`. * * @param viewType Type of the webview that can be revived. * @param reviver Webview revivier. */ - export function registerWebviewReviver(viewType: string, reviver: WebviewReviver): Disposable; + export function registerWebviewSerializer(viewType: string, reviver: WebviewSerializer): Disposable; } //#endregion diff --git a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts index bb8d2dd4dd1fd..ecaaa5442b99b 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts @@ -5,7 +5,7 @@ import * as map from 'vs/base/common/map'; import { MainThreadWebviewsShape, MainContext, IExtHostContext, ExtHostContext, ExtHostWebviewsShape, WebviewHandle } from 'vs/workbench/api/node/extHost.protocol'; -import { dispose, Disposable } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { extHostNamedCustomer } from './extHostCustomers'; import { Position } from 'vs/platform/editor/common/editor'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -18,6 +18,8 @@ import { IEditorGroupService } from '../../services/group/common/groupService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import URI from 'vs/base/common/uri'; import { IExtensionService } from '../../services/extensions/common/extensions'; +import { ILifecycleService } from '../../../platform/lifecycle/common/lifecycle'; +import { TPromise } from '../../../base/common/winjs.base'; @extHostNamedCustomer(MainContext.MainThreadWebviews) export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviver { @@ -28,7 +30,7 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv private static revivalPool = 0; - private _toDispose: Disposable[] = []; + private _toDispose: IDisposable[] = []; private readonly _proxy: ExtHostWebviewsShape; private readonly _webviews = new Map(); @@ -38,17 +40,22 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv constructor( context: IExtHostContext, - @IContextKeyService _contextKeyService: IContextKeyService, - @IEditorGroupService _editorGroupService: IEditorGroupService, + @IContextKeyService contextKeyService: IContextKeyService, + @IEditorGroupService editorGroupService: IEditorGroupService, + @ILifecycleService lifecycleService: ILifecycleService, @IWorkbenchEditorService private readonly _editorService: IWorkbenchEditorService, @IWebviewService private readonly _webviewService: IWebviewService, @IOpenerService private readonly _openerService: IOpenerService, - @IExtensionService private readonly _extensionService: IExtensionService + @IExtensionService private readonly _extensionService: IExtensionService, + ) { this._proxy = context.getProxy(ExtHostContext.ExtHostWebviews); - _editorGroupService.onEditorsChanged(this.onEditorsChanged, this, this._toDispose); + editorGroupService.onEditorsChanged(this.onEditorsChanged, this, this._toDispose); _webviewService.registerReviver(MainThreadWebviews.viewType, this); + this._toDispose.push(lifecycleService.onWillShutdown(e => { + e.veto(this._onWillShutdown()); + })); } dispose(): void { @@ -97,11 +104,6 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv webview.html = value; } - $setState(handle: WebviewHandle, value: string): void { - const webview = this.getWebview(handle); - webview.state.state = value; - } - $reveal(handle: WebviewHandle, column: Position): void { const webview = this.getWebview(handle); this._webviewService.revealWebview(webview, column); @@ -121,11 +123,11 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv return (editors.length > 0); } - $registerReviver(viewType: string): void { + $registerSerializer(viewType: string): void { this._revivers.add(viewType); } - $unregisterReviver(viewType: string): void { + $unregisterSerializer(viewType: string): void { this._revivers.delete(viewType); } @@ -145,7 +147,7 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv } }; - this._proxy.$reviveWebview(handle, webview.state.viewType, webview.state.state, webview.position, webview.options); + this._proxy.$deserializeWebview(handle, webview.state.viewType, webview.state.state, webview.position, webview.options); }); } @@ -153,6 +155,28 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv return this._revivers.has(webview.viewType) || webview.reviver !== null; } + private _onWillShutdown(): TPromise { + const toRevive: WebviewHandle[] = []; + this._webviews.forEach((view, key) => { + if (this.canRevive(view)) { + toRevive.push(key); + } + }); + + const reviveResponses = toRevive.map(handle => + this._proxy.$serializeWebview(handle).then(state => ({ handle, state }))); + + return TPromise.join(reviveResponses).then(results => { + for (const result of results) { + const view = this._webviews.get(result.handle); + if (view) { + view.state.state = result.state; + } + } + return false; // Don't veto shutdown + }); + } + private getWebview(handle: WebviewHandle): WebviewEditorInput { const webview = this._webviews.get(handle); if (!webview) { diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 8f9accef0d195..9f4826a9165d7 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -419,8 +419,8 @@ export function createApiFactory( createWebview: proposedApiFunction(extension, (viewType: string, title: string, column: vscode.ViewColumn, options: vscode.WebviewOptions) => { return extHostWebviews.createWebview(viewType, title, column, options, extension.extensionFolderPath); }), - registerWebviewReviver: proposedApiFunction(extension, (viewType: string, reviver: vscode.WebviewReviver) => { - return extHostWebviews.registerWebviewReviver(viewType, reviver); + registerWebviewSerializer: proposedApiFunction(extension, (viewType: string, reviver: vscode.WebviewSerializer) => { + return extHostWebviews.registerWebviewSerializer(viewType, reviver); }) }; diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 31857a9c07cb3..0c3c497157ac1 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -355,11 +355,10 @@ export interface MainThreadWebviewsShape extends IDisposable { $reveal(handle: WebviewHandle, column: EditorPosition): void; $setTitle(handle: WebviewHandle, value: string): void; $setHtml(handle: WebviewHandle, value: string): void; - $setState(handle: WebviewHandle, value: any): void; $sendMessage(handle: WebviewHandle, value: any): Thenable; - $registerReviver(viewType: string): void; - $unregisterReviver(viewType: string): void; + $registerSerializer(viewType: string): void; + $unregisterSerializer(viewType: string): void; } export interface ExtHostWebviewsShape { @@ -367,7 +366,8 @@ export interface ExtHostWebviewsShape { $onDidChangeActiveWeview(handle: WebviewHandle | undefined): void; $onDidDisposeWeview(handle: WebviewHandle): Thenable; $onDidChangePosition(handle: WebviewHandle, newPosition: EditorPosition): void; - $reviveWebview(newWebviewHandle: WebviewHandle, viewType: string, state: any, position: EditorPosition, options: vscode.WebviewOptions): void; + $deserializeWebview(newWebviewHandle: WebviewHandle, viewType: string, state: any, position: EditorPosition, options: vscode.WebviewOptions): void; + $serializeWebview(webviewHandle: WebviewHandle): Thenable; } export interface MainThreadWorkspaceShape extends IDisposable { diff --git a/src/vs/workbench/api/node/extHostWebview.ts b/src/vs/workbench/api/node/extHostWebview.ts index 0e2dc76a39d69..90905ce01f522 100644 --- a/src/vs/workbench/api/node/extHostWebview.ts +++ b/src/vs/workbench/api/node/extHostWebview.ts @@ -92,12 +92,6 @@ export class ExtHostWebview implements vscode.Webview { return this._state; } - set state(value: any) { - this.assertNotDisposed(); - this._state = value; - this._proxy.$setState(this._handle, value); - } - get options(): vscode.WebviewOptions { this.assertNotDisposed(); return this._options; @@ -146,7 +140,7 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { private readonly _proxy: MainThreadWebviewsShape; private readonly _webviews = new Map(); - private readonly _revivers = new Map(); + private readonly _serializers = new Map(); private _activeWebview: ExtHostWebview | undefined; @@ -171,20 +165,20 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { return webview; } - registerWebviewReviver( + registerWebviewSerializer( viewType: string, - reviver: vscode.WebviewReviver + reviver: vscode.WebviewSerializer ): vscode.Disposable { - if (this._revivers.has(viewType)) { - throw new Error(`Reviver for '${viewType}' already registered`); + if (this._serializers.has(viewType)) { + throw new Error(`Serializer for '${viewType}' already registered`); } - this._revivers.set(viewType, reviver); - this._proxy.$registerReviver(viewType); + this._serializers.set(viewType, reviver); + this._proxy.$registerSerializer(viewType); return new Disposable(() => { - this._revivers.delete(viewType); - this._proxy.$unregisterReviver(viewType); + this._serializers.delete(viewType); + this._proxy.$unregisterSerializer(viewType); }); } @@ -237,23 +231,34 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { } } - $reviveWebview( + $deserializeWebview( webviewHandle: WebviewHandle, viewType: string, state: any, position: Position, options: vscode.WebviewOptions ): void { - const reviver = this._revivers.get(viewType); + const reviver = this._serializers.get(viewType); if (!reviver) { return; } - const webview = new ExtHostWebview(webviewHandle, this._proxy, viewType, typeConverters.toViewColumn(position), options); - webview.state = state; + const revivedWebview = new ExtHostWebview(webviewHandle, this._proxy, viewType, typeConverters.toViewColumn(position), options); + this._webviews.set(webviewHandle, revivedWebview); + reviver.deserializeWebview(revivedWebview, state); + } + + $serializeWebview( + webviewHandle: WebviewHandle + ): any { + const webview = this.getWebview(webviewHandle); + + const reviver = this._serializers.get(webview.viewType); + if (!reviver) { + return {}; + } - this._webviews.set(webviewHandle, webview); - reviver.reviveWebview(webview); + return reviver.serializeWebview(webview); } private getWebview(handle: WebviewHandle) { From efc7c2fdc2db413735cc180f7dfc26bd6e1176dc Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 2 Apr 2018 18:09:16 -0700 Subject: [PATCH 5/6] Cleanup --- src/vs/vscode.proposed.d.ts | 19 ++++++------ .../api/electron-browser/mainThreadWebview.ts | 30 +++++++++---------- src/vs/workbench/api/node/extHost.api.impl.ts | 4 +-- src/vs/workbench/api/node/extHostWebview.ts | 16 +++++----- 4 files changed, 33 insertions(+), 36 deletions(-) diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 35357e5cd08f2..a8a76fdd4d460 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -637,27 +637,26 @@ declare module 'vscode' { } /** - * Restores webviews that have been persisted when vscode shuts down. + * Save and restore webviews that have been persisted when vscode shuts down. */ interface WebviewSerializer { /** * Save a webview's `state`. * - * Called when a webview is about to be hidden + * Called before shutdown. Webview may or may not be visible. * - * @param webview Webview to revive. + * @param webview Webview to serialize. * - * @returns state JSON serializable blob. + * @returns JSON serializable state blob. */ serializeWebview(webview: Webview): any; - /** - * Restore a webview's `html` from its `state`. + * Restore a webview from its `state`. * * Called when a serialized webview first becomes active. * - * @param webview Webview to revive. + * @param webview Webview to restore. The serializer should take ownership of this webview. * @param state Persisted state. */ deserializeWebview(webview: Webview, state: any): void; @@ -680,10 +679,10 @@ declare module 'vscode' { * Extensions that support reviving should have an `"onView:viewType"` activation method and * make sure that `registerWebviewSerializer` is called during activation. * - * Only a single reviver may be registered at a time for a given `viewType`. + * Only a single serializer may be registered at a time for a given `viewType`. * - * @param viewType Type of the webview that can be revived. - * @param reviver Webview revivier. + * @param viewType Type of the webview that can be serialized. + * @param reviver Webview serializer. */ export function registerWebviewSerializer(viewType: string, reviver: WebviewSerializer): Disposable; } diff --git a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts index ecaaa5442b99b..4e1f2d6c6ec5c 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts @@ -2,24 +2,22 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import * as map from 'vs/base/common/map'; -import { MainThreadWebviewsShape, MainContext, IExtHostContext, ExtHostContext, ExtHostWebviewsShape, WebviewHandle } from 'vs/workbench/api/node/extHost.protocol'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { extHostNamedCustomer } from './extHostCustomers'; -import { Position } from 'vs/platform/editor/common/editor'; -import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import URI from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import * as vscode from 'vscode'; -import { WebviewEditorInput } from 'vs/workbench/parts/webview/electron-browser/webviewInput'; -import { WebviewEditor } from 'vs/workbench/parts/webview/electron-browser/webviewEditor'; -import { IWebviewService, WebviewReviver } from 'vs/workbench/parts/webview/electron-browser/webviewService'; -import { IEditorGroupService } from '../../services/group/common/groupService'; +import { Position } from 'vs/platform/editor/common/editor'; +import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import URI from 'vs/base/common/uri'; -import { IExtensionService } from '../../services/extensions/common/extensions'; -import { ILifecycleService } from '../../../platform/lifecycle/common/lifecycle'; -import { TPromise } from '../../../base/common/winjs.base'; +import { ExtHostContext, ExtHostWebviewsShape, IExtHostContext, MainContext, MainThreadWebviewsShape, WebviewHandle } from 'vs/workbench/api/node/extHost.protocol'; +import { WebviewEditor } from 'vs/workbench/parts/webview/electron-browser/webviewEditor'; +import { WebviewEditorInput } from 'vs/workbench/parts/webview/electron-browser/webviewInput'; +import { IWebviewService, WebviewInputOptions, WebviewReviver } from 'vs/workbench/parts/webview/electron-browser/webviewService'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; +import { extHostNamedCustomer } from './extHostCustomers'; @extHostNamedCustomer(MainContext.MainThreadWebviews) export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviver { @@ -67,7 +65,7 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv viewType: string, title: string, column: Position, - options: vscode.WebviewOptions, + options: WebviewInputOptions, extensionFolderPath: string ): void { const webview = this._webviewService.createWebview(MainThreadWebviews.viewType, title, column, options, extensionFolderPath, { diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 9f4826a9165d7..6757325af7650 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -419,8 +419,8 @@ export function createApiFactory( createWebview: proposedApiFunction(extension, (viewType: string, title: string, column: vscode.ViewColumn, options: vscode.WebviewOptions) => { return extHostWebviews.createWebview(viewType, title, column, options, extension.extensionFolderPath); }), - registerWebviewSerializer: proposedApiFunction(extension, (viewType: string, reviver: vscode.WebviewSerializer) => { - return extHostWebviews.registerWebviewSerializer(viewType, reviver); + registerWebviewSerializer: proposedApiFunction(extension, (viewType: string, serializer: vscode.WebviewSerializer) => { + return extHostWebviews.registerWebviewSerializer(viewType, serializer); }) }; diff --git a/src/vs/workbench/api/node/extHostWebview.ts b/src/vs/workbench/api/node/extHostWebview.ts index 90905ce01f522..ec7fafa6aa873 100644 --- a/src/vs/workbench/api/node/extHostWebview.ts +++ b/src/vs/workbench/api/node/extHostWebview.ts @@ -167,13 +167,13 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { registerWebviewSerializer( viewType: string, - reviver: vscode.WebviewSerializer + serializer: vscode.WebviewSerializer ): vscode.Disposable { if (this._serializers.has(viewType)) { throw new Error(`Serializer for '${viewType}' already registered`); } - this._serializers.set(viewType, reviver); + this._serializers.set(viewType, serializer); this._proxy.$registerSerializer(viewType); return new Disposable(() => { @@ -238,14 +238,14 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { position: Position, options: vscode.WebviewOptions ): void { - const reviver = this._serializers.get(viewType); - if (!reviver) { + const serializer = this._serializers.get(viewType); + if (!serializer) { return; } const revivedWebview = new ExtHostWebview(webviewHandle, this._proxy, viewType, typeConverters.toViewColumn(position), options); this._webviews.set(webviewHandle, revivedWebview); - reviver.deserializeWebview(revivedWebview, state); + serializer.deserializeWebview(revivedWebview, state); } $serializeWebview( @@ -253,12 +253,12 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { ): any { const webview = this.getWebview(webviewHandle); - const reviver = this._serializers.get(webview.viewType); - if (!reviver) { + const serialzer = this._serializers.get(webview.viewType); + if (!serialzer) { return {}; } - return reviver.serializeWebview(webview); + return serialzer.serializeWebview(webview); } private getWebview(handle: WebviewHandle) { From d7c56bf046ad292a973e0adbc3c367352d32038f Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 3 Apr 2018 18:17:24 -0700 Subject: [PATCH 6/6] Make async --- .../src/features/previewManager.ts | 13 ++++++------- src/vs/vscode.proposed.d.ts | 6 ++++-- .../api/electron-browser/mainThreadWebview.ts | 10 ++++++---- src/vs/workbench/api/node/extHostWebview.ts | 4 ++-- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/extensions/markdown-language-features/src/features/previewManager.ts b/extensions/markdown-language-features/src/features/previewManager.ts index b52505f5450eb..d9a2752edd399 100644 --- a/extensions/markdown-language-features/src/features/previewManager.ts +++ b/extensions/markdown-language-features/src/features/previewManager.ts @@ -88,12 +88,10 @@ export class MarkdownPreviewManager implements vscode.WebviewSerializer { } } - public deserializeWebview( + public async deserializeWebview( webview: vscode.Webview, state: any - ): void { - console.log('It\'s close to midnight...'); - + ): Promise { const preview = MarkdownPreview.revive( webview, state, @@ -104,13 +102,14 @@ export class MarkdownPreviewManager implements vscode.WebviewSerializer { this.registerPreview(preview); preview.refresh(); + return true; } - public serializeWebview( + public async serializeWebview( webview: vscode.Webview, - ): any { + ): Promise { const preview = this.previews.find(preview => preview.isWebviewOf(webview)); - return preview ? preview.state : {}; + return preview ? preview.state : undefined; } private getExistingPreview( diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index a8a76fdd4d460..33c125d846709 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -649,7 +649,7 @@ declare module 'vscode' { * * @returns JSON serializable state blob. */ - serializeWebview(webview: Webview): any; + serializeWebview(webview: Webview): Thenable; /** * Restore a webview from its `state`. @@ -658,8 +658,10 @@ declare module 'vscode' { * * @param webview Webview to restore. The serializer should take ownership of this webview. * @param state Persisted state. + * + * @return Was deserialization successful? */ - deserializeWebview(webview: Webview, state: any): void; + deserializeWebview(webview: Webview, state: any): Thenable; } namespace window { diff --git a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts index 4e1f2d6c6ec5c..323f9e9db0eba 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts @@ -166,9 +166,11 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv return TPromise.join(reviveResponses).then(results => { for (const result of results) { - const view = this._webviews.get(result.handle); - if (view) { - view.state.state = result.state; + if (result.state) { + const view = this._webviews.get(result.handle); + if (view) { + view.state.state = result.state; + } } } return false; // Don't veto shutdown @@ -209,7 +211,7 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv } } - private onDidClickLink(link: URI, options: vscode.WebviewOptions): void { + private onDidClickLink(link: URI, options: WebviewInputOptions): void { if (!link) { return; } diff --git a/src/vs/workbench/api/node/extHostWebview.ts b/src/vs/workbench/api/node/extHostWebview.ts index ec7fafa6aa873..5e3a1b3812680 100644 --- a/src/vs/workbench/api/node/extHostWebview.ts +++ b/src/vs/workbench/api/node/extHostWebview.ts @@ -250,12 +250,12 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { $serializeWebview( webviewHandle: WebviewHandle - ): any { + ): Thenable { const webview = this.getWebview(webviewHandle); const serialzer = this._serializers.get(webview.viewType); if (!serialzer) { - return {}; + return TPromise.as(undefined); } return serialzer.serializeWebview(webview);