mainThreadWebview.ts 14.8 KB
Newer Older
M
Matt Bierner 已提交
1 2 3 4
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
5
import { onUnexpectedError } from 'vs/base/common/errors';
6
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
M
Matt Bierner 已提交
7
import * as map from 'vs/base/common/map';
8
import { URI, UriComponents } from 'vs/base/common/uri';
9
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
10
import { localize } from 'vs/nls';
11 12
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
13
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
M
Matt Bierner 已提交
14
import { IOpenerService } from 'vs/platform/opener/common/opener';
15 16
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ExtHostContext, ExtHostWebviewsShape, IExtHostContext, MainContext, MainThreadWebviewsShape, WebviewInsetHandle, WebviewPanelHandle, WebviewPanelShowOptions } from 'vs/workbench/api/node/extHost.protocol';
17
import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/shared/editor';
18
import { CodeInsetController } from 'vs/workbench/contrib/codeinset/electron-browser/codeInset.contribution';
19 20
import { WebviewEditor } from 'vs/workbench/contrib/webview/electron-browser/webviewEditor';
import { WebviewEditorInput } from 'vs/workbench/contrib/webview/electron-browser/webviewEditorInput';
21 22 23
import { ICreateWebViewShowOptions, IWebviewEditorService, WebviewInputOptions } from 'vs/workbench/contrib/webview/electron-browser/webviewEditorService';
import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/webviewElement';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
24
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
25
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
26
import { IPartService, Parts } from 'vs/workbench/services/part/common/partService';
27
import * as vscode from 'vscode';
28
import { extHostNamedCustomer } from './extHostCustomers';
R
Rob DeLine 已提交
29

M
Matt Bierner 已提交
30
@extHostNamedCustomer(MainContext.MainThreadWebviews)
31
export class MainThreadWebviews implements MainThreadWebviewsShape {
32

M
Matt Bierner 已提交
33 34
	private static readonly standardSupportedLinkSchemes = ['http', 'https', 'mailto'];

35 36 37
	private static revivalPool = 0;

	private _toDispose: IDisposable[] = [];
M
Matt Bierner 已提交
38 39

	private readonly _proxy: ExtHostWebviewsShape;
40
	private readonly _webviews = new Map<WebviewPanelHandle, WebviewEditorInput>();
41
	private readonly _webviewsElements = new Map<WebviewInsetHandle, WebviewElement>();
42
	private readonly _revivers = new Map<string, IDisposable>();
M
Matt Bierner 已提交
43

44
	private _activeWebview: WebviewPanelHandle | undefined = undefined;
M
Matt Bierner 已提交
45 46 47

	constructor(
		context: IExtHostContext,
48
		@ILifecycleService lifecycleService: ILifecycleService,
M
Matt Bierner 已提交
49
		@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
50
		@IEditorService private readonly _editorService: IEditorService,
51
		@IWebviewEditorService private readonly _webviewService: IWebviewEditorService,
52 53
		@IOpenerService private readonly _openerService: IOpenerService,
		@IExtensionService private readonly _extensionService: IExtensionService,
54 55 56 57
		@ITelemetryService private readonly _telemetryService: ITelemetryService,
		@IInstantiationService private readonly _instantiationService: IInstantiationService,
		@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
		@IPartService private readonly _partService: IPartService,
M
Matt Bierner 已提交
58 59
	) {
		this._proxy = context.getProxy(ExtHostContext.ExtHostWebviews);
60 61
		_editorService.onDidActiveEditorChange(this.onActiveEditorChanged, this, this._toDispose);
		_editorService.onDidVisibleEditorsChange(this.onVisibleEditorsChanged, this, this._toDispose);
62

63 64
		lifecycleService.onBeforeShutdown(e => {
			e.veto(this._onBeforeShutdown());
65
		}, this, this._toDispose);
M
Matt Bierner 已提交
66 67
	}

68
	public dispose(): void {
M
Matt Bierner 已提交
69
		this._toDispose = dispose(this._toDispose);
M
Matt Bierner 已提交
70 71
	}

72
	public $createWebviewPanel(
73
		handle: WebviewPanelHandle,
74 75
		viewType: string,
		title: string,
M
Matt Bierner 已提交
76
		showOptions: { viewColumn?: EditorViewColumn, preserveFocus?: boolean },
77
		options: WebviewInputOptions,
78
		extensionId: ExtensionIdentifier,
79
		extensionLocation: UriComponents
80
	): void {
81 82
		const mainThreadShowOptions: ICreateWebViewShowOptions = Object.create(null);
		if (showOptions) {
M
Matt Bierner 已提交
83
			mainThreadShowOptions.preserveFocus = !!showOptions.preserveFocus;
84
			mainThreadShowOptions.group = viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn);
85 86
		}

87
		const webview = this._webviewService.createWebview(this.getInternalWebviewId(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), URI.revive(extensionLocation), this.createWebviewEventDelegate(handle));
88 89 90 91
		webview.state = {
			viewType: viewType,
			state: undefined
		};
M
Matt Bierner 已提交
92

93
		this._webviews.set(handle, webview);
94
		this._activeWebview = handle;
95 96 97 98 99 100

