mainThreadWebview.ts 15.4 KB
Newer Older
A
Alex Dima 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6 7
import { onUnexpectedError } from 'vs/base/common/errors';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
8
import { isWeb } from 'vs/base/common/platform';
9
import { startsWith } from 'vs/base/common/strings';
10
import { URI, UriComponents } from 'vs/base/common/uri';
A
Alex Dima 已提交
11
import * as modes from 'vs/editor/common/modes';
12
import { localize } from 'vs/nls';
A
Alex Dima 已提交
13
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
14
import { IOpenerService } from 'vs/platform/opener/common/opener';
15
import { IProductService } from 'vs/platform/product/common/productService';
16
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
17
import { ExtHostContext, ExtHostWebviewsShape, IExtHostContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle, WebviewPanelShowOptions, WebviewPanelViewStateData } from 'vs/workbench/api/common/extHost.protocol';
18
import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor';
19 20
import { IEditorInput } from 'vs/workbench/common/editor';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
21
import { CustomFileEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput';
22
import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput';
23
import { ICreateWebViewShowOptions, WebviewInputOptions, IWebviewWorkbenchService } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService';
24
import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
25
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
26
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
A
Alex Dima 已提交
27 28
import { extHostNamedCustomer } from '../common/extHostCustomers';

29 30 31 32
/**
 * Bi-directional map between webview handles and inputs.
 */
class WebviewHandleStore {
33 34
	private readonly _handlesToInputs = new Map<string, WebviewInput>();
	private readonly _inputsToHandles = new Map<WebviewInput, string>();
35

36
	public add(handle: string, input: WebviewInput): void {
37 38 39 40
		this._handlesToInputs.set(handle, input);
		this._inputsToHandles.set(input, handle);
	}

41
	public getHandleForInput(input: WebviewInput): string | undefined {
42 43 44
		return this._inputsToHandles.get(input);
	}

45
	public getInputForHandle(handle: string): WebviewInput | undefined {
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
		return this._handlesToInputs.get(handle);
	}

	public delete(handle: string): void {
		const input = this.getInputForHandle(handle);
		this._handlesToInputs.delete(handle);
		if (input) {
			this._inputsToHandles.delete(input);
		}
	}

	public get size(): number {
		return this._handlesToInputs.size;
	}
}

M
Matt Bierner 已提交
62 63 64 65 66 67 68 69 70 71 72 73 74 75
class InternalWebviewViewType {
	private static prefix = 'mainThreadWebview-';

	public static fromExternal(viewType: string): string {
		return InternalWebviewViewType.prefix + viewType;
	}

	public static toExternal(viewType: string): string | undefined {
		return startsWith(viewType, InternalWebviewViewType.prefix)
			? viewType.substr(InternalWebviewViewType.prefix.length)
			: undefined;
	}
}

A
Alex Dima 已提交
76 77
@extHostNamedCustomer(MainContext.MainThreadWebviews)
export class MainThreadWebviews extends Disposable implements MainThreadWebviewsShape {
78 79 80 81 82 83 84 85 86 87

	private static readonly standardSupportedLinkSchemes = new Set([
		'http',
		'https',
		'mailto',
		'vscode',
		'vscode-insider',
	]);

	private readonly _proxy: ExtHostWebviewsShape;
88
	private readonly _webviewEditorInputs = new WebviewHandleStore();
89
	private readonly _revivers = new Map<string, IDisposable>();
90
	private readonly _editorProviders = new Map<string, IDisposable>();
91 92 93 94 95 96

	constructor(
		context: IExtHostContext,
		@IExtensionService extensionService: IExtensionService,
		@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
		@IEditorService private readonly _editorService: IEditorService,
97
		@IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService,
98 99 100 101 102 103 104
		@IOpenerService private readonly _openerService: IOpenerService,
		@ITelemetryService private readonly _telemetryService: ITelemetryService,
		@IProductService private readonly _productService: IProductService,
	) {
		super();

		this._proxy = context.getProxy(ExtHostContext.ExtHostWebviews);
105 106
		this._register(_editorService.onDidActiveEditorChange(this.updateWebviewViewStates, this));
		this._register(_editorService.onDidVisibleEditorsChange(this.updateWebviewViewStates, this));
107

108
		// This reviver's only job is to activate webview panel extensions
109
		// This should trigger the real reviver to be registered from the extension host side.
110
		this._register(_webviewWorkbenchService.registerResolver({
111
			canResolve: (webview: WebviewInput) => {
M
Matt Bierner 已提交
112
				if (webview.getTypeId() === CustomFileEditorInput.typeId) {
113
					extensionService.activateByEvent(`onWebviewEditor:${(webview as CustomFileEditorInput).viewType}`);
M
Matt Bierner 已提交
114 115 116
					return false;
				}

M
Matt Bierner 已提交
117
				const viewType = InternalWebviewViewType.toExternal(webview.viewType);
M
Matt Bierner 已提交
118
				if (typeof viewType === 'string') {
119 120 121 122
					extensionService.activateByEvent(`onWebviewPanel:${viewType}`);
				}
				return false;
			},
123
			resolveWebview: () => { throw new Error('not implemented'); }
124 125 126 127 128 129 130
		}));
	}

	public $createWebviewPanel(
		handle: WebviewPanelHandle,
		viewType: string,
		title: string,
M
Matt Bierner 已提交
131
		showOptions: { viewColumn?: EditorViewColumn, preserveFocus?: boolean; },
132 133 134 135 136 137 138 139 140 141
		options: WebviewInputOptions,
		extensionId: ExtensionIdentifier,
		extensionLocation: UriComponents
	): void {
		const mainThreadShowOptions: ICreateWebViewShowOptions = Object.create(null);
		if (showOptions) {
			mainThreadShowOptions.preserveFocus = !!showOptions.preserveFocus;
			mainThreadShowOptions.group = viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn);
		}

M
Matt Bierner 已提交
142
		const webview = this._webviewWorkbenchService.createWebview(handle, InternalWebviewViewType.fromExternal(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), {
143 144
			location: URI.revive(extensionLocation),
			id: extensionId
145 146
		});
		this.hookupWebviewEventDelegate(handle, webview);
147

148
		this._webviewEditorInputs.add(handle, webview);
149 150 151 152 153 154 155 156 157 158

		/* __GDPR__
			"webviews:createWebviewPanel" : {
				"extensionId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
			}
		*/
		this._telemetryService.publicLog('webviews:createWebviewPanel', { extensionId: extensionId.value });
	}

	public $disposeWebview(handle: WebviewPanelHandle): void {
159
		const webview = this.getWebviewEditorInput(handle);
160 161 162 163
		webview.dispose();
	}

	public $setTitle(handle: WebviewPanelHandle, value: string): void {
164
		const webview = this.getWebviewEditorInput(handle);
165 166 167
		webview.setName(value);
	}

168
	public $setState(handle: WebviewPanelHandle, state: modes.WebviewContentState): void {
169 170 171 172 173 174
		const webview = this.getWebviewEditorInput(handle);
		if (webview instanceof CustomFileEditorInput) {
			webview.setState(state);
		}
	}

M
Matt Bierner 已提交
175
	public $setIconPath(handle: WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void {
176
		const webview = this.getWebviewEditorInput(handle);
177 178 179 180
		webview.iconPath = reviveWebviewIcon(value);
	}

	public $setHtml(handle: WebviewPanelHandle, value: string): void {
181 182
		const webview = this.getWebviewEditorInput(handle);
		webview.webview.html = value;
183 184 185
	}

	public $setOptions(handle: WebviewPanelHandle, options: modes.IWebviewOptions): void {
186 187
		const webview = this.getWebviewEditorInput(handle);
		webview.webview.contentOptions = reviveWebviewOptions(options as any /*todo@mat */);
188 189 190
	}

	public $reveal(handle: WebviewPanelHandle, showOptions: WebviewPanelShowOptions): void {
191
		const webview = this.getWebviewEditorInput(handle);
192 193 194 195 196 197
		if (webview.isDisposed()) {
			return;
		}

		const targetGroup = this._editorGroupService.getGroup(viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn)) || this._editorGroupService.getGroup(webview.group || 0);
		if (targetGroup) {
198
			this._webviewWorkbenchService.revealWebview(webview, targetGroup, !!showOptions.preserveFocus);
199 200 201 202
		}
	}

	public async $postMessage(handle: WebviewPanelHandle, message: any): Promise<boolean> {
203 204
		const webview = this.getWebviewEditorInput(handle);
		webview.webview.sendMessage(message);
205
		return true;
A
Alex Dima 已提交
206
	}
207 208 209 210 211 212

	public $registerSerializer(viewType: string): void {
		if (this._revivers.has(viewType)) {
			throw new Error(`Reviver for ${viewType} already registered`);
		}

213
		this._revivers.set(viewType, this._webviewWorkbenchService.registerResolver({
214
			canResolve: (webviewEditorInput) => {
M
Matt Bierner 已提交
215
				return webviewEditorInput.viewType === InternalWebviewViewType.fromExternal(viewType);
216
			},
217
			resolveWebview: async (webviewEditorInput): Promise<void> => {
M
Matt Bierner 已提交
218
				const viewType = InternalWebviewViewType.toExternal(webviewEditorInput.viewType);
M
Matt Bierner 已提交
219
				if (!viewType) {
220
					webviewEditorInput.webview.html = MainThreadWebviews.getDeserializationFailedContents(webviewEditorInput.viewType);
M
Matt Bierner 已提交
221 222 223
					return;
				}

224
				const handle = webviewEditorInput.id;
225
				this._webviewEditorInputs.add(handle, webviewEditorInput);
226
				this.hookupWebviewEventDelegate(handle, webviewEditorInput);
227

228
				let state = undefined;
229
				if (webviewEditorInput.webview.state) {
230
					try {
231
						state = JSON.parse(webviewEditorInput.webview.state);
232 233 234 235 236 237
					} catch {
						// noop
					}
				}

				try {
238
					await this._proxy.$deserializeWebviewPanel(handle, viewType, webviewEditorInput.getTitle(), state, editorGroupToViewColumn(this._editorGroupService, webviewEditorInput.group || 0), webviewEditorInput.webview.options);
239 240
				} catch (error) {
					onUnexpectedError(error);
241
					webviewEditorInput.webview.html = MainThreadWebviews.getDeserializationFailedContents(viewType);
242 243 244
				}
			}
		}));
A
Alex Dima 已提交
245
	}
246 247 248 249 250 251 252 253 254

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

		reviver.dispose();
		this._revivers.delete(viewType);
A
Alex Dima 已提交
255
	}
