mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 21:09:43 +00:00
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:
parent
c7b37f5915
commit
dd21d3520a
|
@ -22,7 +22,8 @@
|
|||
"onCommand:markdown.showPreviewToSide",
|
||||
"onCommand:markdown.showLockedPreviewToSide",
|
||||
"onCommand:markdown.showSource",
|
||||
"onCommand:markdown.showPreviewSecuritySelector"
|
||||
"onCommand:markdown.showPreviewSecuritySelector",
|
||||
"onView:markdown.preview"
|
||||
],
|
||||
"contributes": {
|
||||
"commands": [
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
45
src/vs/vscode.proposed.d.ts
vendored
45
src/vs/vscode.proposed.d.ts
vendored
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
};
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue