Skip to content

Commit

Permalink
First attempt at webview restoration api
Browse files Browse the repository at this point in the history
From #45994

Adds an experimental API that allows extensions to persist webviews and restore them even after vscode restarts. Uses this api to restore markdown previews. This is done by:

- Adding a new `state` field on webviews. This is a json serializable blob of data
- Adds a new `WebviewReviver` interface (name will probably change). This binds a specific viewType to a provider that can restore a webview's contents from its `state`
- In VS Code core, persist webview editors. When we restart and need to show a webview, activate all extensions that may have reviviers for it using the `onView:viewType` activation event.

Current implementation is sort of a mess. Will try to clean things up
  • Loading branch information
mjbvz committed Apr 4, 2018
1 parent 5900899 commit 7aa9982
Show file tree
Hide file tree
Showing 14 changed files with 616 additions and 152 deletions.
3 changes: 2 additions & 1 deletion extensions/markdown-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"onCommand:markdown.showPreviewToSide",
"onCommand:markdown.showLockedPreviewToSide",
"onCommand:markdown.showSource",
"onCommand:markdown.showPreviewSecuritySelector"
"onCommand:markdown.showPreviewSecuritySelector",
"onView:markdown.preview"
],
"contributes": {
"commands": [
Expand Down
93 changes: 74 additions & 19 deletions extensions/markdown-language-features/src/features/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const localize = nls.loadMessageBundle();

export class MarkdownPreview {

public static previewViewType = 'markdown.preview';
public static viewType = 'markdown.preview';

private readonly webview: vscode.Webview;
private throttleTimer: any;
Expand All @@ -29,26 +29,67 @@ export class MarkdownPreview {
private forceUpdate = false;
private isScrolling = false;

constructor(
private _resource: vscode.Uri,
public static revive(
webview: vscode.Webview,
contentProvider: MarkdownContentProvider,
previewConfigurations: MarkdownPreviewConfigurationManager,
logger: Logger,
topmostLineMonitor: MarkdownFileTopmostLineMonitor
): MarkdownPreview {
const resource = vscode.Uri.parse(webview.state.resource);
const locked = webview.state.locked;

return new MarkdownPreview(
webview,
resource,
locked,
contentProvider,
previewConfigurations,
logger,
topmostLineMonitor);
}

public static create(
resource: vscode.Uri,
previewColumn: vscode.ViewColumn,
public locked: boolean,
private readonly contentProvider: MarkdownContentProvider,
private readonly previewConfigurations: MarkdownPreviewConfigurationManager,
private readonly logger: Logger,
locked: boolean,
contentProvider: MarkdownContentProvider,
previewConfigurations: MarkdownPreviewConfigurationManager,
logger: Logger,
topmostLineMonitor: MarkdownFileTopmostLineMonitor,
private readonly contributions: MarkdownContributions
) {
this.webview = vscode.window.createWebview(
MarkdownPreview.previewViewType,
this.getPreviewTitle(this._resource),
contributions: MarkdownContributions
): MarkdownPreview {
const webview = vscode.window.createWebview(
MarkdownPreview.viewType,
MarkdownPreview.getPreviewTitle(resource, locked),
previewColumn, {
enableScripts: true,
enableCommandUris: true,
enableFindWidget: true,
localResourceRoots: this.getLocalResourceRoots(_resource)
localResourceRoots: MarkdownPreview.getLocalResourceRoots(resource, contributions)
});

return new MarkdownPreview(
webview,
resource,
locked,
contentProvider,
previewConfigurations,
logger,
topmostLineMonitor);
}

private constructor(
webview: vscode.Webview,
private _resource: vscode.Uri,
public locked: boolean,
private readonly contentProvider: MarkdownContentProvider,
private readonly previewConfigurations: MarkdownPreviewConfigurationManager,
private readonly logger: Logger,
topmostLineMonitor: MarkdownFileTopmostLineMonitor
) {
this.webview = webview;

this.webview.onDidDispose(() => {
this.dispose();
}, null, this.disposables);
Expand Down Expand Up @@ -99,6 +140,8 @@ export class MarkdownPreview {
});
}
}, null, this.disposables);

this.updateState();
}

private readonly _onDisposeEmitter = new vscode.EventEmitter<void>();
Expand Down Expand Up @@ -195,11 +238,12 @@ export class MarkdownPreview {

public toggleLock() {
this.locked = !this.locked;
this.webview.title = this.getPreviewTitle(this._resource);
this.webview.title = MarkdownPreview.getPreviewTitle(this._resource, this.locked);
this.updateState();
}

private getPreviewTitle(resource: vscode.Uri): string {
return this.locked
private static getPreviewTitle(resource: vscode.Uri, locked: boolean): string {
return locked
? localize('lockedPreviewTitle', '[Preview] {0}', path.basename(resource.fsPath))
: localize('previewTitle', 'Preview {0}', path.basename(resource.fsPath));
}
Expand Down Expand Up @@ -244,14 +288,18 @@ export class MarkdownPreview {
this.contentProvider.provideTextDocumentContent(document, this.previewConfigurations, this.initialLine)
.then(content => {
if (this._resource === resource) {
this.webview.title = this.getPreviewTitle(this._resource);
this.webview.title = MarkdownPreview.getPreviewTitle(this._resource, this.locked);
this.webview.html = content;
this.updateState();
}
});
}

private getLocalResourceRoots(resource: vscode.Uri): vscode.Uri[] {
const baseRoots = this.contributions.previewResourceRoots;
private static getLocalResourceRoots(
resource: vscode.Uri,
contributions: MarkdownContributions
): vscode.Uri[] {
const baseRoots = contributions.previewResourceRoots;

const folder = vscode.workspace.getWorkspaceFolder(resource);
if (folder) {
Expand Down Expand Up @@ -292,6 +340,13 @@ export class MarkdownPreview {
}
}
}

private updateState() {
this.webview.state = {
resource: this.resource.toString(),
locked: this.locked
};
}
}

export interface PreviewSettings {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { isMarkdownFile } from '../util/file';
import { MarkdownPreviewConfigurationManager } from './previewConfig';
import { MarkdownContributions } from '../markdownExtensions';

export class MarkdownPreviewManager {
export class MarkdownPreviewManager implements vscode.WebviewReviver {
private static readonly markdownPreviewActiveContextKey = 'markdownPreviewFocus';

private readonly topmostLineMonitor = new MarkdownFileTopmostLineMonitor();
Expand All @@ -29,15 +29,14 @@ export class MarkdownPreviewManager {
private readonly contributions: MarkdownContributions
) {
vscode.window.onDidChangeActiveTextEditor(editor => {
if (editor) {
if (isMarkdownFile(editor.document)) {
for (const preview of this.previews.filter(preview => !preview.locked)) {
preview.update(editor.document.uri);
}
if (editor && isMarkdownFile(editor.document)) {
for (const preview of this.previews.filter(preview => !preview.locked)) {
preview.update(editor.document.uri);
}
}
}, null, this.disposables);

this.disposables.push(vscode.window.registerWebviewReviver(MarkdownPreview.viewType, this));
}

public dispose(): void {
Expand Down Expand Up @@ -66,7 +65,6 @@ export class MarkdownPreviewManager {
preview.reveal(previewSettings.previewColumn);
} else {
preview = this.createNewPreview(resource, previewSettings);
this.previews.push(preview);
}

preview.update(resource);
Expand All @@ -90,6 +88,22 @@ export class MarkdownPreviewManager {
}
}

public reviveWebview(
webview: vscode.Webview
): void {
console.log('It\'s close to midnight...');

const preview = MarkdownPreview.revive(
webview,
this.contentProvider,
this.previewConfigurations,
this.logger,
this.topmostLineMonitor);

this.registerPreview(preview);
preview.refresh();
}

private getExistingPreview(
resource: vscode.Uri,
previewSettings: PreviewSettings
Expand All @@ -101,8 +115,8 @@ export class MarkdownPreviewManager {
private createNewPreview(
resource: vscode.Uri,
previewSettings: PreviewSettings
) {
const preview = new MarkdownPreview(
): MarkdownPreview {
const preview = MarkdownPreview.create(
resource,
previewSettings.previewColumn,
previewSettings.locked,
Expand All @@ -112,6 +126,14 @@ export class MarkdownPreviewManager {
this.topmostLineMonitor,
this.contributions);

return this.registerPreview(preview);
}

private registerPreview(
preview: MarkdownPreview
): MarkdownPreview {
this.previews.push(preview);

preview.onDispose(() => {
const existing = this.previews.indexOf(preview!);
if (existing >= 0) {
Expand Down
36 changes: 34 additions & 2 deletions src/vs/vscode.proposed.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -589,6 +589,11 @@ declare module 'vscode' {
*/
html: string;

/**
* JSON serializable blob of data saved on webviews for revival.
*/
state: any;

/**
* The column in which the webview is showing.
*/
Expand Down Expand Up @@ -636,16 +641,43 @@ declare module 'vscode' {
dispose(): any;
}

/**
* Restores webviews that have been persisted when vscode shuts down.
*/
interface WebviewReviver {
/**
* Restore a webview's `html` from its `state`.
*
* Called when a serialized webview first becomes active.
*
* @param webview Webview to revive.
*/
reviveWebview(webview: Webview): void;
}

namespace window {
/**
* Create and show a new webview.
*
* @param viewType Identifier the type of the webview.
* @param viewType Identifies the type of the webview.
* @param title Title of the webview.
* @param column Editor column to show the new webview in.
* @param options Content settings for the webview.
*/
export function createWebview(viewType: string, title: string, column: ViewColumn, options: WebviewOptions): Webview;

/**
* Registers a webview reviver.
*
* Extensions that support reviving should have an `"onView:viewType"` activation method and
* make sure that `registerWebviewReviver` is called during activation.
*
* Only a single reviver may be registered at a time for a given `viewType`.
*
* @param viewType Type of the webview that can be revived.
* @param reviver Webview revivier.
*/
export function registerWebviewReviver(viewType: string, reviver: WebviewReviver): Disposable;
}

//#endregion
Expand Down
Loading

0 comments on commit 7aa9982

Please sign in to comment.