		/* __GDPR__
			"webviews:createWebviewPanel" : {
				"extensionId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
			}
		*/
101
		this._telemetryService.publicLog('webviews:createWebviewPanel', { extensionId: extensionId.value });
M
Matt Bierner 已提交
102 103
	}

104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
	$createWebviewCodeInset(handle: WebviewInsetHandle, symbolId: string, options: vscode.WebviewOptions, extensionLocation: UriComponents): void {
		// todo@joh main is for the lack of a code-inset service
		// which we maybe wanna have... this is how it now works
		// 1) create webview element
		// 2) find the code inset controller that request it
		// 3) let the controller adopt the widget
		// 4) continue to forward messages to the webview
		const webview = this._instantiationService.createInstance(
			WebviewElement,
			this._partService.getContainer(Parts.EDITOR_PART),
			{
				useSameOriginForRoot: true,
				extensionLocation: URI.revive(extensionLocation)
			},
			{
				allowScripts: options.enableScripts
			}
		);

		let found = false;
		for (const editor of this._codeEditorService.listCodeEditors()) {
			const ctrl = CodeInsetController.get(editor);
			if (ctrl && ctrl.acceptWebview(symbolId, webview)) {
				found = true;
				break;
			}
		}
R
Rob DeLine 已提交
131

132 133 134 135 136 137 138 139
		if (!found) {
			webview.dispose();
			return;
		}
		// this will leak... the adopted webview will be disposed by the
		// code inset controller. we might need a dispose-event here so that
		// we can clean up things.
		this._webviewsElements.set(handle, webview);
R
Rob DeLine 已提交
140 141
	}

142
	public $disposeWebview(handle: WebviewPanelHandle): void {
M
Matt Bierner 已提交
143
		const webview = this.getWebview(handle);
144
		webview.dispose();
M
Matt Bierner 已提交
145 146
	}

147
	public $setTitle(handle: WebviewPanelHandle, value: string): void {
M
Matt Bierner 已提交
148 149
		const webview = this.getWebview(handle);
		webview.setName(value);
M
Matt Bierner 已提交
150 151
	}

152 153
	public $setIconPath(handle: WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents } | undefined): void {
		const webview = this.getWebview(handle);
154
		webview.iconPath = reviveWebviewIcon(value);
155 156
	}

157 158 159 160 161 162 163
	public $setHtml(handle: WebviewPanelHandle | WebviewInsetHandle, value: string): void {
		if (typeof handle === 'number') {
			this._webviewsElements.get(handle).contents = value;
		} else {
			const webview = this.getWebview(handle);
			webview.html = value;
		}
M
Matt Bierner 已提交
164 165
	}

166 167 168 169 170 171 172
	public $setOptions(handle: WebviewPanelHandle | WebviewInsetHandle, options: vscode.WebviewOptions): void {
		if (typeof handle === 'number') {
			this._webviewsElements.get(handle).options = reviveWebviewOptions(options);
		} else {
			const webview = this.getWebview(handle);
			webview.setOptions(reviveWebviewOptions(options));
		}
173 174
	}

M
Matt Bierner 已提交
175
	public $reveal(handle: WebviewPanelHandle, showOptions: WebviewPanelShowOptions): void {
176
		const webview = this.getWebview(handle);
177 178 179 180
		if (webview.isDisposed()) {
			return;
		}

M
Matt Bierner 已提交
181
		const targetGroup = this._editorGroupService.getGroup(viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn));
182

183
		this._webviewService.revealWebview(webview, targetGroup || this._editorGroupService.getGroup(webview.group), !!showOptions.preserveFocus);
M
Matt Bierner 已提交
184 185
	}

