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..5a3ca7443d429 100644 --- a/extensions/markdown-language-features/src/features/preview.ts +++ b/extensions/markdown-language-features/src/features/preview.ts @@ -18,37 +18,85 @@ 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; - 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 }; private forceUpdate = false; private isScrolling = false; - constructor( - private _resource: vscode.Uri, + public static revive( + webview: vscode.Webview, + state: any, + contentProvider: MarkdownContentProvider, + previewConfigurations: MarkdownPreviewConfigurationManager, + logger: Logger, + topmostLineMonitor: MarkdownFileTopmostLineMonitor + ): MarkdownPreview { + const resource = vscode.Uri.parse(state.resource); + const locked = state.locked; + const line = state.line; + + const preview = new MarkdownPreview( + webview, + resource, + locked, + contentProvider, + previewConfigurations, + logger, + topmostLineMonitor); + + if (!isNaN(line)) { + preview.line = line; + } + return preview; + } + + 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); @@ -111,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(); @@ -124,9 +180,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 @@ -169,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, @@ -195,11 +253,11 @@ 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); } - 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)); } @@ -216,7 +274,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, @@ -233,25 +291,28 @@ 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 = this.getPreviewTitle(this._resource); + this.webview.title = MarkdownPreview.getPreviewTitle(this._resource, this.locked); this.webview.html = content; } }); } - 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) { @@ -266,6 +327,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; diff --git a/extensions/markdown-language-features/src/features/previewManager.ts b/extensions/markdown-language-features/src/features/previewManager.ts index 61fee517b43ad..d9a2752edd399 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.WebviewSerializer { 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.registerWebviewSerializer(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,30 @@ export class MarkdownPreviewManager { } } + public async deserializeWebview( + webview: vscode.Webview, + state: any + ): Promise { + const preview = MarkdownPreview.revive( + webview, + state, + this.contentProvider, + this.previewConfigurations, + this.logger, + this.topmostLineMonitor); + + this.registerPreview(preview); + preview.refresh(); + return true; + } + + public async serializeWebview( + webview: vscode.Webview, + ): Promise { + const preview = this.previews.find(preview => preview.isWebviewOf(webview)); + return preview ? preview.state : undefined; + } + private getExistingPreview( resource: vscode.Uri, previewSettings: PreviewSettings @@ -101,8 +123,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 +134,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..33c125d846709 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; @@ -636,16 +636,57 @@ declare module 'vscode' { dispose(): any; } + /** + * Save and restore webviews that have been persisted when vscode shuts down. + */ + interface WebviewSerializer { + /** + * Save a webview's `state`. + * + * Called before shutdown. Webview may or may not be visible. + * + * @param webview Webview to serialize. + * + * @returns JSON serializable state blob. + */ + serializeWebview(webview: Webview): Thenable; + + /** + * Restore a webview from its `state`. + * + * Called when a serialized webview first becomes active. + * + * @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): Thenable; + } + 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 serializer. + * + * Extensions that support reviving should have an `"onView:viewType"` activation method and + * make sure that `registerWebviewSerializer` is called during activation. + * + * Only a single serializer may be registered at a time for a given `viewType`. + * + * @param viewType Type of the webview that can be serialized. + * @param reviver Webview serializer. + */ + 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 aba0f0c1621d9..323f9e9db0eba 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts @@ -2,44 +2,58 @@ * 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, Disposable } 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 { IPartService } from 'vs/workbench/services/part/common/partService'; +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 * 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 { 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 { +export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviver { + + private static readonly viewType = 'mainThreadWebview'; + private static readonly standardSupportedLinkSchemes = ['http', 'https', 'mailto']; - private _toDispose: Disposable[] = []; + private static revivalPool = 0; + + private _toDispose: IDisposable[] = []; 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, + @IContextKeyService contextKeyService: IContextKeyService, + @IEditorGroupService editorGroupService: IEditorGroupService, + @ILifecycleService lifecycleService: ILifecycleService, @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); + editorGroupService.onEditorsChanged(this.onEditorsChanged, this, this._toDispose); + + _webviewService.registerReviver(MainThreadWebviews.viewType, this); + this._toDispose.push(lifecycleService.onWillShutdown(e => { + e.veto(this._onWillShutdown()); + })); } dispose(): void { @@ -51,30 +65,31 @@ export class MainThreadWebviews implements MainThreadWebviewsShape { viewType: string, title: string, column: Position, - options: vscode.WebviewOptions, + options: WebviewInputOptions, 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 +99,20 @@ export class MainThreadWebviews implements MainThreadWebviewsShape { $setHtml(handle: WebviewHandle, value: string): void { const webview = this.getWebview(handle); - webview.setHtml(value); + webview.html = 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,74 @@ export class MainThreadWebviews implements MainThreadWebviewsShape { return (editors.length > 0); } - private getWebview(handle: number): WebviewInput { - const webviewInput = this._webviews.get(handle); - if (!webviewInput) { + $registerSerializer(viewType: string): void { + this._revivers.add(viewType); + } + + $unregisterSerializer(viewType: string): void { + this._revivers.delete(viewType); + } + + reviveWebview(webview: WebviewEditorInput) { + 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.$deserializeWebview(handle, webview.state.viewType, webview.state.state, webview.position, webview.options); + }); + } + + canRevive(webview: WebviewEditorInput): boolean { + 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) { + if (result.state) { + 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) { 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 +199,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; } @@ -144,7 +211,7 @@ export class MainThreadWebviews implements MainThreadWebviewsShape { } } - 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/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index de10ae5c3cfd5..6757325af7650 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); + }), + registerWebviewSerializer: proposedApiFunction(extension, (viewType: string, serializer: vscode.WebviewSerializer) => { + return extHostWebviews.registerWebviewSerializer(viewType, serializer); }) }; diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 1a1336e336739..0c3c497157ac1 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; @@ -356,12 +356,18 @@ export interface MainThreadWebviewsShape extends IDisposable { $setTitle(handle: WebviewHandle, value: string): void; $setHtml(handle: WebviewHandle, value: string): void; $sendMessage(handle: WebviewHandle, value: any): Thenable; + + $registerSerializer(viewType: string): void; + $unregisterSerializer(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; + $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 27f69d0ec1dc8..5e3a1b3812680 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,11 @@ export class ExtHostWebview implements vscode.Webview { } } + get state(): any { + this.assertNotDisposed(); + return this._state; + } + get options(): vscode.WebviewOptions { this.assertNotDisposed(); return this._options; @@ -128,11 +135,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 _serializers = new Map(); private _activeWebview: ExtHostWebview | undefined; @@ -149,7 +157,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 +165,23 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { return webview; } + registerWebviewSerializer( + viewType: string, + serializer: vscode.WebviewSerializer + ): vscode.Disposable { + if (this._serializers.has(viewType)) { + throw new Error(`Serializer for '${viewType}' already registered`); + } + + this._serializers.set(viewType, serializer); + this._proxy.$registerSerializer(viewType); + + return new Disposable(() => { + this._serializers.delete(viewType); + this._proxy.$unregisterSerializer(viewType); + }); + } + $onMessage(handle: WebviewHandle, message: any): void { const webview = this.getWebview(handle); if (webview) { @@ -206,8 +231,35 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { } } - private readonly _onDidChangeActiveWebview = new Emitter(); - public readonly onDidChangeActiveWebview = this._onDidChangeActiveWebview.event; + $deserializeWebview( + webviewHandle: WebviewHandle, + viewType: string, + state: any, + position: Position, + options: vscode.WebviewOptions + ): void { + 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); + serializer.deserializeWebview(revivedWebview, state); + } + + $serializeWebview( + webviewHandle: WebviewHandle + ): Thenable { + const webview = this.getWebview(webviewHandle); + + const serialzer = this._serializers.get(webview.viewType); + if (!serialzer) { + return TPromise.as(undefined); + } + + return serialzer.serializeWebview(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..e9bead7bb4230 --- /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 + ): void; +} + +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