Files
azuredatastudio/src/vs/workbench/api/common/extHostWebview.ts
Anthony Dresser 30d9e9c141 Merge from vscode 4636be2b71c87bfb0bfe3c94278b447a5efcc1f1 (#8722)
* Merge from vscode 4636be2b71c87bfb0bfe3c94278b447a5efcc1f1

* remove tests that aren't working
2019-12-18 00:14:28 -08:00

486 lines
16 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { URI, UriComponents } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import * as modes from 'vs/editor/common/modes';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters';
import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor';
import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview';
import * as vscode from 'vscode';
import { ExtHostWebviewsShape, IMainContext, MainContext, MainThreadWebviewsShape, WebviewEditorCapabilities, WebviewPanelHandle, WebviewPanelViewStateData } from './extHost.protocol';
import { Disposable as VSCodeDisposable } from './extHostTypes';
type IconPath = URI | { light: URI, dark: URI };
export class ExtHostWebview implements vscode.Webview {
private _html: string = '';
private _isDisposed: boolean = false;
private _hasCalledAsWebviewUri = false;
public readonly _onMessageEmitter = new Emitter<any>();
public readonly onDidReceiveMessage: Event<any> = this._onMessageEmitter.event;
constructor(
private readonly _handle: WebviewPanelHandle,
private readonly _proxy: MainThreadWebviewsShape,
private _options: vscode.WebviewOptions,
private readonly _initData: WebviewInitData,
private readonly _workspace: IExtHostWorkspace | undefined,
private readonly _extension: IExtensionDescription,
private readonly _logService: ILogService,
) { }
public dispose() {
this._onMessageEmitter.dispose();
}
public asWebviewUri(resource: vscode.Uri): vscode.Uri {
this._hasCalledAsWebviewUri = true;
return asWebviewUri(this._initData, this._handle, resource);
}
public get cspSource(): string {
return this._initData.webviewCspSource
.replace('{{uuid}}', this._handle);
}
public get html(): string {
this.assertNotDisposed();
return this._html;
}
public set html(value: string) {
this.assertNotDisposed();
if (this._html !== value) {
this._html = value;
if (this._initData.isExtensionDevelopmentDebug && !this._hasCalledAsWebviewUri) {
if (/(["'])vscode-resource:([^\s'"]+?)(["'])/i.test(value)) {
this._hasCalledAsWebviewUri = true;
this._logService.warn(`${this._extension.identifier.value} created a webview that appears to use the vscode-resource scheme directly. Please migrate to use the 'webview.asWebviewUri' api instead: https://aka.ms/vscode-webview-use-aswebviewuri`);
}
}
this._proxy.$setHtml(this._handle, value);
}
}
public get options(): vscode.WebviewOptions {
this.assertNotDisposed();
return this._options;
}
public set options(newOptions: vscode.WebviewOptions) {
this.assertNotDisposed();
this._proxy.$setOptions(this._handle, convertWebviewOptions(this._extension, this._workspace, newOptions));
this._options = newOptions;
}
public postMessage(message: any): Promise<boolean> {
this.assertNotDisposed();
return this._proxy.$postMessage(this._handle, message);
}
private assertNotDisposed() {
if (this._isDisposed) {
throw new Error('Webview is disposed');
}
}
}
export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPanel {
private readonly _handle: WebviewPanelHandle;
private readonly _proxy: MainThreadWebviewsShape;
private readonly _viewType: string;
private _title: string;
private _iconPath?: IconPath;
private readonly _options: vscode.WebviewPanelOptions;
private readonly _webview: ExtHostWebview;
private _viewColumn: vscode.ViewColumn | undefined;
private _visible: boolean = true;
private _active: boolean = true;
_isDisposed: boolean = false;
readonly _onDisposeEmitter = this._register(new Emitter<void>());
public readonly onDidDispose: Event<void> = this._onDisposeEmitter.event;
readonly _onDidChangeViewStateEmitter = this._register(new Emitter<vscode.WebviewPanelOnDidChangeViewStateEvent>());
public readonly onDidChangeViewState: Event<vscode.WebviewPanelOnDidChangeViewStateEvent> = this._onDidChangeViewStateEmitter.event;
constructor(
handle: WebviewPanelHandle,
proxy: MainThreadWebviewsShape,
viewType: string,
title: string,
viewColumn: vscode.ViewColumn | undefined,
editorOptions: vscode.WebviewPanelOptions,
webview: ExtHostWebview
) {
super();
this._handle = handle;
this._proxy = proxy;
this._viewType = viewType;
this._options = editorOptions;
this._viewColumn = viewColumn;
this._title = title;
this._webview = webview;
}
public dispose() {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
this._onDisposeEmitter.fire();
this._proxy.$disposeWebview(this._handle);
this._webview.dispose();
super.dispose();
}
get webview() {
this.assertNotDisposed();
return this._webview;
}
get viewType(): string {
this.assertNotDisposed();
return this._viewType;
}
get title(): string {
this.assertNotDisposed();
return this._title;
}
set title(value: string) {
this.assertNotDisposed();
if (this._title !== value) {
this._title = value;
this._proxy.$setTitle(this._handle, value);
}
}
get iconPath(): IconPath | undefined {
this.assertNotDisposed();
return this._iconPath;
}
set iconPath(value: IconPath | undefined) {
this.assertNotDisposed();
if (this._iconPath !== value) {
this._iconPath = value;
this._proxy.$setIconPath(this._handle, URI.isUri(value) ? { light: value, dark: value } : value);
}
}
get options() {
return this._options;
}
get viewColumn(): vscode.ViewColumn | undefined {
this.assertNotDisposed();
if (typeof this._viewColumn === 'number' && this._viewColumn < 0) {
// We are using a symbolic view column
// Return undefined instead to indicate that the real view column is currently unknown but will be resolved.
return undefined;
}
return this._viewColumn;
}
_setViewColumn(value: vscode.ViewColumn) {
this.assertNotDisposed();
this._viewColumn = value;
}
public get active(): boolean {
this.assertNotDisposed();
return this._active;
}
_setActive(value: boolean) {
this.assertNotDisposed();
this._active = value;
}
public get visible(): boolean {
this.assertNotDisposed();
return this._visible;
}
_setVisible(value: boolean) {
this.assertNotDisposed();
this._visible = value;
}
public postMessage(message: any): Promise<boolean> {
this.assertNotDisposed();
return this._proxy.$postMessage(this._handle, message);
}
public reveal(viewColumn?: vscode.ViewColumn, preserveFocus?: boolean): void {
this.assertNotDisposed();
this._proxy.$reveal(this._handle, {
viewColumn: viewColumn ? typeConverters.ViewColumn.from(viewColumn) : undefined,
preserveFocus: !!preserveFocus
});
}
private assertNotDisposed() {
if (this._isDisposed) {
throw new Error('Webview is disposed');
}
}
}
export class ExtHostWebviews implements ExtHostWebviewsShape {
private static newHandle(): WebviewPanelHandle {
return generateUuid();
}
private readonly _proxy: MainThreadWebviewsShape;
private readonly _webviewPanels = new Map<WebviewPanelHandle, ExtHostWebviewEditor>();
private readonly _serializers = new Map<string, { readonly serializer: vscode.WebviewPanelSerializer, readonly extension: IExtensionDescription }>();
private readonly _editorProviders = new Map<string, { readonly provider: vscode.WebviewCustomEditorProvider, readonly extension: IExtensionDescription }>();
constructor(
mainContext: IMainContext,
private readonly initData: WebviewInitData,
private readonly workspace: IExtHostWorkspace | undefined,
private readonly _logService: ILogService,
) {
this._proxy = mainContext.getProxy(MainContext.MainThreadWebviews);
}
public createWebviewPanel(
extension: IExtensionDescription,
viewType: string,
title: string,
showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean },
options: (vscode.WebviewPanelOptions & vscode.WebviewOptions) = {},
): vscode.WebviewPanel {
const viewColumn = typeof showOptions === 'object' ? showOptions.viewColumn : showOptions;
const webviewShowOptions = {
viewColumn: typeConverters.ViewColumn.from(viewColumn),
preserveFocus: typeof showOptions === 'object' && !!showOptions.preserveFocus
};
const handle = ExtHostWebviews.newHandle();
this._proxy.$createWebviewPanel({ id: extension.identifier, location: extension.extensionLocation }, handle, viewType, title, webviewShowOptions, convertWebviewOptions(extension, this.workspace, options));
const webview = new ExtHostWebview(handle, this._proxy, options, this.initData, this.workspace, extension, this._logService);
const panel = new ExtHostWebviewEditor(handle, this._proxy, viewType, title, viewColumn, options, webview);
this._webviewPanels.set(handle, panel);
return panel;
}
public registerWebviewPanelSerializer(
extension: IExtensionDescription,
viewType: string,
serializer: vscode.WebviewPanelSerializer
): vscode.Disposable {
if (this._serializers.has(viewType)) {
throw new Error(`Serializer for '${viewType}' already registered`);
}
this._serializers.set(viewType, { serializer, extension });
this._proxy.$registerSerializer(viewType);
return new VSCodeDisposable(() => {
this._serializers.delete(viewType);
this._proxy.$unregisterSerializer(viewType);
});
}
public registerWebviewCustomEditorProvider(
extension: IExtensionDescription,
viewType: string,
provider: vscode.WebviewCustomEditorProvider,
options?: vscode.WebviewPanelOptions,
): vscode.Disposable {
if (this._editorProviders.has(viewType)) {
throw new Error(`Editor provider for '${viewType}' already registered`);
}
this._editorProviders.set(viewType, { extension, provider, });
this._proxy.$registerEditorProvider({ id: extension.identifier, location: extension.extensionLocation }, viewType, options || {}, this.getCapabilites(provider));
provider?.editingDelegate?.onEdit(({ edit, resource }) => {
this._proxy.$onEdit(resource, viewType, edit);
});
return new VSCodeDisposable(() => {
this._editorProviders.delete(viewType);
this._proxy.$unregisterEditorProvider(viewType);
});
}
public $onMessage(
handle: WebviewPanelHandle,
message: any
): void {
const panel = this.getWebviewPanel(handle);
if (panel) {
panel.webview._onMessageEmitter.fire(message);
}
}
public $onMissingCsp(
_handle: WebviewPanelHandle,
extensionId: string
): void {
this._logService.warn(`${extensionId} created a webview without a content security policy: https://aka.ms/vscode-webview-missing-csp`);
}
public $onDidChangeWebviewPanelViewStates(newStates: WebviewPanelViewStateData): void {
const handles = Object.keys(newStates);
// Notify webviews of state changes in the following order:
// - Non-visible
// - Visible
// - Active
handles.sort((a, b) => {
const stateA = newStates[a];
const stateB = newStates[b];
if (stateA.active) {
return 1;
}
if (stateB.active) {
return -1;
}
return (+stateA.visible) - (+stateB.visible);
});
for (const handle of handles) {
const panel = this.getWebviewPanel(handle);
if (!panel || panel._isDisposed) {
continue;
}
const newState = newStates[handle];
const viewColumn = typeConverters.ViewColumn.to(newState.position);
if (panel.active !== newState.active || panel.visible !== newState.visible || panel.viewColumn !== viewColumn) {
panel._setActive(newState.active);
panel._setVisible(newState.visible);
panel._setViewColumn(viewColumn);
panel._onDidChangeViewStateEmitter.fire({ webviewPanel: panel });
}
}
}
async $onDidDisposeWebviewPanel(handle: WebviewPanelHandle): Promise<void> {
const panel = this.getWebviewPanel(handle);
if (panel) {
panel.dispose();
this._webviewPanels.delete(handle);
}
}
async $deserializeWebviewPanel(
webviewHandle: WebviewPanelHandle,
viewType: string,
title: string,
state: any,
position: EditorViewColumn,
options: modes.IWebviewOptions & modes.IWebviewPanelOptions
): Promise<void> {
const entry = this._serializers.get(viewType);
if (!entry) {
throw new Error(`No serializer found for '${viewType}'`);
}
const { serializer, extension } = entry;
const webview = new ExtHostWebview(webviewHandle, this._proxy, options, this.initData, this.workspace, extension, this._logService);
const revivedPanel = new ExtHostWebviewEditor(webviewHandle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview);
this._webviewPanels.set(webviewHandle, revivedPanel);
await serializer.deserializeWebviewPanel(revivedPanel, state);
}
async $resolveWebviewEditor(
resource: UriComponents,
handle: WebviewPanelHandle,
viewType: string,
title: string,
position: EditorViewColumn,
options: modes.IWebviewOptions & modes.IWebviewPanelOptions
): Promise<void> {
const entry = this._editorProviders.get(viewType);
if (!entry) {
return Promise.reject(new Error(`No provider found for '${viewType}'`));
}
const { provider, extension } = entry;
const webview = new ExtHostWebview(handle, this._proxy, options, this.initData, this.workspace, extension, this._logService);
const revivedPanel = new ExtHostWebviewEditor(handle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview);
this._webviewPanels.set(handle, revivedPanel);
const revivedResource = URI.revive(resource);
await provider.resolveWebviewEditor(revivedResource, revivedPanel);
}
$undoEdits(resource: UriComponents, viewType: string, edits: readonly any[]): void {
const provider = this.getEditorProvider(viewType);
provider?.editingDelegate?.undoEdits(URI.revive(resource), edits);
}
$applyEdits(resource: UriComponents, viewType: string, edits: readonly any[]): void {
const provider = this.getEditorProvider(viewType);
provider?.editingDelegate?.applyEdits(URI.revive(resource), edits);
}
async $onSave(resource: UriComponents, viewType: string): Promise<void> {
const provider = this.getEditorProvider(viewType);
return provider?.editingDelegate?.save(URI.revive(resource));
}
async $onSaveAs(resource: UriComponents, viewType: string, targetResource: UriComponents): Promise<void> {
const provider = this.getEditorProvider(viewType);
return provider?.editingDelegate?.saveAs(URI.revive(resource), URI.revive(targetResource));
}
private getWebviewPanel(handle: WebviewPanelHandle): ExtHostWebviewEditor | undefined {
return this._webviewPanels.get(handle);
}
private getEditorProvider(viewType: string): vscode.WebviewCustomEditorProvider | undefined {
return this._editorProviders.get(viewType)?.provider;
}
private getCapabilites(capabilities: vscode.WebviewCustomEditorProvider) {
const declaredCapabilites: WebviewEditorCapabilities[] = [];
if (capabilities.editingDelegate) {
declaredCapabilites.push(WebviewEditorCapabilities.Editable);
}
return declaredCapabilites;
}
}
function convertWebviewOptions(
extension: IExtensionDescription,
workspace: IExtHostWorkspace | undefined,
options: vscode.WebviewPanelOptions & vscode.WebviewOptions,
): modes.IWebviewOptions {
return {
...options,
localResourceRoots: options.localResourceRoots || getDefaultLocalResourceRoots(extension, workspace)
};
}
function getDefaultLocalResourceRoots(
extension: IExtensionDescription,
workspace: IExtHostWorkspace | undefined,
): URI[] {
return [
...(workspace?.getWorkspaceFolders() || []).map(x => x.uri),
extension.extensionLocation,
];
}