Add webview restoration api proposal (#46380)

Adds a proposed webiew serialization api that allows webviews to be restored automatically when vscode restarts
This commit is contained in:
Matt Bierner
2018-04-03 18:25:22 -07:00
committed by GitHub
parent c7b37f5915
commit dd21d3520a
14 changed files with 690 additions and 176 deletions

View File

@@ -22,7 +22,8 @@
"onCommand:markdown.showPreviewToSide", "onCommand:markdown.showPreviewToSide",
"onCommand:markdown.showLockedPreviewToSide", "onCommand:markdown.showLockedPreviewToSide",
"onCommand:markdown.showSource", "onCommand:markdown.showSource",
"onCommand:markdown.showPreviewSecuritySelector" "onCommand:markdown.showPreviewSecuritySelector",
"onView:markdown.preview"
], ],
"contributes": { "contributes": {
"commands": [ "commands": [

View File

@@ -18,37 +18,85 @@ const localize = nls.loadMessageBundle();
export class MarkdownPreview { export class MarkdownPreview {
public static previewViewType = 'markdown.preview'; public static viewType = 'markdown.preview';
private readonly webview: vscode.Webview; private readonly webview: vscode.Webview;
private throttleTimer: any; private throttleTimer: any;
private initialLine: number | undefined = undefined; private line: number | undefined = undefined;
private readonly disposables: vscode.Disposable[] = []; private readonly disposables: vscode.Disposable[] = [];
private firstUpdate = true; private firstUpdate = true;
private currentVersion?: { resource: vscode.Uri, version: number }; private currentVersion?: { resource: vscode.Uri, version: number };
private forceUpdate = false; private forceUpdate = false;
private isScrolling = false; private isScrolling = false;
constructor( public static revive(
private _resource: vscode.Uri, 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, previewColumn: vscode.ViewColumn,
public locked: boolean, locked: boolean,
private readonly contentProvider: MarkdownContentProvider, contentProvider: MarkdownContentProvider,
private readonly previewConfigurations: MarkdownPreviewConfigurationManager, previewConfigurations: MarkdownPreviewConfigurationManager,
private readonly logger: Logger, logger: Logger,
topmostLineMonitor: MarkdownFileTopmostLineMonitor, topmostLineMonitor: MarkdownFileTopmostLineMonitor,
private readonly contributions: MarkdownContributions contributions: MarkdownContributions
) { ): MarkdownPreview {
this.webview = vscode.window.createWebview( const webview = vscode.window.createWebview(
MarkdownPreview.previewViewType, MarkdownPreview.viewType,
this.getPreviewTitle(this._resource), MarkdownPreview.getPreviewTitle(resource, locked),
previewColumn, { previewColumn, {
enableScripts: true, enableScripts: true,
enableCommandUris: true, enableCommandUris: true,
enableFindWidget: 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.webview.onDidDispose(() => {
this.dispose(); this.dispose();
}, null, this.disposables); }, null, this.disposables);
@@ -111,6 +159,14 @@ export class MarkdownPreview {
return this._resource; return this._resource;
} }
public get state() {
return {
resource: this.resource.toString(),
locked: this.locked,
line: this.line
};
}
public dispose() { public dispose() {
this._onDisposeEmitter.fire(); this._onDisposeEmitter.fire();
@@ -124,9 +180,7 @@ export class MarkdownPreview {
public update(resource: vscode.Uri) { public update(resource: vscode.Uri) {
const editor = vscode.window.activeTextEditor; const editor = vscode.window.activeTextEditor;
if (editor && editor.document.uri.fsPath === resource.fsPath) { if (editor && editor.document.uri.fsPath === resource.fsPath) {
this.initialLine = getVisibleLine(editor); this.line = getVisibleLine(editor);
} else {
this.initialLine = undefined;
} }
// If we have changed resources, cancel any pending updates // If we have changed resources, cancel any pending updates
@@ -169,6 +223,10 @@ export class MarkdownPreview {
return this._resource.fsPath === resource.fsPath; return this._resource.fsPath === resource.fsPath;
} }
public isWebviewOf(webview: vscode.Webview): boolean {
return this.webview === webview;
}
public matchesResource( public matchesResource(
otherResource: vscode.Uri, otherResource: vscode.Uri,
otherViewColumn: vscode.ViewColumn | undefined, otherViewColumn: vscode.ViewColumn | undefined,
@@ -195,11 +253,11 @@ export class MarkdownPreview {
public toggleLock() { public toggleLock() {
this.locked = !this.locked; 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 { private static getPreviewTitle(resource: vscode.Uri, locked: boolean): string {
return this.locked return locked
? localize('lockedPreviewTitle', '[Preview] {0}', path.basename(resource.fsPath)) ? localize('lockedPreviewTitle', '[Preview] {0}', path.basename(resource.fsPath))
: localize('previewTitle', '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') { if (typeof topLine === 'number') {
this.logger.log('updateForView', { markdownFile: resource }); this.logger.log('updateForView', { markdownFile: resource });
this.initialLine = topLine; this.line = topLine;
this.webview.postMessage({ this.webview.postMessage({
type: 'updateView', type: 'updateView',
line: topLine, line: topLine,
@@ -233,25 +291,28 @@ export class MarkdownPreview {
const document = await vscode.workspace.openTextDocument(resource); 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.forceUpdate && this.currentVersion && this.currentVersion.resource.fsPath === resource.fsPath && this.currentVersion.version === document.version) {
if (this.initialLine) { if (this.line) {
this.updateForView(resource, this.initialLine); this.updateForView(resource, this.line);
} }
return; return;
} }
this.forceUpdate = false; this.forceUpdate = false;
this.currentVersion = { resource, version: document.version }; this.currentVersion = { resource, version: document.version };
this.contentProvider.provideTextDocumentContent(document, this.previewConfigurations, this.initialLine) this.contentProvider.provideTextDocumentContent(document, this.previewConfigurations, this.line)
.then(content => { .then(content => {
if (this._resource === resource) { 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.webview.html = content;
} }
}); });
} }
private getLocalResourceRoots(resource: vscode.Uri): vscode.Uri[] { private static getLocalResourceRoots(
const baseRoots = this.contributions.previewResourceRoots; resource: vscode.Uri,
contributions: MarkdownContributions
): vscode.Uri[] {
const baseRoots = contributions.previewResourceRoots;
const folder = vscode.workspace.getWorkspaceFolder(resource); const folder = vscode.workspace.getWorkspaceFolder(resource);
if (folder) { if (folder) {
@@ -266,6 +327,7 @@ export class MarkdownPreview {
} }
private onDidScrollPreview(line: number) { private onDidScrollPreview(line: number) {
this.line = line;
for (const editor of vscode.window.visibleTextEditors) { for (const editor of vscode.window.visibleTextEditors) {
if (!this.isPreviewOf(editor.document.uri)) { if (!this.isPreviewOf(editor.document.uri)) {
continue; continue;

View File

@@ -14,7 +14,7 @@ import { isMarkdownFile } from '../util/file';
import { MarkdownPreviewConfigurationManager } from './previewConfig'; import { MarkdownPreviewConfigurationManager } from './previewConfig';
import { MarkdownContributions } from '../markdownExtensions'; import { MarkdownContributions } from '../markdownExtensions';
export class MarkdownPreviewManager { export class MarkdownPreviewManager implements vscode.WebviewSerializer {
private static readonly markdownPreviewActiveContextKey = 'markdownPreviewFocus'; private static readonly markdownPreviewActiveContextKey = 'markdownPreviewFocus';
private readonly topmostLineMonitor = new MarkdownFileTopmostLineMonitor(); private readonly topmostLineMonitor = new MarkdownFileTopmostLineMonitor();
@@ -29,15 +29,14 @@ export class MarkdownPreviewManager {
private readonly contributions: MarkdownContributions private readonly contributions: MarkdownContributions
) { ) {
vscode.window.onDidChangeActiveTextEditor(editor => { vscode.window.onDidChangeActiveTextEditor(editor => {
if (editor) { if (editor && isMarkdownFile(editor.document)) {
if (isMarkdownFile(editor.document)) { for (const preview of this.previews.filter(preview => !preview.locked)) {
for (const preview of this.previews.filter(preview => !preview.locked)) { preview.update(editor.document.uri);
preview.update(editor.document.uri);
}
} }
} }
}, null, this.disposables); }, null, this.disposables);
this.disposables.push(vscode.window.registerWebviewSerializer(MarkdownPreview.viewType, this));
} }
public dispose(): void { public dispose(): void {
@@ -66,7 +65,6 @@ export class MarkdownPreviewManager {
preview.reveal(previewSettings.previewColumn); preview.reveal(previewSettings.previewColumn);
} else { } else {
preview = this.createNewPreview(resource, previewSettings); preview = this.createNewPreview(resource, previewSettings);
this.previews.push(preview);
} }
preview.update(resource); preview.update(resource);
@@ -90,6 +88,30 @@ export class MarkdownPreviewManager {
} }
} }
public async deserializeWebview(
webview: vscode.Webview,
state: any
): Promise<boolean> {
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<any> {
const preview = this.previews.find(preview => preview.isWebviewOf(webview));
return preview ? preview.state : undefined;
}
private getExistingPreview( private getExistingPreview(
resource: vscode.Uri, resource: vscode.Uri,
previewSettings: PreviewSettings previewSettings: PreviewSettings
@@ -101,8 +123,8 @@ export class MarkdownPreviewManager {
private createNewPreview( private createNewPreview(
resource: vscode.Uri, resource: vscode.Uri,
previewSettings: PreviewSettings previewSettings: PreviewSettings
) { ): MarkdownPreview {
const preview = new MarkdownPreview( const preview = MarkdownPreview.create(
resource, resource,
previewSettings.previewColumn, previewSettings.previewColumn,
previewSettings.locked, previewSettings.locked,
@@ -112,6 +134,14 @@ export class MarkdownPreviewManager {
this.topmostLineMonitor, this.topmostLineMonitor,
this.contributions); this.contributions);
return this.registerPreview(preview);
}
private registerPreview(
preview: MarkdownPreview
): MarkdownPreview {
this.previews.push(preview);
preview.onDispose(() => { preview.onDispose(() => {
const existing = this.previews.indexOf(preview!); const existing = this.previews.indexOf(preview!);
if (existing >= 0) { if (existing >= 0) {

View File

@@ -568,7 +568,7 @@ declare module 'vscode' {
*/ */
export interface Webview { export interface Webview {
/** /**
* The type of the webview, such as `'markdownw.preview'` * The type of the webview, such as `'markdown.preview'`
*/ */
readonly viewType: string; readonly viewType: string;
@@ -636,16 +636,57 @@ declare module 'vscode' {
dispose(): any; 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<any>;
/**
* 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<boolean>;
}
namespace window { namespace window {
/** /**
* Create and show a new webview. * 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 title Title of the webview.
* @param column Editor column to show the new webview in. * @param column Editor column to show the new webview in.
* @param options Content settings for the webview. * @param options Content settings for the webview.
*/ */
export function createWebview(viewType: string, title: string, column: ViewColumn, options: WebviewOptions): 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 //#endregion

View File

@@ -2,44 +2,58 @@
* Copyright (c) Microsoft Corporation. All rights reserved. * Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information. * 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 * 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 { 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 URI from 'vs/base/common/uri';
import { WebviewInput } from 'vs/workbench/parts/webview/electron-browser/webviewInput'; import { TPromise } from 'vs/base/common/winjs.base';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
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 { ExtHostContext, ExtHostWebviewsShape, IExtHostContext, MainContext, MainThreadWebviewsShape, WebviewHandle } from 'vs/workbench/api/node/extHost.protocol';
import { WebviewEditor } from 'vs/workbench/parts/webview/electron-browser/webviewEditor'; 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) @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 readonly standardSupportedLinkSchemes = ['http', 'https', 'mailto'];
private _toDispose: Disposable[] = []; private static revivalPool = 0;
private _toDispose: IDisposable[] = [];
private readonly _proxy: ExtHostWebviewsShape; private readonly _proxy: ExtHostWebviewsShape;
private readonly _webviews = new Map<WebviewHandle, WebviewInput>(); private readonly _webviews = new Map<WebviewHandle, WebviewEditorInput>();
private readonly _revivers = new Set<string>();
private _activeWebview: WebviewInput | undefined = undefined; private _activeWebview: WebviewEditorInput | undefined = undefined;
constructor( constructor(
context: IExtHostContext, context: IExtHostContext,
@IContextKeyService _contextKeyService: IContextKeyService, @IContextKeyService contextKeyService: IContextKeyService,
@IPartService private readonly _partService: IPartService, @IEditorGroupService editorGroupService: IEditorGroupService,
@ILifecycleService lifecycleService: ILifecycleService,
@IWorkbenchEditorService private readonly _editorService: IWorkbenchEditorService, @IWorkbenchEditorService private readonly _editorService: IWorkbenchEditorService,
@IEditorGroupService private readonly _editorGroupService: IEditorGroupService, @IWebviewService private readonly _webviewService: IWebviewService,
@IOpenerService private readonly _openerService: IOpenerService @IOpenerService private readonly _openerService: IOpenerService,
@IExtensionService private readonly _extensionService: IExtensionService,
) { ) {
this._proxy = context.getProxy(ExtHostContext.ExtHostWebviews); 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 { dispose(): void {
@@ -51,30 +65,31 @@ export class MainThreadWebviews implements MainThreadWebviewsShape {
viewType: string, viewType: string,
title: string, title: string,
column: Position, column: Position,
options: vscode.WebviewOptions, options: WebviewInputOptions,
extensionFolderPath: string extensionFolderPath: string
): void { ): 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), onMessage: message => this._proxy.$onMessage(handle, message),
onDidChangePosition: position => this._proxy.$onDidChangePosition(handle, position), onDidChangePosition: position => this._proxy.$onDidChangePosition(handle, position),
onDispose: () => { onDispose: () => {
this._proxy.$onDidDisposeWeview(handle).then(() => { this._proxy.$onDidDisposeWeview(handle).then(() => {
this._webviews.delete(handle); 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 { $disposeWebview(handle: WebviewHandle): void {
const webview = this.getWebview(handle); const webview = this.getWebview(handle);
if (webview) { webview.dispose();
this._editorService.closeEditor(webview.position, webview);
}
} }
$setTitle(handle: WebviewHandle, value: string): void { $setTitle(handle: WebviewHandle, value: string): void {
@@ -84,24 +99,20 @@ export class MainThreadWebviews implements MainThreadWebviewsShape {
$setHtml(handle: WebviewHandle, value: string): void { $setHtml(handle: WebviewHandle, value: string): void {
const webview = this.getWebview(handle); const webview = this.getWebview(handle);
webview.setHtml(value); webview.html = value;
} }
$reveal(handle: WebviewHandle, column: Position): void { $reveal(handle: WebviewHandle, column: Position): void {
const webviewInput = this.getWebview(handle); const webview = this.getWebview(handle);
if (webviewInput.position === column) { this._webviewService.revealWebview(webview, column);
this._editorService.openEditor(webviewInput, { preserveFocus: true }, column);
} else {
this._editorGroupService.moveEditor(webviewInput, webviewInput.position, column, { preserveFocus: true });
}
} }
async $sendMessage(handle: WebviewHandle, message: any): Promise<boolean> { async $sendMessage(handle: WebviewHandle, message: any): Promise<boolean> {
const webviewInput = this.getWebview(handle); const webview = this.getWebview(handle);
const editors = this._editorService.getVisibleEditors() const editors = this._editorService.getVisibleEditors()
.filter(e => e instanceof WebviewEditor) .filter(e => e instanceof WebviewEditor)
.map(e => e as WebviewEditor) .map(e => e as WebviewEditor)
.filter(e => e.input.matches(webviewInput)); .filter(e => e.input.matches(webview));
for (const editor of editors) { for (const editor of editors) {
editor.sendMessage(message); editor.sendMessage(message);
@@ -110,18 +121,74 @@ export class MainThreadWebviews implements MainThreadWebviewsShape {
return (editors.length > 0); return (editors.length > 0);
} }
private getWebview(handle: number): WebviewInput { $registerSerializer(viewType: string): void {
const webviewInput = this._webviews.get(handle); this._revivers.add(viewType);
if (!webviewInput) { }
$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<boolean> {
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); throw new Error('Unknown webview handle:' + handle);
} }
return webviewInput; return webview;
} }
private onEditorsChanged() { private onEditorsChanged() {
const activeEditor = this._editorService.getActiveEditor(); const activeEditor = this._editorService.getActiveEditor();
let newActiveWebview: { input: WebviewInput, handle: WebviewHandle } | undefined = undefined; let newActiveWebview: { input: WebviewEditorInput, handle: WebviewHandle } | undefined = undefined;
if (activeEditor && activeEditor.input instanceof WebviewInput) { if (activeEditor && activeEditor.input instanceof WebviewEditorInput) {
for (const handle of map.keys(this._webviews)) { for (const handle of map.keys(this._webviews)) {
const input = this._webviews.get(handle); const input = this._webviews.get(handle);
if (input.matches(activeEditor.input)) { if (input.matches(activeEditor.input)) {
@@ -132,7 +199,7 @@ export class MainThreadWebviews implements MainThreadWebviewsShape {
} }
if (newActiveWebview) { if (newActiveWebview) {
if (!this._activeWebview || !newActiveWebview.input.matches(this._activeWebview)) { if (!this._activeWebview || newActiveWebview.input !== this._activeWebview) {
this._proxy.$onDidChangeActiveWeview(newActiveWebview.handle); this._proxy.$onDidChangeActiveWeview(newActiveWebview.handle);
this._activeWebview = newActiveWebview.input; 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) { if (!link) {
return; return;
} }

View File

@@ -418,6 +418,9 @@ export function createApiFactory(
}), }),
createWebview: proposedApiFunction(extension, (viewType: string, title: string, column: vscode.ViewColumn, options: vscode.WebviewOptions) => { createWebview: proposedApiFunction(extension, (viewType: string, title: string, column: vscode.ViewColumn, options: vscode.WebviewOptions) => {
return extHostWebviews.createWebview(viewType, title, column, options, extension.extensionFolderPath); return extHostWebviews.createWebview(viewType, title, column, options, extension.extensionFolderPath);
}),
registerWebviewSerializer: proposedApiFunction(extension, (viewType: string, serializer: vscode.WebviewSerializer) => {
return extHostWebviews.registerWebviewSerializer(viewType, serializer);
}) })
}; };

View File

@@ -347,7 +347,7 @@ export interface MainThreadTelemetryShape extends IDisposable {
$publicLog(eventName: string, data?: any): void; $publicLog(eventName: string, data?: any): void;
} }
export type WebviewHandle = number; export type WebviewHandle = string;
export interface MainThreadWebviewsShape extends IDisposable { export interface MainThreadWebviewsShape extends IDisposable {
$createWebview(handle: WebviewHandle, viewType: string, title: string, column: EditorPosition, options: vscode.WebviewOptions, extensionFolderPath: string): void; $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; $setTitle(handle: WebviewHandle, value: string): void;
$setHtml(handle: WebviewHandle, value: string): void; $setHtml(handle: WebviewHandle, value: string): void;
$sendMessage(handle: WebviewHandle, value: any): Thenable<boolean>; $sendMessage(handle: WebviewHandle, value: any): Thenable<boolean>;
$registerSerializer(viewType: string): void;
$unregisterSerializer(viewType: string): void;
} }
export interface ExtHostWebviewsShape { export interface ExtHostWebviewsShape {
$onMessage(handle: WebviewHandle, message: any): void; $onMessage(handle: WebviewHandle, message: any): void;
$onDidChangeActiveWeview(handle: WebviewHandle | undefined): void; $onDidChangeActiveWeview(handle: WebviewHandle | undefined): void;
$onDidDisposeWeview(handle: WebviewHandle): Thenable<void>; $onDidDisposeWeview(handle: WebviewHandle): Thenable<void>;
$onDidChangePosition(handle: WebviewHandle, newPosition: EditorPosition): void; $onDidChangePosition(handle: WebviewHandle, newPosition: EditorPosition): void;
$deserializeWebview(newWebviewHandle: WebviewHandle, viewType: string, state: any, position: EditorPosition, options: vscode.WebviewOptions): void;
$serializeWebview(webviewHandle: WebviewHandle): Thenable<any>;
} }
export interface MainThreadWorkspaceShape extends IDisposable { export interface MainThreadWorkspaceShape extends IDisposable {

View File

@@ -9,6 +9,7 @@ import { Event, Emitter } from 'vs/base/common/event';
import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters'; import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters';
import { Position } from 'vs/platform/editor/common/editor'; import { Position } from 'vs/platform/editor/common/editor';
import { TPromise } from 'vs/base/common/winjs.base'; import { TPromise } from 'vs/base/common/winjs.base';
import { Disposable } from './extHostTypes';
export class ExtHostWebview implements vscode.Webview { export class ExtHostWebview implements vscode.Webview {
@@ -19,6 +20,7 @@ export class ExtHostWebview implements vscode.Webview {
private _isDisposed: boolean = false; private _isDisposed: boolean = false;
private _viewColumn: vscode.ViewColumn; private _viewColumn: vscode.ViewColumn;
private _active: boolean; private _active: boolean;
private _state: any;
public readonly onMessageEmitter = new Emitter<any>(); public readonly onMessageEmitter = new Emitter<any>();
public readonly onDidReceiveMessage: Event<any> = this.onMessageEmitter.event; public readonly onDidReceiveMessage: Event<any> = 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 { get options(): vscode.WebviewOptions {
this.assertNotDisposed(); this.assertNotDisposed();
return this._options; return this._options;
@@ -128,11 +135,12 @@ export class ExtHostWebview implements vscode.Webview {
} }
export class ExtHostWebviews implements ExtHostWebviewsShape { export class ExtHostWebviews implements ExtHostWebviewsShape {
private static handlePool = 1; private static webviewHandlePool = 1;
private readonly _proxy: MainThreadWebviewsShape; private readonly _proxy: MainThreadWebviewsShape;
private readonly _webviews = new Map<WebviewHandle, ExtHostWebview>(); private readonly _webviews = new Map<WebviewHandle, ExtHostWebview>();
private readonly _serializers = new Map<string, vscode.WebviewSerializer>();
private _activeWebview: ExtHostWebview | undefined; private _activeWebview: ExtHostWebview | undefined;
@@ -149,7 +157,7 @@ export class ExtHostWebviews implements ExtHostWebviewsShape {
options: vscode.WebviewOptions, options: vscode.WebviewOptions,
extensionFolderPath: string extensionFolderPath: string
): vscode.Webview { ): vscode.Webview {
const handle = ExtHostWebviews.handlePool++; const handle = ExtHostWebviews.webviewHandlePool++ + '';
this._proxy.$createWebview(handle, viewType, title, typeConverters.fromViewColumn(viewColumn), options, extensionFolderPath); this._proxy.$createWebview(handle, viewType, title, typeConverters.fromViewColumn(viewColumn), options, extensionFolderPath);
const webview = new ExtHostWebview(handle, this._proxy, viewType, viewColumn, options); const webview = new ExtHostWebview(handle, this._proxy, viewType, viewColumn, options);
@@ -157,6 +165,23 @@ export class ExtHostWebviews implements ExtHostWebviewsShape {
return webview; 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 { $onMessage(handle: WebviewHandle, message: any): void {
const webview = this.getWebview(handle); const webview = this.getWebview(handle);
if (webview) { if (webview) {
@@ -206,8 +231,35 @@ export class ExtHostWebviews implements ExtHostWebviewsShape {
} }
} }
private readonly _onDidChangeActiveWebview = new Emitter<ExtHostWebview | undefined>(); $deserializeWebview(
public readonly onDidChangeActiveWebview = this._onDidChangeActiveWebview.event; 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<any> {
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) { private getWebview(handle: WebviewHandle) {
return this._webviews.get(handle); return this._webviews.get(handle);

View File

@@ -5,29 +5,29 @@
'use strict'; '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 { marked } from 'vs/base/common/marked/marked';
import { IModeService } from 'vs/editor/common/services/modeService'; import { OS } from 'vs/base/common/platform';
import { tokenizeToString } from 'vs/editor/common/modes/textToHtmlTokenizer'; 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 { IMode, TokenizationRegistry } from 'vs/editor/common/modes';
import { generateTokensCSSForColorMap } from 'vs/editor/common/modes/supports/tokenization'; 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 { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IModeService } from 'vs/editor/common/services/modeService';
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 { 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 * as nls from 'vs/nls';
import { OS } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
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 { addGAParameters } from 'vs/platform/telemetry/node/telemetryNodeUtils';
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( function renderBody(
body: string, body: string,
@@ -51,18 +51,17 @@ export class ReleaseNotesManager {
private _releaseNotesCache: { [version: string]: TPromise<string>; } = Object.create(null); private _releaseNotesCache: { [version: string]: TPromise<string>; } = Object.create(null);
private _currentReleaseNotes: WebviewInput | undefined = undefined; private _currentReleaseNotes: WebviewEditorInput | undefined = undefined;
public constructor( public constructor(
@IEditorGroupService private readonly _editorGroupService: IEditorGroupService,
@IEnvironmentService private readonly _environmentService: IEnvironmentService, @IEnvironmentService private readonly _environmentService: IEnvironmentService,
@IKeybindingService private readonly _keybindingService: IKeybindingService, @IKeybindingService private readonly _keybindingService: IKeybindingService,
@IModeService private readonly _modeService: IModeService, @IModeService private readonly _modeService: IModeService,
@IOpenerService private readonly _openerService: IOpenerService, @IOpenerService private readonly _openerService: IOpenerService,
@IPartService private readonly _partService: IPartService,
@IRequestService private readonly _requestService: IRequestService, @IRequestService private readonly _requestService: IRequestService,
@ITelemetryService private readonly _telemetryService: ITelemetryService, @ITelemetryService private readonly _telemetryService: ITelemetryService,
@IWorkbenchEditorService private readonly _editorService: IWorkbenchEditorService, @IWorkbenchEditorService private readonly _editorService: IWorkbenchEditorService,
@IWebviewService private readonly _webviewService: IWebviewService,
) { } ) { }
public async show( public async show(
@@ -73,21 +72,23 @@ export class ReleaseNotesManager {
const html = await this.renderBody(releaseNoteText); const html = await this.renderBody(releaseNoteText);
const title = nls.localize('releaseNotesInputName', "Release Notes: {0}", version); const title = nls.localize('releaseNotesInputName', "Release Notes: {0}", version);
const activeEditor = this._editorService.getActiveEditor();
if (this._currentReleaseNotes) { if (this._currentReleaseNotes) {
this._currentReleaseNotes.setName(title); this._currentReleaseNotes.setName(title);
this._currentReleaseNotes.setHtml(html); this._currentReleaseNotes.html = html;
const activeEditor = this._editorService.getActiveEditor(); this._webviewService.revealWebview(this._currentReleaseNotes, activeEditor ? activeEditor.position : undefined);
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 });
}
} else { } else {
this._currentReleaseNotes = new WebviewInput(title, { tryRestoreScrollPosition: true, enableFindWidget: true }, html, { this._currentReleaseNotes = this._webviewService.createWebview(
onDidClickLink: uri => this.onDidClickLink(uri), 'releaseNotes',
onDispose: () => { this._currentReleaseNotes = undefined; } title,
}, this._partService); activeEditor ? activeEditor.position : Position.ONE,
await this._editorService.openEditor(this._currentReleaseNotes, { pinned: true }); { tryRestoreScrollPosition: true, enableFindWidget: true },
undefined, {
onDidClickLink: uri => this.onDidClickLink(uri),
onDispose: () => { this._currentReleaseNotes = undefined; }
});
this._currentReleaseNotes.html = html;
} }
return true; return true;

View File

@@ -7,11 +7,21 @@ import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } fro
import { WebviewEditor } from './webviewEditor'; import { WebviewEditor } from './webviewEditor';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { Registry } from 'vs/platform/registry/common/platform'; import { Registry } from 'vs/platform/registry/common/platform';
import { WebviewInput } from './webviewInput'; import { WebviewEditorInput } from './webviewInput';
import { localize } from 'vs/nls'; 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<IEditorRegistry>(EditorExtensions.Editors)).registerEditor(new EditorDescriptor( (Registry.as<IEditorRegistry>(EditorExtensions.Editors)).registerEditor(new EditorDescriptor(
WebviewEditor, WebviewEditor,
WebviewEditor.ID, WebviewEditor.ID,
localize('webview.editor.label', "webview editor")), localize('webview.editor.label', "webview editor")),
[new SyncDescriptor(WebviewInput)]); [new SyncDescriptor(WebviewEditorInput)]);
Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory(
WebviewInputFactory.ID,
WebviewInputFactory);
registerSingleton(IWebviewService, WebviewService);

View File

@@ -19,7 +19,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import * as DOM from 'vs/base/browser/dom'; import * as DOM from 'vs/base/browser/dom';
import { Event, Emitter } from 'vs/base/common/event'; 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'; import URI from 'vs/base/common/uri';
export class WebviewEditor extends BaseWebviewEditor { export class WebviewEditor extends BaseWebviewEditor {
@@ -29,10 +29,12 @@ export class WebviewEditor extends BaseWebviewEditor {
private editorFrame: HTMLElement; private editorFrame: HTMLElement;
private content: HTMLElement; private content: HTMLElement;
private webviewContent: HTMLElement | undefined; private webviewContent: HTMLElement | undefined;
private readonly _onDidFocusWebview: Emitter<void>;
private _webviewFocusTracker?: DOM.IFocusTracker; private _webviewFocusTracker?: DOM.IFocusTracker;
private _webviewFocusListenerDisposable?: IDisposable; private _webviewFocusListenerDisposable?: IDisposable;
private readonly _onDidFocusWebview = new Emitter<void>();
constructor( constructor(
@ITelemetryService telemetryService: ITelemetryService, @ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService, @IThemeService themeService: IThemeService,
@@ -43,8 +45,6 @@ export class WebviewEditor extends BaseWebviewEditor {
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService
) { ) {
super(WebviewEditor.ID, telemetryService, themeService, _contextKeyService); super(WebviewEditor.ID, telemetryService, themeService, _contextKeyService);
this._onDidFocusWebview = new Emitter<void>();
} }
protected createEditor(parent: Builder): void { protected createEditor(parent: Builder): void {
@@ -54,7 +54,7 @@ export class WebviewEditor extends BaseWebviewEditor {
} }
private doUpdateContainer() { private doUpdateContainer() {
const webviewContainer = this.input && (this.input as WebviewInput).container; const webviewContainer = this.input && (this.input as WebviewEditorInput).container;
if (webviewContainer && webviewContainer.parentElement) { if (webviewContainer && webviewContainer.parentElement) {
const frameRect = this.editorFrame.getBoundingClientRect(); const frameRect = this.editorFrame.getBoundingClientRect();
const containerRect = webviewContainer.parentElement.getBoundingClientRect(); const containerRect = webviewContainer.parentElement.getBoundingClientRect();
@@ -103,14 +103,14 @@ export class WebviewEditor extends BaseWebviewEditor {
} }
protected setEditorVisible(visible: boolean, position?: Position): void { protected setEditorVisible(visible: boolean, position?: Position): void {
if (this.input && this.input instanceof WebviewInput) { if (this.input && this.input instanceof WebviewEditorInput) {
if (visible) { if (visible) {
this.input.claimWebview(this); this.input.claimWebview(this);
} else { } else {
this.input.releaseWebview(this); this.input.releaseWebview(this);
} }
this.updateWebview(this.input as WebviewInput); this.updateWebview(this.input as WebviewEditorInput);
} }
if (this.webviewContent) { if (this.webviewContent) {
@@ -126,7 +126,7 @@ export class WebviewEditor extends BaseWebviewEditor {
} }
public clearInput() { public clearInput() {
if (this.input && this.input instanceof WebviewInput) { if (this.input && this.input instanceof WebviewEditorInput) {
this.input.releaseWebview(this); this.input.releaseWebview(this);
} }
@@ -136,24 +136,24 @@ export class WebviewEditor extends BaseWebviewEditor {
super.clearInput(); super.clearInput();
} }
async setInput(input: WebviewInput, options: EditorOptions): TPromise<void> { async setInput(input: WebviewEditorInput, options: EditorOptions): TPromise<void> {
if (this.input && this.input.matches(input)) { if (this.input && this.input.matches(input)) {
return undefined; return undefined;
} }
if (this.input) { if (this.input) {
(this.input as WebviewInput).releaseWebview(this); (this.input as WebviewEditorInput).releaseWebview(this);
this._webview = undefined; this._webview = undefined;
this.webviewContent = undefined; this.webviewContent = undefined;
} }
await super.setInput(input, options); await super.setInput(input, options);
input.onDidChangePosition(this.position); input.onBecameActive(this.position);
this.updateWebview(input); this.updateWebview(input);
} }
private updateWebview(input: WebviewInput) { private updateWebview(input: WebviewEditorInput) {
const webview = this.getWebview(input); const webview = this.getWebview(input);
input.claimWebview(this); input.claimWebview(this);
webview.options = { webview.options = {
@@ -163,7 +163,7 @@ export class WebviewEditor extends BaseWebviewEditor {
useSameOriginForRoot: false, useSameOriginForRoot: false,
localResourceRoots: input.options.localResourceRoots || this.getDefaultLocalResourceRoots() localResourceRoots: input.options.localResourceRoots || this.getDefaultLocalResourceRoots()
}; };
input.setHtml(input.html); input.html = input.html;
if (this.webviewContent) { if (this.webviewContent) {
this.webviewContent.style.visibility = 'visible'; this.webviewContent.style.visibility = 'visible';
@@ -174,13 +174,13 @@ export class WebviewEditor extends BaseWebviewEditor {
private getDefaultLocalResourceRoots(): URI[] { private getDefaultLocalResourceRoots(): URI[] {
const rootPaths = this._contextService.getWorkspace().folders.map(x => x.uri); const rootPaths = this._contextService.getWorkspace().folders.map(x => x.uri);
if ((this.input as WebviewInput).extensionFolderPath) { if ((this.input as WebviewEditorInput).extensionFolderPath) {
rootPaths.push((this.input as WebviewInput).extensionFolderPath); rootPaths.push((this.input as WebviewEditorInput).extensionFolderPath);
} }
return rootPaths; return rootPaths;
} }
private getWebview(input: WebviewInput): Webview { private getWebview(input: WebviewEditorInput): Webview {
if (this._webview) { if (this._webview) {
return this._webview; return this._webview;
} }

View File

@@ -5,69 +5,61 @@
'use strict'; 'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { IDisposable, dispose } from 'vs/base/common/lifecycle'; 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 { 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 { Webview } from 'vs/workbench/parts/html/electron-browser/webview';
import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; import { IPartService, Parts } from 'vs/workbench/services/part/common/partService';
import * as vscode from 'vscode'; import { WebviewEvents, WebviewInputOptions, WebviewReviver } from './webviewService';
import URI from 'vs/base/common/uri';
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 { export class WebviewEditorInput extends EditorInput {
tryRestoreScrollPosition?: boolean;
}
export class WebviewInput extends EditorInput {
private static handlePool = 0; private static handlePool = 0;
public static readonly typeId = 'workbench.editors.webviewInput';
private _name: string; private _name: string;
private _options: WebviewInputOptions; private _options: WebviewInputOptions;
private _html: string; private _html: string = '';
private _currentWebviewHtml: string = ''; private _currentWebviewHtml: string = '';
private _events: WebviewEvents | undefined; public _events: WebviewEvents | undefined;
private _container: HTMLElement; private _container: HTMLElement;
private _webview: Webview | undefined; private _webview: Webview | undefined;
private _webviewOwner: any; private _webviewOwner: any;
private _webviewDisposables: IDisposable[] = []; private _webviewDisposables: IDisposable[] = [];
private _position?: Position; private _position?: Position;
private _scrollYPercentage: number = 0; private _scrollYPercentage: number = 0;
private _state: any;
private _revived: boolean = false;
public readonly extensionFolderPath: URI | undefined; public readonly extensionFolderPath: URI | undefined;
constructor( constructor(
public readonly viewType: string,
name: string, name: string,
options: WebviewInputOptions, options: WebviewInputOptions,
html: string, state: any,
events: WebviewEvents, events: WebviewEvents,
partService: IPartService, extensionFolderPath: string | undefined,
extensionFolderPath?: string public readonly reviver: WebviewReviver | undefined,
@IPartService private readonly _partService: IPartService,
) { ) {
super(); super();
this._name = name; this._name = name;
this._options = options; this._options = options;
this._html = html;
this._events = events; this._events = events;
this._state = state;
if (extensionFolderPath) { if (extensionFolderPath) {
this.extensionFolderPath = URI.file(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 { public getTypeId(): string {
return 'webview'; return WebviewEditorInput.typeId;
} }
public dispose() { public dispose() {
@@ -119,7 +111,7 @@ export class WebviewInput extends EditorInput {
return this._html; return this._html;
} }
public setHtml(value: string): void { public set html(value: string) {
if (value === this._currentWebviewHtml) { if (value === this._currentWebviewHtml) {
return; 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 { public get options(): WebviewInputOptions {
return this._options; return this._options;
} }
@@ -149,6 +149,12 @@ export class WebviewInput extends EditorInput {
} }
public get container(): HTMLElement { 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; return this._container;
} }
@@ -215,10 +221,16 @@ export class WebviewInput extends EditorInput {
this._currentWebviewHtml = ''; this._currentWebviewHtml = '';
} }
public onDidChangePosition(position: Position) { public onBecameActive(position: Position) {
this._position = position;
if (this._events && this._events.onDidChangePosition) { if (this._events && this._events.onDidChangePosition) {
this._events.onDidChangePosition(position); this._events.onDidChangePosition(position);
} }
this._position = position;
if (this.reviver && !this._revived) {
this._revived = true;
this.reviver.reviveWebview(this);
}
} }
} }

View File

@@ -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);
}
}

View File

@@ -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<IWebviewService>('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<string, WebviewReviver>();
private readonly _needingRevival = new Map<string, WebviewEditorInput[]>();
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);
}
}
}