mainThreadWebview.ts 15.5 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;
	}
}

A
Alex Dima 已提交
62 63
@extHostNamedCustomer(MainContext.MainThreadWebviews)
export class MainThreadWebviews extends Disposable implements MainThreadWebviewsShape {
64 65 66 67 68 69 70 71 72 73

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

	private readonly _proxy: ExtHostWebviewsShape;
74
	private readonly _webviewEditorInputs = new WebviewHandleStore();
75
	private readonly _revivers = new Map<string, IDisposable>();
76
	private readonly _editorProviders = new Map<string, IDisposable>();
77 78 79 80 81 82

	constructor(
		context: IExtHostContext,
		@IExtensionService extensionService: IExtensionService,
		@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
		@IEditorService private readonly _editorService: IEditorService,
83
		@IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService,
84 85 86 87 88 89 90
		@IOpenerService private readonly _openerService: IOpenerService,
		@ITelemetryService private readonly _telemetryService: ITelemetryService,
		@IProductService private readonly _productService: IProductService,
	) {
		super();

		this._proxy = context.getProxy(ExtHostContext.ExtHostWebviews);
91 92
		this._register(_editorService.onDidActiveEditorChange(this.updateWebviewViewStates, this));
		this._register(_editorService.onDidVisibleEditorsChange(this.updateWebviewViewStates, this));
93

94
		// This reviver's only job is to activate webview panel extensions
95
		// This should trigger the real reviver to be registered from the extension host side.
96
		this._register(_webviewWorkbenchService.registerResolver({
97
			canResolve: (webview: WebviewInput) => {
M
Matt Bierner 已提交
98
				if (webview.getTypeId() === CustomFileEditorInput.typeId) {
99
					extensionService.activateByEvent(`onWebviewEditor:${(webview as CustomFileEditorInput).viewType}`);
M
Matt Bierner 已提交
100 101 102 103
					return false;
				}

				const viewType = this.fromInternalWebviewViewType(webview.viewType);
M
Matt Bierner 已提交
104
				if (typeof viewType === 'string') {
105 106 107 108
					extensionService.activateByEvent(`onWebviewPanel:${viewType}`);
				}
				return false;
			},
109
			resolveWebview: () => { throw new Error('not implemented'); }
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
		}));
	}

	public $createWebviewPanel(
		handle: WebviewPanelHandle,
		viewType: string,
		title: string,
		showOptions: { viewColumn?: EditorViewColumn, preserveFocus?: boolean },
		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);
		}

128
		const webview = this._webviewWorkbenchService.createWebview(handle, this.getInternalWebviewViewType(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), {
129 130
			location: URI.revive(extensionLocation),
			id: extensionId
131 132
		});
		this.hookupWebviewEventDelegate(handle, webview);
133

134
		this._webviewEditorInputs.add(handle, webview);
135 136 137 138 139 140 141 142 143 144

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

	public $disposeWebview(handle: WebviewPanelHandle): void {
145
		const webview = this.getWebviewEditorInput(handle);
146 147 148 149
		webview.dispose();
	}

	public $setTitle(handle: WebviewPanelHandle, value: string): void {
150
		const webview = this.getWebviewEditorInput(handle);
151 152 153
		webview.setName(value);
	}

154
	public $setState(handle: WebviewPanelHandle, state: modes.WebviewContentState): void {
155 156 157 158 159 160
		const webview = this.getWebviewEditorInput(handle);
		if (webview instanceof CustomFileEditorInput) {
			webview.setState(state);
		}
	}

161
	public $setIconPath(handle: WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents } | undefined): void {
162
		const webview = this.getWebviewEditorInput(handle);
163 164 165 166
		webview.iconPath = reviveWebviewIcon(value);
	}

	public $setHtml(handle: WebviewPanelHandle, value: string): void {
167 168
		const webview = this.getWebviewEditorInput(handle);
		webview.webview.html = value;
169 170 171
	}

	public $setOptions(handle: WebviewPanelHandle, options: modes.IWebviewOptions): void {
172 173
		const webview = this.getWebviewEditorInput(handle);
		webview.webview.contentOptions = reviveWebviewOptions(options as any /*todo@mat */);
174 175
	}

176 177 178 179 180
	public $setExtension(handle: WebviewPanelHandle, extensionId: ExtensionIdentifier, extensionLocation: UriComponents): void {
		const webview = this.getWebviewEditorInput(handle);
		webview.webview.extension = { id: extensionId, location: URI.revive(extensionLocation) };
	}

