diff --git a/extensions/markdown/media/loading.js b/extensions/markdown/media/loading.js index 7889d432ef7..720ff7fb204 100644 --- a/extensions/markdown/media/loading.js +++ b/extensions/markdown/media/loading.js @@ -31,6 +31,6 @@ window.parent.postMessage({ command: 'did-click-link', data: `command:_markdown.onPreviewStyleLoadError?${encodeURIComponent(JSON.stringify(args))}` - }, 'file://'); + }, '*'); }); }()); \ No newline at end of file diff --git a/extensions/markdown/media/main.js b/extensions/markdown/media/main.js index 1d9bf7a3608..53ac317d851 100644 --- a/extensions/markdown/media/main.js +++ b/extensions/markdown/media/main.js @@ -35,9 +35,9 @@ */ function postMessage(command, args) { window.parent.postMessage({ - command: 'did-click-link', - data: `command:${command}?${encodeURIComponent(JSON.stringify(args))}` - }, 'file://'); + command, + args + }, '*'); } /** diff --git a/extensions/markdown/package.json b/extensions/markdown/package.json index 5f019bc94ec..99ce9bedd46 100644 --- a/extensions/markdown/package.json +++ b/extensions/markdown/package.json @@ -2,6 +2,7 @@ "name": "vscode-markdown", "displayName": "VS Code Markdown", "description": "Markdown for VS Code", + "enableProposedApi": true, "version": "0.2.0", "publisher": "Microsoft", "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", @@ -138,17 +139,17 @@ }, { "command": "markdown.showSource", - "when": "resourceScheme == markdown", + "when": "markdownPreview", "group": "navigation" }, { "command": "markdown.refreshPreview", - "when": "resourceScheme == markdown", + "when": "markdownPreview", "group": "1_markdown" }, { "command": "markdown.showPreviewSecuritySelector", - "when": "resourceScheme == markdown", + "when": "markdownPreview", "group": "1_markdown" } ], @@ -172,7 +173,7 @@ }, { "command": "markdown.showSource", - "when": "resourceScheme == markdown", + "when": "markdownPreview", "group": "navigation" }, { @@ -181,7 +182,7 @@ }, { "command": "markdown.showPreviewSecuritySelector", - "when": "resourceScheme == markdown" + "when": "markdownPreview" } ] }, diff --git a/extensions/markdown/src/commands/refreshPreview.ts b/extensions/markdown/src/commands/refreshPreview.ts index 93cc0fcf761..107e565da28 100644 --- a/extensions/markdown/src/commands/refreshPreview.ts +++ b/extensions/markdown/src/commands/refreshPreview.ts @@ -5,28 +5,23 @@ import * as vscode from 'vscode'; import { Command } from '../commandManager'; -import { isMarkdownFile, MDDocumentContentProvider, getMarkdownUri } from '../features/previewContentProvider'; +import { isMarkdownFile, getMarkdownUri, MarkdownPreviewWebviewManager } from '../features/previewContentProvider'; export class RefreshPreviewCommand implements Command { public readonly id = 'markdown.refreshPreview'; public constructor( - private readonly contentProvider: MDDocumentContentProvider + private readonly webviewManager: MarkdownPreviewWebviewManager ) { } public execute(resource: string | undefined) { if (resource) { const source = vscode.Uri.parse(resource); - this.contentProvider.update(source); + this.webviewManager.update(source); } else if (vscode.window.activeTextEditor && isMarkdownFile(vscode.window.activeTextEditor.document)) { - this.contentProvider.update(getMarkdownUri(vscode.window.activeTextEditor.document.uri)); + this.webviewManager.update(getMarkdownUri(vscode.window.activeTextEditor.document.uri)); } else { - // update all generated md documents - for (const document of vscode.workspace.textDocuments) { - if (document.uri.scheme === MDDocumentContentProvider.scheme) { - this.contentProvider.update(document.uri); - } - } + this.webviewManager.updateAll(); } } } \ No newline at end of file diff --git a/extensions/markdown/src/commands/showPreview.ts b/extensions/markdown/src/commands/showPreview.ts index e3cb65f0a3f..5383c686496 100644 --- a/extensions/markdown/src/commands/showPreview.ts +++ b/extensions/markdown/src/commands/showPreview.ts @@ -3,15 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vscode-nls'; -const localize = nls.loadMessageBundle(); - import * as vscode from 'vscode'; -import * as path from 'path'; import { Command } from '../commandManager'; -import { ExtensionContentSecurityPolicyArbiter } from '../security'; -import { getMarkdownUri, } from '../features/previewContentProvider'; +import { MarkdownPreviewWebviewManager, } from '../features/previewContentProvider'; import { TelemetryReporter } from '../telemetryReporter'; @@ -36,7 +31,7 @@ function getViewColumn(sideBySide: boolean): vscode.ViewColumn | undefined { } function showPreview( - cspArbiter: ExtensionContentSecurityPolicyArbiter, + webviewManager: MarkdownPreviewWebviewManager, telemetryReporter: TelemetryReporter, uri?: vscode.Uri, sideBySide: boolean = false, @@ -58,34 +53,29 @@ function showPreview( return; } - const thenable = vscode.commands.executeCommand('vscode.previewHtml', - getMarkdownUri(resource), - getViewColumn(sideBySide), - localize('previewTitle', 'Preview {0}', path.basename(resource.fsPath)), - { - allowScripts: true, - allowSvgs: cspArbiter.shouldAllowSvgsForResource(resource) - }); + const view = webviewManager.create( + resource, + getViewColumn(sideBySide) || vscode.ViewColumn.Active); telemetryReporter.sendTelemetryEvent('openPreview', { where: sideBySide ? 'sideBySide' : 'inPlace', how: (uri instanceof vscode.Uri) ? 'action' : 'pallete' }); - return thenable; + return view; } export class ShowPreviewCommand implements Command { public readonly id = 'markdown.showPreview'; public constructor( - private readonly cspArbiter: ExtensionContentSecurityPolicyArbiter, + private readonly webviewManager: MarkdownPreviewWebviewManager, private readonly telemetryReporter: TelemetryReporter ) { } public execute(mainUri?: vscode.Uri, allUris?: vscode.Uri[]) { for (const uri of (allUris || [mainUri])) { - showPreview(this.cspArbiter, this.telemetryReporter, uri, false); + showPreview(this.webviewManager, this.telemetryReporter, uri, false); } } } @@ -94,11 +84,11 @@ export class ShowPreviewToSideCommand implements Command { public readonly id = 'markdown.showPreviewToSide'; public constructor( - private readonly cspArbiter: ExtensionContentSecurityPolicyArbiter, + private readonly webviewManager: MarkdownPreviewWebviewManager, private readonly telemetryReporter: TelemetryReporter ) { } public execute(uri?: vscode.Uri) { - showPreview(this.cspArbiter, this.telemetryReporter, uri, true); + showPreview(this.webviewManager, this.telemetryReporter, uri, true); } } diff --git a/extensions/markdown/src/extension.ts b/extensions/markdown/src/extension.ts index 2c64c561fe9..524bbe3fcd1 100644 --- a/extensions/markdown/src/extension.ts +++ b/extensions/markdown/src/extension.ts @@ -14,7 +14,7 @@ import { loadDefaultTelemetryReporter } from './telemetryReporter'; import { loadMarkdownExtensions } from './markdownExtensions'; import LinkProvider from './features/documentLinkProvider'; import MDDocumentSymbolProvider from './features/documentSymbolProvider'; -import { MDDocumentContentProvider, getMarkdownUri, isMarkdownFile } from './features/previewContentProvider'; +import { MarkdownContentProvider, getMarkdownUri, isMarkdownFile, MarkdownPreviewWebviewManager } from './features/previewContentProvider'; export function activate(context: vscode.ExtensionContext) { @@ -27,22 +27,23 @@ export function activate(context: vscode.ExtensionContext) { const selector = 'markdown'; - const contentProvider = new MDDocumentContentProvider(engine, context, cspArbiter, logger); - context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(MDDocumentContentProvider.scheme, contentProvider)); - + const contentProvider = new MarkdownContentProvider(engine, context, cspArbiter, logger); loadMarkdownExtensions(contentProvider, engine); + const webviewManager = new MarkdownPreviewWebviewManager(contentProvider); + context.subscriptions.push(webviewManager); + context.subscriptions.push(vscode.languages.registerDocumentSymbolProvider(selector, new MDDocumentSymbolProvider(engine))); context.subscriptions.push(vscode.languages.registerDocumentLinkProvider(selector, new LinkProvider())); - const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, contentProvider); + const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, webviewManager); const commandManager = new CommandManager(); context.subscriptions.push(commandManager); - commandManager.register(new commands.ShowPreviewCommand(cspArbiter, telemetryReporter)); - commandManager.register(new commands.ShowPreviewToSideCommand(cspArbiter, telemetryReporter)); + commandManager.register(new commands.ShowPreviewCommand(webviewManager, telemetryReporter)); + commandManager.register(new commands.ShowPreviewToSideCommand(webviewManager, telemetryReporter)); commandManager.register(new commands.ShowSourceCommand()); - commandManager.register(new commands.RefreshPreviewCommand(contentProvider)); + commandManager.register(new commands.RefreshPreviewCommand(webviewManager)); commandManager.register(new commands.RevealLineCommand(logger)); commandManager.register(new commands.MoveCursorToPositionCommand()); commandManager.register(new commands.ShowPreviewSecuritySelectorCommand(previewSecuritySelector)); @@ -50,23 +51,9 @@ export function activate(context: vscode.ExtensionContext) { commandManager.register(new commands.DidClickCommand()); commandManager.register(new commands.OpenDocumentLinkCommand(engine)); - context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(document => { - if (isMarkdownFile(document)) { - const uri = getMarkdownUri(document.uri); - contentProvider.update(uri); - } - })); - - context.subscriptions.push(vscode.workspace.onDidChangeTextDocument(event => { - if (isMarkdownFile(event.document)) { - const uri = getMarkdownUri(event.document.uri); - contentProvider.update(uri); - } - })); - context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => { logger.updateConfiguration(); - contentProvider.updateConfiguration(); + webviewManager.updateConfiguration(); })); context.subscriptions.push(vscode.window.onDidChangeTextEditorSelection(event => { diff --git a/extensions/markdown/src/features/previewContentProvider.ts b/extensions/markdown/src/features/previewContentProvider.ts index c2d984b7d74..a0356d51ce4 100644 --- a/extensions/markdown/src/features/previewContentProvider.ts +++ b/extensions/markdown/src/features/previewContentProvider.ts @@ -20,22 +20,22 @@ const previewStrings = { export function isMarkdownFile(document: vscode.TextDocument) { return document.languageId === 'markdown' - && document.uri.scheme !== MDDocumentContentProvider.scheme; // prevent processing of own documents + && document.uri.scheme !== MarkdownContentProvider.scheme; // prevent processing of own documents } export function getMarkdownUri(uri: vscode.Uri) { - if (uri.scheme === MDDocumentContentProvider.scheme) { + if (uri.scheme === MarkdownContentProvider.scheme) { return uri; } return uri.with({ - scheme: MDDocumentContentProvider.scheme, + scheme: MarkdownContentProvider.scheme, path: uri.path + '.rendered', query: uri.toString() }); } -class MarkdownPreviewConfig { +export class MarkdownPreviewConfig { public static getConfigForResource(resource: vscode.Uri) { return new MarkdownPreviewConfig(resource); } @@ -105,7 +105,7 @@ class MarkdownPreviewConfig { [key: string]: any; } -class PreviewConfigManager { +export class PreviewConfigManager { private previewConfigurationsForWorkspaces = new Map(); public loadAndCacheConfiguration( @@ -136,13 +136,9 @@ class PreviewConfigManager { } } -export class MDDocumentContentProvider implements vscode.TextDocumentContentProvider { +export class MarkdownContentProvider { public static readonly scheme = 'markdown'; - private _onDidChange = new vscode.EventEmitter(); - private _waiting: boolean = false; - private previewConfigurations = new PreviewConfigManager(); - private extraStyles: Array = []; private extraScripts: Array = []; @@ -162,7 +158,9 @@ export class MDDocumentContentProvider implements vscode.TextDocumentContentProv } private getMediaPath(mediaFile: string): string { - return vscode.Uri.file(this.context.asAbsolutePath(path.join('media', mediaFile))).toString(); + return vscode.Uri.file(this.context.asAbsolutePath(path.join('media', mediaFile))) + .with({ scheme: 'vscode-extension-resource' }) + .toString(); } private fixHref(resource: vscode.Uri, href: string): string { @@ -172,23 +170,29 @@ export class MDDocumentContentProvider implements vscode.TextDocumentContentProv // Use href if it is already an URL const hrefUri = vscode.Uri.parse(href); - if (['file', 'http', 'https'].indexOf(hrefUri.scheme) >= 0) { + if (['http', 'https'].indexOf(hrefUri.scheme) >= 0) { return hrefUri.toString(); } // Use href as file URI if it is absolute - if (path.isAbsolute(href)) { - return vscode.Uri.file(href).toString(); + if (path.isAbsolute(href) || hrefUri.scheme === 'file') { + return vscode.Uri.file(href) + .with({ scheme: 'vscode-workspace-resource' }) + .toString(); } // use a workspace relative path if there is a workspace let root = vscode.workspace.getWorkspaceFolder(resource); if (root) { - return vscode.Uri.file(path.join(root.uri.fsPath, href)).toString(); + return vscode.Uri.file(path.join(root.uri.fsPath, href)) + .with({ scheme: 'vscode-workspace-resource' }) + .toString(); } // otherwise look relative to the markdown file - return vscode.Uri.file(path.join(path.dirname(resource.fsPath), href)).toString(); + return vscode.Uri.file(path.join(path.dirname(resource.fsPath), href)) + .with({ scheme: 'vscode-workspace-resource' }) + .toString(); } private computeCustomStyleSheetIncludes(resource: vscode.Uri, config: MarkdownPreviewConfig): string { @@ -228,9 +232,10 @@ export class MDDocumentContentProvider implements vscode.TextDocumentContentProv .join('\n'); } - public async provideTextDocumentContent(uri: vscode.Uri): Promise { - const sourceUri = vscode.Uri.parse(uri.query); - + public async provideTextDocumentContent( + sourceUri: vscode.Uri, + previewConfigurations: PreviewConfigManager + ): Promise { let initialLine: number | undefined = undefined; const editor = vscode.window.activeTextEditor; if (editor && editor.document.uri.toString() === sourceUri.toString()) { @@ -238,10 +243,10 @@ export class MDDocumentContentProvider implements vscode.TextDocumentContentProv } const document = await vscode.workspace.openTextDocument(sourceUri); - const config = this.previewConfigurations.loadAndCacheConfiguration(sourceUri); + const config = previewConfigurations.loadAndCacheConfiguration(sourceUri); const initialData = { - previewUri: uri.toString(), + previewUri: sourceUri.toString(), // TODO source: sourceUri.toString(), line: initialLine, scrollPreviewWithEditorSelection: config.scrollPreviewWithEditorSelection, @@ -266,7 +271,7 @@ export class MDDocumentContentProvider implements vscode.TextDocumentContentProv ${this.getStyles(sourceUri, nonce, config)} - + ${body} @@ -276,43 +281,96 @@ export class MDDocumentContentProvider implements vscode.TextDocumentContentProv `; } - public updateConfiguration() { - // update all generated md documents - for (const document of vscode.workspace.textDocuments) { - if (document.uri.scheme === MDDocumentContentProvider.scheme) { - const sourceUri = vscode.Uri.parse(document.uri.query); - if (this.previewConfigurations.shouldUpdateConfiguration(sourceUri)) { - this.update(document.uri); - } - } - } - } - - get onDidChange(): vscode.Event { - return this._onDidChange.event; - } - - public update(uri: vscode.Uri) { - if (!this._waiting) { - this._waiting = true; - setTimeout(() => { - this._waiting = false; - this._onDidChange.fire(uri); - }, 300); - } - } - private getCspForResource(resource: vscode.Uri, nonce: string): string { switch (this.cspArbiter.getSecurityLevelForResource(resource)) { case MarkdownPreviewSecurityLevel.AllowInsecureContent: - return ``; + return ``; case MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent: return ''; case MarkdownPreviewSecurityLevel.Strict: default: - return ``; + return ``; } } } + +export class MarkdownPreviewWebviewManager { + private readonly webviews = new Map(); + private readonly previewConfigurations = new PreviewConfigManager(); + + private readonly disposables: vscode.Disposable[] = []; + + public constructor( + private readonly contentProvider: MarkdownContentProvider + ) { + vscode.workspace.onDidSaveTextDocument(document => { + this.update(document.uri); + }, null, this.disposables); + + vscode.workspace.onDidChangeTextDocument(event => { + this.update(event.document.uri); + }, null, this.disposables); + } + + public dispose(): void { + while (this.disposables.length) { + const item = this.disposables.pop(); + if (item) { + item.dispose(); + } + } + this.webviews.clear(); + } + + 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); + } + } + + public updateAll() { + for (const resource of this.webviews.keys()) { + const sourceUri = vscode.Uri.parse(resource); + this.update(sourceUri); + } + } + + public updateConfiguration() { + for (const resource of this.webviews.keys()) { + const sourceUri = vscode.Uri.parse(resource); + if (this.previewConfigurations.shouldUpdateConfiguration(sourceUri)) { + this.update(sourceUri); + } + } + } + + public create( + resource: vscode.Uri, + viewColumn: vscode.ViewColumn + ) { + const view = vscode.window.createWebview( + localize('previewTitle', 'Preview {0}', path.basename(resource.fsPath)), + viewColumn, + { enableScripts: true }); + + this.contentProvider.provideTextDocumentContent(resource, this.previewConfigurations).then(x => view.html = x); + + view.onMessage(e => { + vscode.commands.executeCommand(e.command, ...e.args); + }); + + view.onBecameActive(() => { + vscode.commands.executeCommand('setContext', 'markdownPreview', true); + }); + + view.onBecameInactive(() => { + vscode.commands.executeCommand('setContext', 'markdownPreview', false); + }); + + this.webviews.set(resource.fsPath, view); + return view; + } +} \ No newline at end of file diff --git a/extensions/markdown/src/markdownEngine.ts b/extensions/markdown/src/markdownEngine.ts index 8650479e524..44fe586602c 100644 --- a/extensions/markdown/src/markdownEngine.ts +++ b/extensions/markdown/src/markdownEngine.ts @@ -152,7 +152,7 @@ export class MarkdownEngine { if (fragment) { uri = uri.with({ fragment }); } - return normalizeLink(uri.toString(true)); + return normalizeLink(uri.with({ scheme: 'vscode-workspace-resource' }).toString(true)); } } catch (e) { // noop diff --git a/extensions/markdown/src/markdownExtensions.ts b/extensions/markdown/src/markdownExtensions.ts index 4504cf3dfe0..6bba131e107 100644 --- a/extensions/markdown/src/markdownExtensions.ts +++ b/extensions/markdown/src/markdownExtensions.ts @@ -6,20 +6,17 @@ import * as vscode from 'vscode'; import * as path from 'path'; -import { MDDocumentContentProvider } from './features/previewContentProvider'; +import { MarkdownContentProvider } from './features/previewContentProvider'; import { MarkdownEngine } from './markdownEngine'; -const resolveExtensionResources = (extension: vscode.Extension, stylePath: string): vscode.Uri => { - const resource = vscode.Uri.parse(stylePath); - if (resource.scheme) { - return resource; - } - return vscode.Uri.file(path.join(extension.extensionPath, stylePath)); +const resolveExtensionResources = (extension: vscode.Extension, resourcePath: string): vscode.Uri => { + return vscode.Uri.parse(path.join(extension.extensionPath, resourcePath)) + .with({ scheme: 'vscode-extension-resource' }); }; export function loadMarkdownExtensions( - contentProvider: MDDocumentContentProvider, + contentProvider: MarkdownContentProvider, engine: MarkdownEngine ) { for (const extension of vscode.extensions.all) { @@ -50,7 +47,7 @@ function tryLoadMarkdownItPlugins( function tryLoadPreviewScripts( contributes: any, - contentProvider: MDDocumentContentProvider, + contentProvider: MarkdownContentProvider, extension: vscode.Extension ) { const scripts = contributes['markdown.previewScripts']; @@ -67,7 +64,7 @@ function tryLoadPreviewScripts( function tryLoadPreviewStyles( contributes: any, - contentProvider: MDDocumentContentProvider, + contentProvider: MarkdownContentProvider, extension: vscode.Extension ) { const styles = contributes['markdown.previewStyles']; diff --git a/extensions/markdown/src/security.ts b/extensions/markdown/src/security.ts index c4625250af0..5978a9102b9 100644 --- a/extensions/markdown/src/security.ts +++ b/extensions/markdown/src/security.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; -import { getMarkdownUri, MDDocumentContentProvider } from './features/previewContentProvider'; +import { getMarkdownUri, MarkdownPreviewWebviewManager } from './features/previewContentProvider'; import * as nls from 'vscode-nls'; @@ -90,7 +90,7 @@ export class PreviewSecuritySelector { public constructor( private cspArbiter: ContentSecurityPolicyArbiter, - private contentProvider: MDDocumentContentProvider + private webviewManager: MarkdownPreviewWebviewManager ) { } public async showSecutitySelectorForResource(resource: vscode.Uri): Promise { @@ -146,19 +146,12 @@ export class PreviewSecuritySelector { const sourceUri = getMarkdownUri(resource); if (selection.type === 'toggle') { this.cspArbiter.setShouldDisableSecurityWarning(!this.cspArbiter.shouldDisableSecurityWarnings()); - this.contentProvider.update(sourceUri); + this.webviewManager.update(sourceUri); return; } await this.cspArbiter.setSecurityLevelForResource(resource, selection.type); - await vscode.commands.executeCommand('_workbench.htmlPreview.updateOptions', - sourceUri, - { - allowScripts: true, - allowSvgs: this.cspArbiter.shouldAllowSvgsForResource(resource) - }); - - this.contentProvider.update(sourceUri); + this.webviewManager.update(sourceUri); } } diff --git a/extensions/markdown/src/typings/ref.d.ts b/extensions/markdown/src/typings/ref.d.ts index bc057c55878..954bab971e3 100644 --- a/extensions/markdown/src/typings/ref.d.ts +++ b/extensions/markdown/src/typings/ref.d.ts @@ -4,4 +4,5 @@ *--------------------------------------------------------------------------------------------*/ /// +/// /// diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 757a2d14339..a13e9b98405 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -437,7 +437,6 @@ declare module 'vscode' { } export namespace languages { - export interface RenameProvider2 extends RenameProvider { resolveInitialRenameValue?(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; } @@ -488,4 +487,98 @@ declare module 'vscode' { */ validateInput?(value: string, cursorPosition: number): ProviderResult; } + + /** + * Content settings for a webview. + */ + export interface WebviewOptions { + /** + * Should scripts be enabled in the webview? + * + * Defaults to false (scripts-disabled). + */ + readonly enableScripts?: boolean; + + /** + * Should command uris be enabled? + * + * Defaults to false. + */ + readonly enableCommandUris?: boolean; + + /** + * Should the webview content be kept arount even when the webview is no longer visible? + * + * Normally a webview content is created when the webview becomes visible + * and destroyed when the webview is hidden. Apps that have complex state + * or UI can set the `keepAlive` property to make VS Code keep the webview + * content around, even when the webview itself is no longer visible. When + * the webview becomes visible again, the content is automatically restored + * in the exact same state it was in originally + * + * `keepAlive` has a high memory overhead and should only be used if your + * webview content cannot be quickly saved and restored. + */ + readonly keepAlive?: boolean; + } + + /** + * A webview is an editor with html content, like an iframe. + */ + export interface Webview { + /** + * Title of the webview. + */ + title: string; + + /** + * Contents of the webview. + */ + html: string; + + /** + * Content settings for the webview. + */ + options: WebviewOptions; + + /** + * Fired when the webview content posts a message. + */ + readonly onMessage: Event; + + /** + * Fired when the webview becomes the active editor. + */ + readonly onBecameActive: Event; + + /** + * Fired when the webview stops being the active editor + */ + readonly onBecameInactive: Event; + + /** + * Post a message to the webview content. + * + * Messages are only develivered if the webview is visible. + * + * @param message Body of the message. + */ + postMessage(message: any): Thenable; + + /** + * Dispose the webview. + */ + dispose(): any; + } + + namespace window { + /** + * Create and show a new webview. + * + * @param title Title of 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; + } } diff --git a/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts b/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts index d610ada2b88..93091f5ba0d 100644 --- a/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts @@ -47,6 +47,7 @@ import './mainThreadTelemetry'; import './mainThreadTerminalService'; import './mainThreadTreeViews'; import './mainThreadLogService'; +import './mainThreadWebview'; import './mainThreadWindow'; import './mainThreadWorkspace'; diff --git a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts new file mode 100644 index 00000000000..ae51d1a4655 --- /dev/null +++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts @@ -0,0 +1,376 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as map from 'vs/base/common/map'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { MainThreadWebviewShape, MainContext, IExtHostContext, ExtHostContext, ExtHostWebviewsShape } from 'vs/workbench/api/node/extHost.protocol'; +import { IDisposable, dispose, toDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { extHostNamedCustomer } from './extHostCustomers'; +import { EditorInput, EditorModel, EditorOptions } from 'vs/workbench/common/editor'; +import { IEditorModel, Position } from 'vs/platform/editor/common/editor'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { WebviewEditor as BaseWebviewEditor, KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS, KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE } from 'vs/workbench/parts/html/browser/webviewEditor'; +import { Builder, Dimension } from 'vs/base/browser/builder'; +import WebView from 'vs/workbench/parts/html/browser/webview'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { localize } from 'vs/nls'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import * as vscode from 'vscode'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import URI from 'vs/base/common/uri'; + +interface WebviewEvents { + onMessage(message: any): void; + onFocus(): void; + onBlur(): void; +} + +class WebviewInput extends EditorInput { + private _name: string; + private _options: vscode.WebviewOptions; + private _html: string; + + constructor( + name: string, + options: vscode.WebviewOptions, + html: string, + public readonly events: WebviewEvents + ) { + super(); + this._name = name; + this._options = options; + this._html = html; + } + + public getName(): string { + return this._name; + } + + public setName(value: string): void { + this._name = value; + this._onDidChangeLabel.fire(); + } + + public get html(): string { + return this._html; + } + + public set html(value: string) { + this._html = value; + } + + public get options(): vscode.WebviewOptions { + return this._options; + } + + public set options(value: vscode.WebviewOptions) { + this._options = value; + } + + public getTypeId(): string { + return 'webview'; + } + + public resolve(refresh?: boolean): TPromise { + return TPromise.as(new EditorModel()); + } +} + +class WebviewEditor extends BaseWebviewEditor { + private static webviewIndex = 0; + + public static readonly ID = 'WebviewEditor'; + + private static readonly standardSupportedLinkSchemes = ['http', 'https', 'mailto']; + + private frame: HTMLElement; + private container: HTMLElement; + private webviewContent: HTMLDivElement; + + private _contentDisposables: IDisposable[] = []; + + constructor( + @ITelemetryService telemetryService: ITelemetryService, + @IStorageService storageService: IStorageService, + @IContextKeyService private _contextKeyService: IContextKeyService, + @IThemeService themeService: IThemeService, + @IPartService private readonly _partService: IPartService, + @IContextViewService private readonly _contextViewService: IContextViewService, + @IEnvironmentService private readonly _environmentService: IEnvironmentService, + @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, + @IOpenerService private readonly _openerService: IOpenerService + ) { + super(WebviewEditor.ID, telemetryService, themeService, storageService, _contextKeyService); + } + + protected createEditor(parent: Builder): void { + this.frame = parent.getHTMLElement(); + this.container = this._partService.getContainer(Parts.EDITOR_PART); + + this.webviewContent = document.createElement('div'); + this.webviewContent.id = `webview-${WebviewEditor.webviewIndex++}`; + this._contextKeyService = this._contextKeyService.createScoped(this.webviewContent); + this.contextKey = KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS.bindTo(this._contextKeyService); + this.findInputFocusContextKey = KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED.bindTo(this._contextKeyService); + this.findWidgetVisible = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE.bindTo(this._contextKeyService); + + this.container.appendChild(this.webviewContent); + + this.content = document.createElement('div'); + this.content.setAttribute('aria-flowto', this.webviewContent.id); + + parent.append(this.content); + this.doUpdateContainer(); + } + + private doUpdateContainer() { + const frameRect = this.frame.getBoundingClientRect(); + const containerRect = this.container.getBoundingClientRect(); + + this.webviewContent.style.position = 'absolute'; + this.webviewContent.style.top = `${frameRect.top - containerRect.top}px`; + this.webviewContent.style.left = `${frameRect.left - containerRect.left}px`; + this.webviewContent.style.width = `${frameRect.width}px`; + this.webviewContent.style.height = `${frameRect.height}px`; + } + + public layout(dimension: Dimension): void { + if (this._webview) { + this.doUpdateContainer(); + this._webview.layout(); + } + } + + public focus() { + if (this._webview) { + this._webview.focus(); + } + } + + public dispose(): void { + this._contentDisposables = dispose(this._contentDisposables); + super.dispose(); + } + + public sendMessage(data: any): void { + if (this._webview) { + this._webview.sendMessage(data); + } + } + + public getFocusContainer(): Builder { + return new Builder(this.webviewContent, false); + } + + protected setEditorVisible(visible: boolean, position?: Position): void { + if (visible) { + this.webviewContent.style.visibility = 'visible'; + this.doUpdateContainer(); + } else { + if (this._webview) { + this.webviewContent.style.visibility = 'hidden'; + } + } + super.setEditorVisible(visible, position); + } + + public clearInput(): void { + if (this.input && this.input instanceof WebviewInput) { + if (this.input.options.keepAlive) { + // Noop + return; + } + } + super.clearInput(); + } + + async setInput(input: WebviewInput, options: EditorOptions): TPromise { + if (this.input && this.input.matches(input)) { + return undefined; + } + + await super.setInput(input, options); + + this.webview.options = { + allowScripts: input.options.enableScripts, + enableWrappedPostMessage: true + }; + this.webview.contents = [input.html]; + this.webview.style(this.themeService.getTheme()); + } + + private get webview(): WebView { + if (!this._webview) { + this._contentDisposables = dispose(this._contentDisposables); + + this._webview = new WebView( + this.webviewContent, + this._partService.getContainer(Parts.EDITOR_PART), + this._environmentService, + this._contextService, + this._contextViewService, + this.contextKey, + this.findInputFocusContextKey, + { + enableWrappedPostMessage: true + }, + false); + this.webview.style(this.themeService.getTheme()); + + this._webview.onDidClickLink(this.onDidClickLink, this, this._contentDisposables); + + + this.themeService.onThemeChange(theme => { + if (this._webview) { + this._webview.style(theme); + } + }, null, this._contentDisposables); + + this._webview.onMessage(message => { + if (this.input) { + (this.input as WebviewInput).events.onMessage(message); + } + }, null, this._contentDisposables); + + this._webview.onFocus(() => { + if (this.input) { + (this.input as WebviewInput).events.onFocus(); + } + }, this, this._contentDisposables); + + this._webview.onBlur(() => { + if (this.input) { + (this.input as WebviewInput).events.onBlur(); + } + }, this, this._contentDisposables); + + this._contentDisposables.push(this._webview); + this._contentDisposables.push(toDisposable(() => this._webview = null)); + } + return this._webview; + } + + private onDidClickLink(link: URI): void { + if (!link) { + return; + } + + const enableCommandUris = (this.input as WebviewInput).options.enableCommandUris; + if (WebviewEditor.standardSupportedLinkSchemes.indexOf(link.scheme) >= 0 || enableCommandUris && link.scheme === 'command') { + this._openerService.open(link); + } + } +} + + +@extHostNamedCustomer(MainContext.MainThreadWebview) +export class MainThreadWebview implements MainThreadWebviewShape { + private readonly _toDispose: Disposable[] = []; + + private readonly _proxy: ExtHostWebviewsShape; + private readonly _webviews = new Map(); + // private _activeWebview: WebviewInput | undefined; + + constructor( + context: IExtHostContext, + @IEditorGroupService _editorGroupService: IEditorGroupService, + @IWorkbenchEditorService private readonly _editorService: IWorkbenchEditorService, + @IInstantiationService private readonly _instantiationService: IInstantiationService + ) { + this._proxy = context.getProxy(ExtHostContext.ExtHostWebviews); + + _editorGroupService.onEditorsChanged(() => this.onEditorsChanged, null, this._toDispose); + } + + dispose(): void { + dispose(this._toDispose); + } + + $createWebview(handle: number): void { + const webview = new WebviewInput('', {}, '', { + onMessage: (message) => this._proxy.$onMessage(handle, message), + onFocus: () => this._proxy.$onBecameActive(handle), + onBlur: () => this._proxy.$onBecameInactive(handle) + }); + this._webviews.set(handle, webview); + } + + $disposeWebview(handle: number): void { + const webview = this._webviews.get(handle); + this._editorService.closeEditor(Position.ONE, webview); + } + + $setTitle(handle: number, value: string): void { + const webview = this._webviews.get(handle); + webview.setName(value); + } + + $setHtml(handle: number, value: string): void { + this.updateInput(handle, existingInput => + this._instantiationService.createInstance(WebviewInput, existingInput.getName(), existingInput.options, value, existingInput.events)); + } + + $setOptions(handle: number, newOptions: vscode.WebviewOptions): void { + this.updateInput(handle, existingInput => + this._instantiationService.createInstance(WebviewInput, existingInput.getName(), newOptions, existingInput.html, existingInput.events)); + } + + $show(handle: number, column: Position): void { + const webviewInput = this._webviews.get(handle); + this._editorService.openEditor(webviewInput, { pinned: true }, column); + } + + async $sendMessage(handle: number, message: any): Promise { + const webviewInput = this._webviews.get(handle); + const editors = this._editorService.getVisibleEditors() + .filter(e => e instanceof WebviewInput) + .map(e => e as WebviewEditor) + .filter(e => e.input.matches(webviewInput)); + + for (const editor of editors) { + editor.sendMessage(message); + } + + return (editors.length > 0); + } + + private updateInput(handle: number, f: (existingInput: WebviewInput) => WebviewInput) { + const existingInput = this._webviews.get(handle); + const newInput = f(existingInput); + this._webviews.set(handle, newInput); + this._editorService.replaceEditors([{ toReplace: existingInput, replaceWith: newInput }]); + } + + private onEditorsChanged() { + const activeEditor = this._editorService.getActiveEditor(); + if (activeEditor.input instanceof WebviewInput) { + // this._activeWebview = activeEditor.input; + for (const handle of map.keys(this._webviews)) { + const input = this._webviews.get(handle); + if (input.matches(activeEditor.input)) { + + } + } + } + } +} + +(Registry.as(EditorExtensions.Editors)).registerEditor(new EditorDescriptor( + WebviewEditor, + WebviewEditor.ID, + localize('webview.editor.label', "webview editor")), + [new SyncDescriptor(WebviewInput)]); \ No newline at end of file diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 78644e0eded..b1db8ce0a59 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -57,6 +57,7 @@ import { ExtensionActivatedByAPI } from 'vs/workbench/api/node/extHostExtensionA import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { OverviewRulerLane } from 'vs/editor/common/model'; import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService'; +import { ExtHostWebviews } from 'vs/workbench/api/node/extHostWebview'; export interface IExtensionApiFactory { (extension: IExtensionDescription): typeof vscode; @@ -116,6 +117,7 @@ export function createApiFactory( const extHostTask = rpcProtocol.set(ExtHostContext.ExtHostTask, new ExtHostTask(rpcProtocol, extHostWorkspace)); const extHostWindow = rpcProtocol.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(rpcProtocol)); rpcProtocol.set(ExtHostContext.ExtHostExtensionService, extensionService); + const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol)); // Check that no named customers are missing const expected: ProxyIdentifier[] = Object.keys(ExtHostContext).map((key) => ExtHostContext[key]); @@ -396,6 +398,9 @@ export function createApiFactory( }), registerDecorationProvider: proposedApiFunction(extension, (provider: vscode.DecorationProvider) => { return extHostDecorations.registerDecorationProvider(provider, extension.id); + }), + createWebview: proposedApiFunction(extension, (name: string, column: vscode.ViewColumn, options: vscode.WebviewOptions) => { + return extHostWebviews.createWebview(name, column, options); }) }; diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 3a62eda7a27..d3fae306747 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -345,6 +345,21 @@ export interface MainThreadTelemetryShape extends IDisposable { $publicLog(eventName: string, data?: any): void; } +export interface MainThreadWebviewShape extends IDisposable { + $createWebview(handle: number): void; + $disposeWebview(handle: number): void; + $show(handle: number, column: EditorPosition): void; + $setTitle(handle: number, value: string): void; + $setHtml(handle: number, value: string): void; + $setOptions(handle: number, value: vscode.WebviewOptions): void; + $sendMessage(handle: number, value: any): Thenable; +} +export interface ExtHostWebviewsShape { + $onMessage(handle: number, message: any): void; + $onBecameActive(handle: number): void; + $onBecameInactive(handle: number): void; +} + export interface MainThreadWorkspaceShape extends IDisposable { $startSearch(includePattern: string, includeFolder: string, excludePatternOrDisregardExcludes: string | false, maxResults: number, requestId: number): Thenable; $cancelSearch(requestId: number): Thenable; @@ -796,6 +811,7 @@ export const MainContext = { MainThreadStorage: createMainId('MainThreadStorage'), MainThreadTelemetry: createMainId('MainThreadTelemetry'), MainThreadTerminalService: createMainId('MainThreadTerminalService'), + MainThreadWebview: createMainId('MainThreadWebview'), MainThreadWorkspace: createMainId('MainThreadWorkspace'), MainThreadFileSystem: createMainId('MainThreadFileSystem'), MainThreadExtensionService: createMainId('MainThreadExtensionService'), @@ -828,4 +844,5 @@ export const ExtHostContext = { ExtHostTask: createExtId('ExtHostTask'), ExtHostWorkspace: createExtId('ExtHostWorkspace'), ExtHostWindow: createExtId('ExtHostWindow'), + ExtHostWebviews: createExtId('ExtHostWebviews') }; diff --git a/src/vs/workbench/api/node/extHostWebview.ts b/src/vs/workbench/api/node/extHostWebview.ts new file mode 100644 index 00000000000..dac84a1bfcf --- /dev/null +++ b/src/vs/workbench/api/node/extHostWebview.ts @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MainContext, MainThreadWebviewShape, IMainContext, ExtHostWebviewsShape } from './extHost.protocol'; +import * as vscode from 'vscode'; +import { Emitter } from 'vs/base/common/event'; +import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters'; + +class ExtHostWebview implements vscode.Webview { + private _title: string; + private _html: string; + private _options: vscode.WebviewOptions; + private _isDisposed: boolean = false; + + public readonly onMessageEmitter = new Emitter(); + public readonly onMessage = this.onMessageEmitter.event; + + public readonly onBecameActiveEmitter = new Emitter(); + public readonly onBecameActive = this.onBecameActiveEmitter.event; + + public readonly onBecameInactiveEmitter = new Emitter(); + public readonly onBecameInactive = this.onBecameInactiveEmitter.event; + + constructor( + private readonly _proxy: MainThreadWebviewShape, + private readonly _handle: number, + ) { } + + public dispose() { + if (this._isDisposed) { + return; + } + this._isDisposed = true; + this._proxy.$disposeWebview(this._handle); + } + + get title(): string { + return this._title; + } + + set title(value: string) { + if (this._title !== value) { + this._title = value; + this._proxy.$setTitle(this._handle, value); + } + } + + get html(): string { + return this._html; + } + + set html(value: string) { + if (this._html !== value) { + this._html = value; + this._proxy.$setHtml(this._handle, value); + } + } + + get options(): vscode.WebviewOptions { + return this._options; + } + + set options(value: vscode.WebviewOptions) { + this._proxy.$setOptions(this._handle, value); + } + + public postMessage(message: any): Thenable { + return this._proxy.$sendMessage(this._handle, message); + } +} + +export class ExtHostWebviews implements ExtHostWebviewsShape { + private static _handlePool = 0; + + private readonly _proxy: MainThreadWebviewShape; + + private readonly _webviews = new Map(); + + constructor( + mainContext: IMainContext + ) { + this._proxy = mainContext.getProxy(MainContext.MainThreadWebview); + } + + createWebview( + title: string, + column: vscode.ViewColumn, + options: vscode.WebviewOptions + ): vscode.Webview { + const handle = ExtHostWebviews._handlePool++; + this._proxy.$createWebview(handle); + + const webview = new ExtHostWebview(this._proxy, handle); + this._webviews.set(handle, webview); + webview.title = title; + webview.options = options; + this._proxy.$show(handle, typeConverters.fromViewColumn(column)); + return webview; + } + + $onMessage(handle: number, message: any): void { + const webview = this._webviews.get(handle); + webview.onMessageEmitter.fire(message); + } + + $onBecameActive(handle: number): void { + const webview = this._webviews.get(handle); + webview.onBecameActiveEmitter.fire(); + } + + $onBecameInactive(handle: number): void { + const webview = this._webviews.get(handle); + webview.onBecameInactiveEmitter.fire(); + } +} \ No newline at end of file diff --git a/src/vs/workbench/browser/composite.ts b/src/vs/workbench/browser/composite.ts index c7f752077e0..a8ae09dfc71 100644 --- a/src/vs/workbench/browser/composite.ts +++ b/src/vs/workbench/browser/composite.ts @@ -85,6 +85,10 @@ export abstract class Composite extends Component implements IComposite { return this.parent; } + public getFocusContainer(): Builder { + return this.getContainer(); + } + /** * Note: Clients should not call this method, the workbench calls this * method. Calling it otherwise may result in unexpected behavior. diff --git a/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts b/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts index 0c500492a5c..fde984c7ae9 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts @@ -424,7 +424,7 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro } // Track focus on editor container - const focusTracker = DOM.trackFocus(editor.getContainer().getHTMLElement()); + const focusTracker = DOM.trackFocus(editor.getFocusContainer().getHTMLElement()); const listenerDispose = focusTracker.onDidFocus(() => { this.onFocusGained(editor); }); diff --git a/src/vs/workbench/parts/extensions/browser/extensionEditor.ts b/src/vs/workbench/parts/extensions/browser/extensionEditor.ts index 20d38a211f6..a0566ec09fc 100644 --- a/src/vs/workbench/parts/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/parts/extensions/browser/extensionEditor.ts @@ -46,7 +46,7 @@ import { IPartService, Parts } from 'vs/workbench/services/part/common/partServi import { IThemeService } from 'vs/platform/theme/common/themeService'; import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { IContextKeyService, RawContextKey, ContextKeyExpr, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Command, ICommandOptions } from 'vs/editor/browser/editorExtensions'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -54,15 +54,12 @@ import { Color } from 'vs/base/common/color'; import { WorkbenchTree } from 'vs/platform/list/browser/listService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { assign } from 'vs/base/common/objects'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; /** A context key that is set when an extension editor webview has focus. */ export const KEYBINDING_CONTEXT_EXTENSIONEDITOR_WEBVIEW_FOCUS = new RawContextKey('extensionEditorWebviewFocus', undefined); -/** A context key that is set when an extension editor webview not have focus. */ -export const KEYBINDING_CONTEXT_EXTENSIONEDITOR_WEBVIEW_NOT_FOCUSED: ContextKeyExpr = KEYBINDING_CONTEXT_EXTENSIONEDITOR_WEBVIEW_FOCUS.toNegated(); /** A context key that is set when the find widget find input in extension editor webview is focused. */ export const KEYBINDING_CONTEXT_EXTENSIONEDITOR_FIND_WIDGET_INPUT_FOCUSED = new RawContextKey('extensionEditorFindWidgetInputFocused', false); -/** A context key that is set when the find widget find input in extension editor webview is not focused. */ -export const KEYBINDING_CONTEXT_EXTENSIONEDITOR_FIND_WIDGET_INPUT_NOT_FOCUSED: ContextKeyExpr = KEYBINDING_CONTEXT_EXTENSIONEDITOR_FIND_WIDGET_INPUT_FOCUSED.toNegated(); function renderBody(body: string): string { const styleSheetPath = require.toUrl('./media/markdown.css').replace('file://', 'vscode-core-resource://'); @@ -197,7 +194,8 @@ export class ExtensionEditor extends BaseEditor { @IContextViewService private contextViewService: IContextViewService, @IContextKeyService private contextKeyService: IContextKeyService, @IExtensionTipsService private extensionTipsService: IExtensionTipsService, - @IEnvironmentService private environmentService: IEnvironmentService + @IEnvironmentService private environmentService: IEnvironmentService, + @IWorkspaceContextService private contextService: IWorkspaceContextService ) { super(ExtensionEditor.ID, telemetryService, themeService); @@ -423,7 +421,7 @@ export class ExtensionEditor extends BaseEditor { .then(body => { const allowedBadgeProviders = this.extensionsWorkbenchService.allowedBadgeProviders; const webViewOptions = allowedBadgeProviders.length > 0 ? { allowScripts: false, allowSvgs: false, svgWhiteList: allowedBadgeProviders } : {}; - this.activeWebview = new WebView(this.content, this.partService.getContainer(Parts.EDITOR_PART), this.environmentService, this.contextViewService, this.contextKey, this.findInputFocusContextKey, webViewOptions, false); + this.activeWebview = new WebView(this.content, this.partService.getContainer(Parts.EDITOR_PART), this.environmentService, this.contextService, this.contextViewService, this.contextKey, this.findInputFocusContextKey, webViewOptions, false); const removeLayoutParticipant = arrays.insert(this.layoutParticipants, this.activeWebview); this.contentDisposables.push(toDisposable(removeLayoutParticipant)); diff --git a/src/vs/workbench/parts/html/browser/html.contribution.ts b/src/vs/workbench/parts/html/browser/html.contribution.ts index cebedb5d247..f0aae61248a 100644 --- a/src/vs/workbench/parts/html/browser/html.contribution.ts +++ b/src/vs/workbench/parts/html/browser/html.contribution.ts @@ -92,27 +92,6 @@ CommandsRegistry.registerCommand('_workbench.htmlPreview.postMessage', function return activePreviews.length > 0; }); -CommandsRegistry.registerCommand('_workbench.htmlPreview.updateOptions', function ( - accessor: ServicesAccessor, - resource: URI | string, - options: HtmlInputOptions -) { - - const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); - const inputOptions: HtmlInputOptions = options; - const allowedBadgeProviders = extensionsWorkbenchService.allowedBadgeProviders; - inputOptions.svgWhiteList = allowedBadgeProviders; - - const uri = resource instanceof URI ? resource : URI.parse(resource); - const activePreviews = getActivePreviewsForResource(accessor, resource); - for (const preview of activePreviews) { - if (preview.input && preview.input instanceof HtmlInput) { - const input = accessor.get(IInstantiationService).createInstance(HtmlInput, preview.input.getName(), '', uri, options); - preview.setInput(input); - } - } -}); - CommandsRegistry.registerCommand('_webview.openDevTools', function () { const elements = document.querySelectorAll('webview.ready'); for (let i = 0; i < elements.length; i++) { diff --git a/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts b/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts index edee199e773..06925da8b84 100644 --- a/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts +++ b/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts @@ -26,6 +26,7 @@ import Webview, { WebviewOptions } from './webview'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { WebviewEditor } from './webviewEditor'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; /** @@ -54,7 +55,8 @@ export class HtmlPreviewPart extends WebviewEditor { @IOpenerService private readonly openerService: IOpenerService, @IPartService private readonly partService: IPartService, @IContextViewService private readonly _contextViewService: IContextViewService, - @IEnvironmentService private readonly _environmentService: IEnvironmentService + @IEnvironmentService private readonly _environmentService: IEnvironmentService, + @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService ) { super(HtmlPreviewPart.ID, telemetryService, themeService, storageService, contextKeyService); } @@ -86,7 +88,17 @@ export class HtmlPreviewPart extends WebviewEditor { webviewOptions = this.input.options; } - this._webview = new Webview(this.content, this.partService.getContainer(Parts.EDITOR_PART), this._environmentService, this._contextViewService, this.contextKey, this.findInputFocusContextKey, webviewOptions, true); + this._webview = new Webview( + this.content, + this.partService.getContainer(Parts.EDITOR_PART), + this._environmentService, + this._contextService, + this._contextViewService, + this.contextKey, + this.findInputFocusContextKey, + webviewOptions, + true); + if (this.input && this.input instanceof HtmlInput) { const state = this.loadViewState(this.input.getResource()); this.scrollYPercentage = state ? state.scrollYPercentage : 0; diff --git a/src/vs/workbench/parts/html/browser/webview-pre.js b/src/vs/workbench/parts/html/browser/webview-pre.js index 17c09cdc32f..ed6c1fc5da8 100644 --- a/src/vs/workbench/parts/html/browser/webview-pre.js +++ b/src/vs/workbench/parts/html/browser/webview-pre.js @@ -12,6 +12,7 @@ var firstLoad = true; var loadTimeout; var pendingMessages = []; + var enableWrappedPostMessage = false; const initData = { initialScrollProgress: undefined @@ -125,6 +126,8 @@ // update iframe-contents ipcRenderer.on('content', function (_event, data) { const options = data.options; + enableWrappedPostMessage = options && options.enableWrappedPostMessage; + const text = data.contents.join('\n'); const newDocument = new DOMParser().parseFromString(text, 'text/html'); @@ -145,7 +148,7 @@ const defaultStyles = newDocument.createElement('style'); defaultStyles.id = '_defaultStyles'; - const vars = Object.keys(initData.styles).map(function (variable) { + const vars = Object.keys(initData.styles || {}).map(function (variable) { return `--${variable}: ${initData.styles[variable]};`; }); defaultStyles.innerHTML = ` @@ -312,9 +315,15 @@ initData.initialScrollProgress = progress; }); - // forward messages from the embedded iframe + // Forward messages from the embedded iframe window.onmessage = function (message) { - ipcRenderer.sendToHost(message.data.command, message.data.data); + if (enableWrappedPostMessage) { + // Modern webview. Forward wrapped message + ipcRenderer.sendToHost('onmessage', message.data); + } else { + // Old school webview. Forward exact message + ipcRenderer.sendToHost(message.data.command, message.data.data); + } }; // signal ready diff --git a/src/vs/workbench/parts/html/browser/webview.ts b/src/vs/workbench/parts/html/browser/webview.ts index 4010801932e..f4a48ff1cf4 100644 --- a/src/vs/workbench/parts/html/browser/webview.ts +++ b/src/vs/workbench/parts/html/browser/webview.ts @@ -17,6 +17,7 @@ import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { normalize, nativeSep } from 'vs/base/common/paths'; import { startsWith } from 'vs/base/common/strings'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; export interface WebviewElementFindInPageOptions { forward?: boolean; @@ -39,16 +40,20 @@ export interface WebviewOptions { allowScripts?: boolean; allowSvgs?: boolean; svgWhiteList?: string[]; + enableWrappedPostMessage?: boolean; } export default class Webview { private readonly _webview: Electron.WebviewTag; private _ready: Promise; private _disposables: IDisposable[] = []; - private _onDidClickLink = new Emitter(); + private _onDidClickLink = new Emitter(); private _onDidScroll = new Emitter<{ scrollYPercentage: number }>(); private _onFoundInPageResults = new Emitter(); + private _onMessage = new Emitter(); + private _onFocus = new Emitter(); + private _onBlur = new Emitter(); private _webviewFindWidget: WebviewFindWidget; private _findStarted: boolean = false; @@ -57,6 +62,7 @@ export default class Webview { private readonly parent: HTMLElement, private readonly _styleElement: Element, private readonly _environmentService: IEnvironmentService, + private readonly _contextService: IWorkspaceContextService, private readonly _contextViewService: IContextViewService, private readonly _contextKey: IContextKey, private readonly _findInputContextKey: IContextKey, @@ -102,7 +108,16 @@ export default class Webview { const contents = this._webview.getWebContents(); if (contents && !contents.isDestroyed()) { - registerFileProtocol(contents, 'vscode-core-resource', [this._environmentService.appRoot]); + registerFileProtocol(contents, 'vscode-core-resource', [ + this._environmentService.appRoot + ]); + registerFileProtocol(contents, 'vscode-extension-resource', [ + this._environmentService.extensionsPath, + this._environmentService.appRoot, + this._environmentService.extensionDevelopmentPath + ]); + registerFileProtocol(contents, 'vscode-workspace-resource', + this._contextService.getWorkspace().folders.map(folder => folder.uri.fsPath)); } })); } @@ -157,6 +172,12 @@ export default class Webview { }), addDisposableListener(this._webview, 'ipc-message', (event) => { switch (event.channel) { + case 'onmessage': + if (this._options.enableWrappedPostMessage && event.args && event.args.length) { + this._onMessage.fire(event.args[0]); + } + return; + case 'did-click-link': let [uri] = event.args; this._onDidClickLink.fire(URI.parse(uri)); @@ -178,13 +199,17 @@ export default class Webview { }), addDisposableListener(this._webview, 'focus', () => { if (this._contextKey) { + console.log('set'); this._contextKey.set(true); } + this._onFocus.fire(); }), addDisposableListener(this._webview, 'blur', () => { if (this._contextKey) { + console.log('reset'); this._contextKey.reset(); } + this._onBlur.fire(); }), addDisposableListener(this._webview, 'found-in-page', (event) => { this._onFoundInPageResults.fire(event.result); @@ -235,6 +260,18 @@ export default class Webview { return this._onFoundInPageResults.event; } + get onMessage(): Event { + return this._onMessage.event; + } + + get onFocus(): Event { + return this._onFocus.event; + } + + get onBlur(): Event { + return this._onBlur.event; + } + private _send(channel: string, ...args: any[]): void { this._ready .then(() => this._webview.send(channel, ...args)) @@ -427,6 +464,7 @@ function registerFileProtocol( callback({ path: normalizedPath }); return; } + } callback({ error: 'Cannot load resource outside of protocol root' }); }, (error) => { diff --git a/src/vs/workbench/parts/html/browser/webviewEditor.ts b/src/vs/workbench/parts/html/browser/webviewEditor.ts index 1b7a5536e61..f1c7a42e68e 100644 --- a/src/vs/workbench/parts/html/browser/webviewEditor.ts +++ b/src/vs/workbench/parts/html/browser/webviewEditor.ts @@ -2,7 +2,8 @@ * 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 * as nls from 'vs/nls'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { BaseWebviewEditor } from 'vs/workbench/browser/parts/editor/webviewEditor'; @@ -17,6 +18,11 @@ import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRe import WebView from './webview'; import { Builder } from 'vs/base/browser/builder'; +import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { Action } from 'vs/base/common/actions'; +import { TPromise } from 'vs/base/common/winjs.base'; export interface HtmlPreviewEditorViewState { scrollYPercentage: number; @@ -24,18 +30,10 @@ export interface HtmlPreviewEditorViewState { /** A context key that is set when a webview editor has focus. */ export const KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS = new RawContextKey('webviewEditorFocus', false); -/** A context key that is set when a webview editor does not have focus. */ -export const KEYBINDING_CONTEXT_WEBVIEWEDITOR_NOT_FOCUSED: ContextKeyExpr = KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS.toNegated(); /** A context key that is set when the find widget find input in webview editor webview is focused. */ export const KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED = new RawContextKey('webviewEditorFindWidgetInputFocused', false); -/** A context key that is set when the find widget find input in webview editor webview is not focused. */ -export const KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_NOT_FOCUSED: ContextKeyExpr = KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED.toNegated(); - /** A context key that is set when the find widget in a webview is visible. */ export const KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE = new RawContextKey('webviewFindWidgetVisible', false); -/** A context key that is set when the find widget in a webview is not visible. */ -export const KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_NOT_VISIBLE: ContextKeyExpr = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE.toNegated(); - /** * This class is only intended to be subclassed and not instantiated. @@ -46,7 +44,7 @@ export abstract class WebviewEditor extends BaseWebviewEditor { protected _webview: WebView; protected content: HTMLElement; protected contextKey: IContextKey; - private findWidgetVisible: IContextKey; + protected findWidgetVisible: IContextKey; protected findInputFocusContextKey: IContextKey; constructor( @@ -104,30 +102,42 @@ export abstract class WebviewEditor extends BaseWebviewEditor { protected abstract createEditor(parent: Builder): void; } -class ShowWebViewEditorFindCommand extends Command { - public runCommand(accessor: ServicesAccessor, args: any): void { - const webViewEditor = this.getWebViewEditor(accessor); +class ShowWebViewEditorFindWidgetAction extends Action { + public static readonly ID = 'editor.action.webvieweditor.showFind'; + public static readonly LABEL = nls.localize('editor.action.webvieweditor.showFind', "Focus Find Widget"); + + public constructor( + id: string, + label: string, + @IWorkbenchEditorService private workbenchEditorService: IWorkbenchEditorService + ) { + super(id, label); + } + + public run(): TPromise { + const webViewEditor = this.getWebViewEditor(); if (webViewEditor) { webViewEditor.showFind(); } + return null; } - private getWebViewEditor(accessor: ServicesAccessor): WebviewEditor { - const activeEditor = accessor.get(IWorkbenchEditorService).getActiveEditor() as WebviewEditor; + private getWebViewEditor(): WebviewEditor { + const activeEditor = this.workbenchEditorService.getActiveEditor() as WebviewEditor; if (activeEditor.isWebviewEditor) { return activeEditor; } return null; } } -const showFindCommand = new ShowWebViewEditorFindCommand({ - id: 'editor.action.webvieweditor.showFind', - precondition: KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS, - kbOpts: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_F - } -}); -KeybindingsRegistry.registerCommandAndKeybindingRule(showFindCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib())); + +const category = 'Webview'; +let actionRegistry = Registry.as(ActionExtensions.WorkbenchActions); + +actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ShowWebViewEditorFindWidgetAction, ShowWebViewEditorFindWidgetAction.ID, ShowWebViewEditorFindWidgetAction.LABEL, { + primary: KeyMod.CtrlCmd | KeyCode.KEY_F +}, KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS), + 'Webview: Focus Find Widget', category); class HideWebViewEditorFindCommand extends Command { public runCommand(accessor: ServicesAccessor, args: any): void { diff --git a/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts b/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts index b78a3529a97..ace4d2f79fd 100644 --- a/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts +++ b/src/vs/workbench/parts/update/electron-browser/releaseNotesEditor.ts @@ -28,6 +28,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { onUnexpectedError } from 'vs/base/common/errors'; import { addGAParameters } from 'vs/platform/telemetry/node/telemetryNodeUtils'; import { generateTokensCSSForColorMap } from 'vs/editor/common/modes/supports/tokenization'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; function renderBody( body: string, @@ -63,7 +64,8 @@ export class ReleaseNotesEditor extends WebviewEditor { @IOpenerService private openerService: IOpenerService, @IModeService private modeService: IModeService, @IPartService private partService: IPartService, - @IContextViewService private _contextViewService: IContextViewService + @IContextViewService private _contextViewService: IContextViewService, + @IWorkspaceContextService private _contextService: IWorkspaceContextService ) { super(ReleaseNotesEditor.ID, telemetryService, themeService, storageService, contextKeyService); } @@ -104,7 +106,17 @@ export class ReleaseNotesEditor extends WebviewEditor { const colorMap = TokenizationRegistry.getColorMap(); const css = generateTokensCSSForColorMap(colorMap); const body = renderBody(marked(text, { renderer }), css); - this._webview = new WebView(this.content, this.partService.getContainer(Parts.EDITOR_PART), this.environmentService, this._contextViewService, this.contextKey, this.findInputFocusContextKey, {}, false); + this._webview = new WebView( + this.content, + this.partService.getContainer(Parts.EDITOR_PART), + this.environmentService, + this._contextService, + this._contextViewService, + this.contextKey, + this.findInputFocusContextKey, + {}, + false); + if (this.input && this.input instanceof ReleaseNotesInput) { const state = this.loadViewState(this.input.version); if (state) {