256

M
Matt Bierner 已提交
257
	public $registerEditorProvider(viewType: string, extensionId: ExtensionIdentifier, extensionLocation: UriComponents): void {
258 259 260 261
		if (this._editorProviders.has(viewType)) {
			throw new Error(`Provider for ${viewType} already registered`);
		}

M
Matt Bierner 已提交
262 263
		const extension = { id: extensionId, location: URI.revive(extensionLocation) };

264
		this._editorProviders.set(viewType, this._webviewWorkbenchService.registerResolver({
265
			canResolve: (webviewEditorInput) => {
266
				return webviewEditorInput.getTypeId() !== WebviewInput.typeId && webviewEditorInput.viewType === viewType;
267
			},
268
			resolveWebview: async (webview) => {
269
				const handle = webview.id;
270 271 272
				this._webviewEditorInputs.add(handle, webview);
				this.hookupWebviewEventDelegate(handle, webview);

M
Matt Bierner 已提交
273 274
				webview.webview.extension = extension;

275 276 277 278 279 280
				if (webview instanceof CustomFileEditorInput) {
					webview.onWillSave(e => {
						e.waitUntil(this._proxy.$save(handle));
					});
				}

281 282
				try {
					await this._proxy.$resolveWebviewEditor(
283
						webview.getResource(),
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307
						handle,
						viewType,
						webview.getTitle(),
						editorGroupToViewColumn(this._editorGroupService, webview.group || 0),
						webview.webview.options
					);
				} catch (error) {
					onUnexpectedError(error);
					webview.webview.html = MainThreadWebviews.getDeserializationFailedContents(viewType);
				}
			}
		}));
	}

	public $unregisterEditorProvider(viewType: string): void {
		const provider = this._editorProviders.get(viewType);
		if (!provider) {
			throw new Error(`No provider for ${viewType} registered`);
		}

		provider.dispose();
		this._editorProviders.delete(viewType);
	}