186 187 188 189
	public $postMessage(handle: WebviewPanelHandle | WebviewInsetHandle, message: any): Promise<boolean> {
		if (typeof handle === 'number') {
			this._webviewsElements.get(handle).sendMessage(message);
			return Promise.resolve(true);
M
Matt Bierner 已提交
190

191 192 193 194 195 196
		} else {
			const webview = this.getWebview(handle);
			const editors = this._editorService.visibleControls
				.filter(e => e instanceof WebviewEditor)
				.map(e => e as WebviewEditor)
				.filter(e => e.input!.matches(webview));
M
Matt Bierner 已提交
197

198 199 200 201 202 203
			for (const editor of editors) {
				editor.sendMessage(message);
			}

			return Promise.resolve(editors.length > 0);
		}
M
Matt Bierner 已提交
204 205
	}

206
	public $registerSerializer(viewType: string): void {
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
		if (this._revivers.has(viewType)) {
			throw new Error(`Reviver for ${viewType} already registered`);
		}
		this._revivers.set(viewType, this._webviewService.registerReviver(this.getInternalWebviewId(viewType), {
			canRevive: (webview) => {
				return !webview.isDisposed() && webview.state;
			},
			reviveWebview: (webview): Promise<void> => {
				const viewType = webview.state.viewType;
				return Promise.resolve(this._extensionService.activateByEvent(`onWebviewPanel:${viewType}`).then(() => {
					const handle = 'revival-' + MainThreadWebviews.revivalPool++;
					this._webviews.set(handle, webview);
					webview._events = this.createWebviewEventDelegate(handle);
					let state = undefined;
					if (webview.state.state) {
						try {
							state = JSON.parse(webview.state.state);
						} catch {
							// noop
						}
					}

					return this._proxy.$deserializeWebviewPanel(handle, webview.state.viewType, webview.getTitle(), state, editorGroupToViewColumn(this._editorGroupService, webview.group), webview.options)
						.then(undefined, error => {
							onUnexpectedError(error);

							webview.html = MainThreadWebviews.getDeserializationFailedContents(viewType);
						});
				}));
236
			}
237
		}));
238 239
	}

240 241 242 243
	public $unregisterSerializer(viewType: string): void {
		const reviver = this._revivers.get(viewType);
		if (!reviver) {
			throw new Error(`No reviver for ${viewType} registered`);
244 245
		}

246 247 248 249 250 251
		reviver.dispose();
		this._revivers.delete(viewType);
	}

	private getInternalWebviewId(viewType: string): string {
		return `mainThreadWebview-${viewType}`;
252 253
	}

254
	private _onBeforeShutdown(): boolean {
255 256 257
		this._webviews.forEach((webview) => {
			if (!webview.isDisposed() && webview.state && this._revivers.has(webview.state.viewType)) {
				webview.state.state = webview.webviewState;
258 259
			}
		});
260
		return false; // Don't veto shutdown
261 262
	}

263
	private createWebviewEventDelegate(handle: WebviewPanelHandle) {
264 265 266 267
		return {
			onDidClickLink: uri => this.onDidClickLink(handle, uri),
			onMessage: message => this._proxy.$onMessage(handle, message),
			onDispose: () => {
M
Matt Bierner 已提交
268
				this._proxy.$onDidDisposeWebviewPanel(handle).finally(() => {
269
					this._webviews.delete(handle);
M
Matt Bierner 已提交
270
				});
271 272 273 274
			}
		};
	}

