Skip to content

Commit

Permalink
Webview API prototype 2
Browse files Browse the repository at this point in the history
Part of #43713

Second try at refining the webview api. This pass specifically looks at managing webviews. Major changes:

- Adds an `id` field to webviews. The id is provided by the extension and identifies the webview. It is used with the new event handling apis

- Adds a new `onDidChangeActiveEditor` api. This is similar to `onDidChangeActiveTextEditor` but is also fired when you change webviews. It replaces the old `onFocus` and `onBlur` events on the webview itself

- Replaces `createWebview` with `getOrCreateWebview`. This new API uses the id and column together as a key. The idea is that only a single webview of id may exist in a given column

- Adds an `onDidCloseWebview` api. This is fired when a webview is closed by the user

The motivation for these changes is #27983, which tracks using the same markdown preview for any active markdown files. I believe this case is similar to how other extensions may use the webview.
  • Loading branch information
mjbvz committed Feb 22, 2018
1 parent 2d1f6d4 commit d405242
Show file tree
Hide file tree
Showing 13 changed files with 249 additions and 176 deletions.
14 changes: 3 additions & 11 deletions extensions/markdown/src/commands/refreshPreview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { Command } from '../commandManager';
import { isMarkdownFile, getMarkdownUri, MarkdownPreviewWebviewManager } from '../features/previewContentProvider';
import { MarkdownPreviewWebviewManager } from '../features/previewContentProvider';