308
	private hookupWebviewEventDelegate(handle: WebviewPanelHandle, input: WebviewInput) {
309 310 311 312
		input.webview.onDidClickLink((uri: URI) => this.onDidClickLink(handle, uri));
		input.webview.onMessage((message: any) => this._proxy.$onMessage(handle, message));
		input.onDispose(() => {
			this._proxy.$onDidDisposeWebviewPanel(handle).finally(() => {
313
				this._webviewEditorInputs.delete(handle);
314 315 316
			});
		});
		input.webview.onDidUpdateState((newState: any) => {
317
			const webview = this.tryGetWebviewEditorInput(handle);
318 319
			if (!webview || webview.isDisposed()) {
				return;
320
			}
321 322
			webview.webview.state = newState;
		});
323
		input.webview.onMissingCsp((extension: ExtensionIdentifier) => this._proxy.$onMissingCsp(handle, extension.value));
A
Alex Dima 已提交
324
	}
325

326
	private updateWebviewViewStates() {
327 328 329
		if (!this._webviewEditorInputs.size) {
			return;
		}
330

331 332
		const activeInput = this._editorService.activeControl && this._editorService.activeControl.input;
		const viewStates: WebviewPanelViewStateData = {};
333

334 335 336 337 338 339 340 341 342 343
		const updateViewStatesForInput = (group: IEditorGroup, topLevelInput: IEditorInput, editorInput: IEditorInput) => {
			if (!(editorInput instanceof WebviewInput)) {
				return;
			}

			editorInput.updateGroup(group.id);

			const handle = this._webviewEditorInputs.getHandleForInput(editorInput);
			if (handle) {
				viewStates[handle] = {
344 345
					visible: topLevelInput === group.activeEditor,
					active: topLevelInput === activeInput,
346 347 348 349
					position: editorGroupToViewColumn(this._editorGroupService, group.id),
				};
			}
		};
350

351 352 353 354 355 356 357
		for (const group of this._editorGroupService.groups) {
			for (const input of group.editors) {
				if (input instanceof DiffEditorInput) {
					updateViewStatesForInput(group, input, input.master);
					updateViewStatesForInput(group, input, input.details);
				} else {
					updateViewStatesForInput(group, input, input);
358 359
				}
			}
360
		}
361 362 363 364

		if (Object.keys(viewStates).length) {
			this._proxy.$onDidChangeWebviewPanelViewStates(viewStates);
		}
365 366 367
	}

	private onDidClickLink(handle: WebviewPanelHandle, link: URI): void {
368
		const webview = this.getWebviewEditorInput(handle);
369 370 371 372 373
		if (this.isSupportedLink(webview, link)) {
			this._openerService.open(link);
		}
	}