181
	public $reveal(handle: WebviewPanelHandle, showOptions: WebviewPanelShowOptions): void {
182
		const webview = this.getWebviewEditorInput(handle);
183 184 185 186 187 188
		if (webview.isDisposed()) {
			return;
		}

		const targetGroup = this._editorGroupService.getGroup(viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn)) || this._editorGroupService.getGroup(webview.group || 0);
		if (targetGroup) {
189
			this._webviewWorkbenchService.revealWebview(webview, targetGroup, !!showOptions.preserveFocus);
190 191 192 193
		}
	}

	public async $postMessage(handle: WebviewPanelHandle, message: any): Promise<boolean> {
194 195
		const webview = this.getWebviewEditorInput(handle);
		webview.webview.sendMessage(message);
196
		return true;
A
Alex Dima 已提交
197
	}
198 199 200 201 202 203

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

204
		this._revivers.set(viewType, this._webviewWorkbenchService.registerResolver({
205
			canResolve: (webviewEditorInput) => {
206
				return webviewEditorInput.viewType === this.getInternalWebviewViewType(viewType);
207
			},
208
			resolveWebview: async (webviewEditorInput): Promise<void> => {
209
				const viewType = this.fromInternalWebviewViewType(webviewEditorInput.viewType);
M
Matt Bierner 已提交
210
				if (!viewType) {
211
					webviewEditorInput.webview.html = MainThreadWebviews.getDeserializationFailedContents(webviewEditorInput.viewType);
M
Matt Bierner 已提交
212 213 214
					return;
				}

215
				const handle = webviewEditorInput.id;
216
				this._webviewEditorInputs.add(handle, webviewEditorInput);
217
				this.hookupWebviewEventDelegate(handle, webviewEditorInput);
218

219
				let state = undefined;
220
				if (webviewEditorInput.webview.state) {
221
					try {
222
						state = JSON.parse(webviewEditorInput.webview.state);
223 224 225 226 227 228
					} catch {
						// noop
					}
				}

				try {
229
					await this._proxy.$deserializeWebviewPanel(handle, viewType, webviewEditorInput.getTitle(), state, editorGroupToViewColumn(this._editorGroupService, webviewEditorInput.group || 0), webviewEditorInput.webview.options);
230 231
				} catch (error) {
					onUnexpectedError(error);
232
					webviewEditorInput.webview.html = MainThreadWebviews.getDeserializationFailedContents(viewType);
233 234 235
				}
			}
		}));
A
Alex Dima 已提交
236
	}
237 238 239 240 241 242 243 244 245

	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 已提交
246
	}
247

248 249 250 251 252
	public $registerEditorProvider(viewType: string): void {
		if (this._editorProviders.has(viewType)) {
			throw new Error(`Provider for ${viewType} already registered`);
		}

253
		this._editorProviders.set(viewType, this._webviewWorkbenchService.registerResolver({
254
			canResolve: (webviewEditorInput) => {
255
				return webviewEditorInput.getTypeId() !== WebviewInput.typeId && webviewEditorInput.viewType === viewType;
256
			},
257
			resolveWebview: async (webview) => {
258
				const handle = webview.id;
259 260 261
				this._webviewEditorInputs.add(handle, webview);
				this.hookupWebviewEventDelegate(handle, webview);

262 263 264 265 266 267
				if (webview instanceof CustomFileEditorInput) {
					webview.onWillSave(e => {
						e.waitUntil(this._proxy.$save(handle));
					});
				}

268 269
				try {
					await this._proxy.$resolveWebviewEditor(
270
						webview.getResource(),
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
						handle,
						viewType,
						webview.getTitle(),
						webview.webview.state,
						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);
	}

296
	private getInternalWebviewViewType(viewType: string): string {
297
		return `mainThreadWebview-${viewType}`;
A
Alex Dima 已提交
298
	}
299

M
Matt Bierner 已提交
300 301 302 303 304 305 306
	private fromInternalWebviewViewType(viewType: string): string | undefined {
		if (!startsWith(viewType, 'mainThreadWebview-')) {
			return undefined;
		}
		return viewType.replace(/^mainThreadWebview-/, '');
	}

307
	private hookupWebviewEventDelegate(handle: WebviewPanelHandle, input: WebviewInput) {
308 309 310 311
		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(() => {
312
				this._webviewEditorInputs.delete(handle);
313 314 315
			});
		});
		input.webview.onDidUpdateState((newState: any) => {
316
			const webview = this.tryGetWebviewEditorInput(handle);
317 318
			if (!webview || webview.isDisposed()) {
				return;
319
			}
320 321
			webview.webview.state = newState;
		});
322
		input.webview.onMissingCsp((extension: ExtensionIdentifier) => this._proxy.$onMissingCsp(handle, extension.value));
A
Alex Dima 已提交
323
	}
324

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

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

333 334 335 336 337 338 339 340 341 342
		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] = {
343 344
					visible: topLevelInput === group.activeEditor,
					active: topLevelInput === activeInput,
345 346 347 348
					position: editorGroupToViewColumn(this._editorGroupService, group.id),
				};
			}
		};
349

350 351 352 353 354 355 356
		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);
357 358
				}
			}
359
		}
360 361 362 363

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

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

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

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

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

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

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

415

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

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