export class RefreshPreviewCommand implements Command {
public readonly id = 'markdown.refreshPreview';
Expand All @@ -14,14 +13,7 @@ export class RefreshPreviewCommand implements Command {
private readonly webviewManager: MarkdownPreviewWebviewManager
) { }

public execute(resource: string | undefined) {
if (resource) {
const source = vscode.Uri.parse(resource);
this.webviewManager.update(source);
} else if (vscode.window.activeTextEditor && isMarkdownFile(vscode.window.activeTextEditor.document)) {
this.webviewManager.update(getMarkdownUri(vscode.window.activeTextEditor.document.uri));
} else {
this.webviewManager.updateAll();
}
public execute() {
this.webviewManager.refresh();
}
}
3 changes: 2 additions & 1 deletion extensions/markdown/src/commands/showPreview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ function showPreview(
return;
}

const view = webviewManager.create(
const view = webviewManager.preview(
resource,
(vscode.window.activeTextEditor && vscode.window.activeTextEditor.viewColumn) || vscode.ViewColumn.One,
getViewColumn(sideBySide) || vscode.ViewColumn.Active);

telemetryReporter.sendTelemetryEvent('openPreview', {
Expand Down
15 changes: 1 addition & 14 deletions extensions/markdown/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { loadDefaultTelemetryReporter } from './telemetryReporter';
import { loadMarkdownExtensions } from './markdownExtensions';
import LinkProvider from './features/documentLinkProvider';
import MDDocumentSymbolProvider from './features/documentSymbolProvider';
import { MarkdownContentProvider, getMarkdownUri, isMarkdownFile, MarkdownPreviewWebviewManager } from './features/previewContentProvider';
import { MarkdownContentProvider, MarkdownPreviewWebviewManager } from './features/previewContentProvider';


export function activate(context: vscode.ExtensionContext) {
Expand Down Expand Up @@ -55,17 +55,4 @@ export function activate(context: vscode.ExtensionContext) {
logger.updateConfiguration();
webviewManager.updateConfiguration();
}));

context.subscriptions.push(vscode.window.onDidChangeTextEditorSelection(event => {
if (isMarkdownFile(event.textEditor.document)) {
const markdownFile = getMarkdownUri(event.textEditor.document.uri);
logger.log('updatePreviewForSelection', { markdownFile: markdownFile.toString() });

vscode.commands.executeCommand('_workbench.htmlPreview.postMessage',
markdownFile,
{
line: event.selections[0].active.line
});
}
}));
}
128 changes: 72 additions & 56 deletions extensions/markdown/src/features/previewContentProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,7 @@ const previewStrings = {
};

export function isMarkdownFile(document: vscode.TextDocument) {
return document.languageId === 'markdown'
&& document.uri.scheme !== MarkdownContentProvider.scheme; // prevent processing of own documents
}

export function getMarkdownUri(uri: vscode.Uri) {
if (uri.scheme === MarkdownContentProvider.scheme) {
return uri;
}

return uri.with({
scheme: MarkdownContentProvider.scheme,
path: uri.path + '.rendered',
query: uri.toString()
});
return document.languageId === 'markdown';
}

export class MarkdownPreviewConfig {
Expand Down Expand Up @@ -137,8 +124,6 @@ export class PreviewConfigManager {
}

export class MarkdownContentProvider {
public static readonly scheme = 'markdown';

private extraStyles: Array<vscode.Uri> = [];
private extraScripts: Array<vscode.Uri> = [];

Expand Down Expand Up @@ -296,21 +281,42 @@ export class MarkdownContentProvider {
}
}

interface MarkdownPreview {
resource: vscode.Uri;
webview: vscode.Webview;
ofColumn: vscode.ViewColumn;
}

export class MarkdownPreviewWebviewManager {
private readonly webviews = new Map<string, vscode.Webview>();
private static webviewId = 'vscode-markdown-preview';

private previews: MarkdownPreview[] = [];
private readonly previewConfigurations = new PreviewConfigManager();

private readonly disposables: vscode.Disposable[] = [];

public constructor(
private readonly contentProvider: MarkdownContentProvider
) {
vscode.workspace.onDidSaveTextDocument(document => {
this.update(document.uri);
vscode.workspace.onDidChangeTextDocument(event => {
this.update(event.document, undefined);
}, null, this.disposables);

vscode.workspace.onDidChangeTextDocument(event => {
this.update(event.document.uri);
vscode.window.onDidChangeActiveEditor(editor => {
vscode.commands.executeCommand('setContext', 'markdownPreview', editor && editor.editorType === 'webview' && editor.id === MarkdownPreviewWebviewManager.webviewId);

if (editor && editor.editorType === 'texteditor') {
this.update(editor.document, editor.viewColumn);
}
}, null, this.disposables);

vscode.window.onDidCloseWebview(webview => {
if (webview.id === MarkdownPreviewWebviewManager.webviewId) {
const existing = this.previews.findIndex(preview => preview.webview === webview);
if (existing >= 0) {
this.previews.splice(existing, 1);
}
}
}, null, this.disposables);
}

Expand All @@ -321,60 +327,66 @@ export class MarkdownPreviewWebviewManager {
item.dispose();
}
}
this.webviews.clear();
this.previews = [];
}

public update(uri: vscode.Uri) {
const webview = this.webviews.get(uri.fsPath);
if (webview) {
this.contentProvider.provideTextDocumentContent(uri, this.previewConfigurations).then(x => webview.html = x);
private update(document: vscode.TextDocument, viewColumn: vscode.ViewColumn | undefined) {
if (!isMarkdownFile(document)) {
return;
}

for (const preview of this.previews) {
if (preview.resource.fsPath === document.uri.fsPath || viewColumn && preview.ofColumn === viewColumn) {
preview.webview.title = this.getPreviewTitle(document.uri);
preview.resource = document.uri;
this.contentProvider.provideTextDocumentContent(document.uri, this.previewConfigurations).then(x => preview.webview.html = x);
}
}
}

public updateAll() {
for (const resource of this.webviews.keys()) {
const sourceUri = vscode.Uri.parse(resource);
this.update(sourceUri);
public refresh() {
for (const preview of this.previews) {
this.contentProvider.provideTextDocumentContent(preview.resource, this.previewConfigurations).then(x => preview.webview.html = x);
}
}

public updateConfiguration() {
for (const resource of this.webviews.keys()) {
const sourceUri = vscode.Uri.parse(resource);
if (this.previewConfigurations.shouldUpdateConfiguration(sourceUri)) {
this.update(sourceUri);
for (const preview of this.previews) {
if (this.previewConfigurations.shouldUpdateConfiguration(preview.resource)) {
this.contentProvider.provideTextDocumentContent(preview.resource, this.previewConfigurations).then(x => preview.webview.html = x);
}
}
}

public create(
public preview(
resource: vscode.Uri,
viewColumn: vscode.ViewColumn
resourceColumn: vscode.ViewColumn,
previewColumn: vscode.ViewColumn
) {
const view = vscode.window.createWebview(
localize('previewTitle', 'Preview {0}', path.basename(resource.fsPath)),
viewColumn,
{
enableScripts: true,
localResourceRoots: this.getLocalResourceRoots(resource)
});
const webview: vscode.Webview = vscode.window.getOrCreateWebview(
MarkdownPreviewWebviewManager.webviewId,
previewColumn);

this.contentProvider.provideTextDocumentContent(resource, this.previewConfigurations).then(x => view.html = x);

view.onMessage(e => {
vscode.commands.executeCommand(e.command, ...e.args);
});
webview.options = {
enableScripts: true,
localResourceRoots: this.getLocalResourceRoots(resource)
};
webview.title = this.getPreviewTitle(resource);

const existing = this.previews.find(preview => preview.webview.viewColumn === webview.viewColumn);
if (existing) {
existing.resource = resource;
} else {
webview.onMessage(e => {
vscode.commands.executeCommand(e.command, ...e.args);
});

view.onBecameActive(() => {
vscode.commands.executeCommand('setContext', 'markdownPreview', true);
});
this.previews.push({ webview, resource, ofColumn: resourceColumn });
}

view.onBecameInactive(() => {
vscode.commands.executeCommand('setContext', 'markdownPreview', false);
});
this.contentProvider.provideTextDocumentContent(resource, this.previewConfigurations).then(x => webview.html = x);

this.webviews.set(resource.fsPath, view);
return view;
return webview;
}

private getLocalResourceRoots(
Expand All @@ -391,4 +403,8 @@ export class MarkdownPreviewWebviewManager {

return [];
}

private getPreviewTitle(resource: vscode.Uri): string {
return localize('previewTitle', 'Preview {0}', path.basename(resource.fsPath));
}
}
11 changes: 4 additions & 7 deletions extensions/markdown/src/security.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import * as vscode from 'vscode';

import { getMarkdownUri, MarkdownPreviewWebviewManager } from './features/previewContentProvider';
import { MarkdownPreviewWebviewManager } from './features/previewContentProvider';

import * as nls from 'vscode-nls';

Expand Down Expand Up @@ -143,15 +143,12 @@ export class PreviewSecuritySelector {
return;
}

const sourceUri = getMarkdownUri(resource);
if (selection.type === 'toggle') {
this.cspArbiter.setShouldDisableSecurityWarning(!this.cspArbiter.shouldDisableSecurityWarnings());
this.webviewManager.update(sourceUri);
return;
} else {
await this.cspArbiter.setSecurityLevelForResource(resource, selection.type);
}

await this.cspArbiter.setSecurityLevelForResource(resource, selection.type);

this.webviewManager.update(sourceUri);
this.webviewManager.refresh();
}
}
4 changes: 4 additions & 0 deletions src/vs/vscode.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1028,6 +1028,10 @@ declare module 'vscode' {
* Represents an editor that is attached to a [document](#TextDocument).
*/
export interface TextEditor {
/**
* Type identifying the editor as a text editor.
*/
readonly editorType: 'texteditor';

/**
* The document associated with this text editor. The document will be the same for the entire lifetime of this text editor.
Expand Down
37 changes: 23 additions & 14 deletions src/vs/vscode.proposed.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,17 @@ declare module 'vscode' {
*/
export interface Webview {
/**
* Title of the webview.
* Type identifying the editor as a webview editor.
*/
readonly editorType: 'webview';

/**
* Unique internal identifer of the webview.
*/
readonly id: string;

/**
* Title of the webview shown in UI.
*/
title: string;

Expand All @@ -463,16 +473,6 @@ declare module 'vscode' {
*/
readonly onMessage: Event<any>;

/**
* Fired when the webview becomes the active editor.
*/
readonly onBecameActive: Event<void>;

/**
* Fired when the webview stops being the active editor
*/
readonly onBecameInactive: Event<void>;

/**
* Post a message to the webview content.
*
Expand All @@ -492,10 +492,19 @@ declare module 'vscode' {
/**
* Create and show a new webview.
*
* @param title Title of the webview.
* @param id Unique identifier for the webview.
* @param column Editor column to show the new webview in.
* @param options Webview content options.
*/
export function createWebview(title: string, column: ViewColumn, options: WebviewOptions): Webview;
export function getOrCreateWebview(id: string, column: ViewColumn): Webview;

/**
* Event fired when the active editor changes.
*/
export const onDidChangeActiveEditor: Event<TextEditor | Webview | undefined>;

/**
* Event fired when a webview is closed.
*/
export const onDidCloseWebview: Event<Webview>;
}
}
Loading

0 comments on commit d405242

Please sign in to comment.