374
	private isSupportedLink(webview: WebviewInput, link: URI): boolean {
375 376 377
		if (MainThreadWebviews.standardSupportedLinkSchemes.has(link.scheme)) {
			return true;
		}
378
		if (!isWeb && this._productService.urlProtocol === link.scheme) {
379 380
			return true;
		}
381
		return !!webview.webview.contentOptions.enableCommandUris && link.scheme === 'command';
A
Alex Dima 已提交
382
	}
383

384
	private getWebviewEditorInput(handle: WebviewPanelHandle): WebviewInput {
385 386 387 388 389 390 391
		const webview = this.tryGetWebviewEditorInput(handle);
		if (!webview) {
			throw new Error('Unknown webview handle:' + handle);
		}
		return webview;
	}

392
	private tryGetWebviewEditorInput(handle: WebviewPanelHandle): WebviewInput | undefined {
393
		return this._webviewEditorInputs.getInputForHandle(handle);
394 395
	}

396 397 398 399 400
	private static getDeserializationFailedContents(viewType: string) {
		return `<!DOCTYPE html>
		<html>
			<head>
				<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
401
				<meta http-equiv="Content-Security-Policy" content="default-src 'none';">
402 403 404
			</head>
			<body>${localize('errorMessage', "An error occurred while restoring view:{0}", viewType)}</body>
		</html>`;
A
Alex Dima 已提交
405
	}
406 407
}

408
function reviveWebviewOptions(options: modes.IWebviewOptions): WebviewInputOptions {
409 410
	return {
		...options,
411
		allowScripts: options.enableScripts,
412 413 414 415
		localResourceRoots: Array.isArray(options.localResourceRoots) ? options.localResourceRoots.map(r => URI.revive(r)) : undefined,
	};
}

416

417
function reviveWebviewIcon(
M
Matt Bierner 已提交
418 419
	value: { light: UriComponents, dark: UriComponents; } | undefined
): { light: URI, dark: URI; } | undefined {
420 421
	if (!value) {
		return undefined;
A
Alex Dima 已提交
422
	}
423 424 425 426 427

	return {
		light: URI.revive(value.light),
		dark: URI.revive(value.dark)
	};
A
Alex Dima 已提交
428
}