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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 690 additions and 176 deletions

View file

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

View file

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

View file

@ -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<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(
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) {

View file

@ -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<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 {
/**
* 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

View file

@ -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 { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import * as vscode from 'vscode';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
import URI from 'vs/base/common/uri';
import { WebviewInput } from 'vs/workbench/parts/webview/electron-browser/webviewInput';
import { 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 { 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<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(
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<boolean> {
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<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);
}
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;
}

View file

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

View file

@ -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<boolean>;
$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<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 {

View file

@ -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<any>();
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 {
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<WebviewHandle, ExtHostWebview>();
private readonly _serializers = new Map<string, vscode.WebviewSerializer>();
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<ExtHostWebview | undefined>();
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<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) {
return this._webviews.get(handle);

View file

@ -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 { 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 { 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 { tokenizeToString } from 'vs/editor/common/modes/textToHtmlTokenizer';
import { IModeService } from 'vs/editor/common/services/modeService';
import * as nls from 'vs/nls';
import { OS } from 'vs/base/common/platform';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
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(
body: string,
@ -51,18 +51,17 @@ export class ReleaseNotesManager {
private _releaseNotesCache: { [version: string]: TPromise<string>; } = 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;

View file

@ -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<IEditorRegistry>(EditorExtensions.Editors)).registerEditor(new EditorDescriptor(
WebviewEditor,
WebviewEditor.ID,
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 * 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<void>;
private _webviewFocusTracker?: DOM.IFocusTracker;
private _webviewFocusListenerDisposable?: IDisposable;
private readonly _onDidFocusWebview = new Emitter<void>();
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<void>();
}
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<void> {
async setInput(input: WebviewEditorInput, options: EditorOptions): TPromise<void> {
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;
}

View file

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

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