275 276
	private onActiveEditorChanged() {
		const activeEditor = this._editorService.activeControl;
277
		let newActiveWebview: { input: WebviewEditorInput, handle: WebviewPanelHandle } | undefined = undefined;
278
		if (activeEditor && activeEditor.input instanceof WebviewEditorInput) {
M
Matt Bierner 已提交
279
			for (const handle of map.keys(this._webviews)) {
M
Matt Bierner 已提交
280
				const input = this._webviews.get(handle)!;
M
Matt Bierner 已提交
281
				if (input.matches(activeEditor.input)) {
M
Matt Bierner 已提交
282
					newActiveWebview = { input, handle };
283 284 285 286
					break;
				}
			}
		}
M
Matt Bierner 已提交
287

288
		if (newActiveWebview && newActiveWebview.handle === this._activeWebview) {
289
			// Webview itself unchanged but position may have changed
M
Matt Bierner 已提交
290 291 292 293 294
			this._proxy.$onDidChangeWebviewPanelViewState(newActiveWebview.handle, {
				active: true,
				visible: true,
				position: editorGroupToViewColumn(this._editorGroupService, newActiveWebview.input.group)
			});
295 296 297 298 299
			return;
		}

		// Broadcast view state update for currently active
		if (typeof this._activeWebview !== 'undefined') {
300
			const oldActiveWebview = this._webviews.get(this._activeWebview);
301
			if (oldActiveWebview) {
M
Matt Bierner 已提交
302 303
				this._proxy.$onDidChangeWebviewPanelViewState(this._activeWebview, {
					active: false,
M
Matt Bierner 已提交
304
					visible: this._editorService.visibleControls.some(editor => !!editor.input && editor.input.matches(oldActiveWebview)),
M
Matt Bierner 已提交
305 306
					position: editorGroupToViewColumn(this._editorGroupService, oldActiveWebview.group),
				});
307
			}
308 309 310 311
		}

		// Then for newly active
		if (newActiveWebview) {
M
Matt Bierner 已提交
312 313 314 315 316
			this._proxy.$onDidChangeWebviewPanelViewState(newActiveWebview.handle, {
				active: true,
				visible: true,
				position: editorGroupToViewColumn(this._editorGroupService, activeEditor.group)
			});
317
			this._activeWebview = newActiveWebview.handle;
318
		} else {
319
			this._activeWebview = undefined;
M
Matt Bierner 已提交
320 321
		}
	}
M
Matt Bierner 已提交
322

323
	private onVisibleEditorsChanged(): void {
M
Matt Bierner 已提交
324 325 326 327
		this._webviews.forEach((input, handle) => {
			for (const workbenchEditor of this._editorService.visibleControls) {
				if (workbenchEditor.input && workbenchEditor.input.matches(input)) {
					const editorPosition = editorGroupToViewColumn(this._editorGroupService, workbenchEditor.group);
328

329
					input.updateGroup(workbenchEditor.group.id);
M
Matt Bierner 已提交
330 331 332 333 334 335
					this._proxy.$onDidChangeWebviewPanelViewState(handle, {
						active: handle === this._activeWebview,
						visible: true,
						position: editorPosition
					});
					break;
336
				}
M
Matt Bierner 已提交
337 338
			}
		});
339
	}
M
Matt Bierner 已提交
340

341
	private onDidClickLink(handle: WebviewPanelHandle, link: URI): void {
M
Matt Bierner 已提交
342 343 344 345
		if (!link) {
			return;
		}

346 347
		const webview = this.getWebview(handle);
		const enableCommandUris = webview.options.enableCommandUris;
M
Matt Bierner 已提交
348 349 350 351
		if (MainThreadWebviews.standardSupportedLinkSchemes.indexOf(link.scheme) >= 0 || enableCommandUris && link.scheme === 'command') {
			this._openerService.open(link);
		}
	}
352

353
	private getWebview(handle: WebviewPanelHandle): WebviewEditorInput {
354 355 356 357 358 359 360
		const webview = this._webviews.get(handle);
		if (!webview) {
			throw new Error('Unknown webview handle:' + handle);
		}
		return webview;
	}

361 362 363 364 365 366 367 368 369 370 371
	private static getDeserializationFailedContents(viewType: string) {
		return `<!DOCTYPE html>
		<html>
			<head>
				<base href="https://code.visualstudio.com/raw/">
				<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
				<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https: data:; media-src https:; script-src 'none'; style-src vscode-core-resource: https: 'unsafe-inline'; child-src 'none'; frame-src 'none';">
			</head>
			<body>${localize('errorMessage', "An error occurred while restoring view:{0}", viewType)}</body>
		</html>`;
	}
M
Matt Bierner 已提交
372 373 374 375 376 377 378 379
}

function reviveWebviewOptions(options: WebviewInputOptions): WebviewInputOptions {
	return {
		...options,
		localResourceRoots: Array.isArray(options.localResourceRoots) ? options.localResourceRoots.map(URI.revive) : undefined
	};
}
380 381 382 383 384 385 386 387 388 389 390 391

function reviveWebviewIcon(
	value: { light: UriComponents, dark: UriComponents } | undefined
): { light: URI, dark: URI } | undefined {
	if (!value) {
		return undefined;
	}

	return {
		light: URI.revive(value.light),
		dark: URI.revive(value.dark)
	};
392
}