mainThreadWebview.ts 18.6 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.
 *--------------------------------------------------------------------------------------------*/

M
Matt Bierner 已提交
6
import { createCancelablePromise } from 'vs/base/common/async';
7
import { onUnexpectedError } from 'vs/base/common/errors';
8
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
9
import { Schemas } from 'vs/base/common/network';
10
import { isWeb } from 'vs/base/common/platform';
11
import { startsWith } from 'vs/base/common/strings';
12
import { URI, UriComponents } from 'vs/base/common/uri';
A
Alex Dima 已提交
13
import * as modes from 'vs/editor/common/modes';
14
import { localize } from 'vs/nls';
A
Alex Dima 已提交
15
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
16
import { IFileService } from 'vs/platform/files/common/files';
17
import { IOpenerService } from 'vs/platform/opener/common/opener';
18
import { IProductService } from 'vs/platform/product/common/productService';
19
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
20
import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol';
21
import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor';
22 23
import { IEditorInput } from 'vs/workbench/common/editor';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
24
import { CustomFileEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput';
25
import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor';
26
import { WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview';
27
import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput';
28
import { ICreateWebViewShowOptions, IWebviewWorkbenchService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService';
29
import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
30
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
31
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
A
Alex Dima 已提交
32 33
import { extHostNamedCustomer } from '../common/extHostCustomers';

34 35 36
/**
 * Bi-directional map between webview handles and inputs.
 */
37
class WebviewInputStore {
38 39
	private readonly _handlesToInputs = new Map<string, WebviewInput>();
	private readonly _inputsToHandles = new Map<WebviewInput, string>();
40

41
	public add(handle: string, input: WebviewInput): void {
42 43 44 45
		this._handlesToInputs.set(handle, input);
		this._inputsToHandles.set(input, handle);
	}

46
	public getHandleForInput(input: WebviewInput): string | undefined {
47 48 49
		return this._inputsToHandles.get(input);
	}

50
	public getInputForHandle(handle: string): WebviewInput | undefined {
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
		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 已提交
67 68 69 70
class WebviewViewTypeTransformer {
	public constructor(
		public readonly prefix: string,
	) { }
M
Matt Bierner 已提交
71

M
Matt Bierner 已提交
72 73
	public fromExternal(viewType: string): string {
		return this.prefix + viewType;
M
Matt Bierner 已提交
74 75
	}

M
Matt Bierner 已提交
76 77 78
	public toExternal(viewType: string): string | undefined {
		return startsWith(viewType, this.prefix)
			? viewType.substr(this.prefix.length)
M
Matt Bierner 已提交
79 80 81 82
			: undefined;
	}
}

M
Matt Bierner 已提交
83 84
const webviewPanelViewType = new WebviewViewTypeTransformer('mainThreadWebview-');

85 86
@extHostNamedCustomer(extHostProtocol.MainContext.MainThreadWebviews)
export class MainThreadWebviews extends Disposable implements extHostProtocol.MainThreadWebviewsShape {
87 88

	private static readonly standardSupportedLinkSchemes = new Set([
M
Matt Bierner 已提交
89 90 91 92
		Schemas.http,
		Schemas.https,
		Schemas.mailto,
		Schemas.vscode,
93 94 95
		'vscode-insider',
	]);

96
	private readonly _proxy: extHostProtocol.ExtHostWebviewsShape;
97
	private readonly _webviewInputs = new WebviewInputStore();
98
	private readonly _revivers = new Map<string, IDisposable>();
99
	private readonly _editorProviders = new Map<string, IDisposable>();
100
	private readonly _customEditorModels = new Map<ICustomEditorModel, { referenceCount: number }>();
101 102

	constructor(
103
		context: extHostProtocol.IExtHostContext,
104
		@IExtensionService extensionService: IExtensionService,
105
		@ICustomEditorService private readonly _customEditorService: ICustomEditorService,
106 107 108 109
		@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
		@IEditorService private readonly _editorService: IEditorService,
		@IOpenerService private readonly _openerService: IOpenerService,
		@IProductService private readonly _productService: IProductService,
110 111
		@ITelemetryService private readonly _telemetryService: ITelemetryService,
		@IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService,
112
		@IFileService private readonly _fileService: IFileService,
113 114 115
	) {
		super();

116
		this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviews);
117 118
		this._register(_editorService.onDidActiveEditorChange(this.updateWebviewViewStates, this));
		this._register(_editorService.onDidVisibleEditorsChange(this.updateWebviewViewStates, this));
119

120
		// This reviver's only job is to activate webview panel extensions
121
		// This should trigger the real reviver to be registered from the extension host side.
122
		this._register(_webviewWorkbenchService.registerResolver({
123
			canResolve: (webview: WebviewInput) => {
M
Matt Bierner 已提交
124 125
				if (webview instanceof CustomFileEditorInput) {
					extensionService.activateByEvent(`onWebviewEditor:${webview.viewType}`);
M
Matt Bierner 已提交
126 127 128
					return false;
				}

M
Matt Bierner 已提交
129
				const viewType = webviewPanelViewType.toExternal(webview.viewType);
M
Matt Bierner 已提交
130
				if (typeof viewType === 'string') {
131 132 133 134
					extensionService.activateByEvent(`onWebviewPanel:${viewType}`);
				}
				return false;
			},
135
			resolveWebview: () => { throw new Error('not implemented'); }
136 137 138 139
		}));
	}

	public $createWebviewPanel(
140 141
		extensionData: extHostProtocol.WebviewExtensionDescription,
		handle: extHostProtocol.WebviewPanelHandle,
142 143
		viewType: string,
		title: string,
M
Matt Bierner 已提交
144
		showOptions: { viewColumn?: EditorViewColumn, preserveFocus?: boolean; },
145
		options: WebviewInputOptions
146 147 148 149 150 151 152
	): void {
		const mainThreadShowOptions: ICreateWebViewShowOptions = Object.create(null);
		if (showOptions) {
			mainThreadShowOptions.preserveFocus = !!showOptions.preserveFocus;
			mainThreadShowOptions.group = viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn);
		}

153
		const extension = reviveWebviewExtension(extensionData);
154
		const webview = this._webviewWorkbenchService.createWebview(handle, webviewPanelViewType.fromExternal(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), extension);
155
		this.hookupWebviewEventDelegate(handle, webview);
156

157
		this._webviewInputs.add(handle, webview);
158 159 160 161 162 163

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

167
	public $disposeWebview(handle: extHostProtocol.WebviewPanelHandle): void {
168
		const webview = this.getWebviewInput(handle);
169 170 171
		webview.dispose();
	}

172
	public $setTitle(handle: extHostProtocol.WebviewPanelHandle, value: string): void {
173
		const webview = this.getWebviewInput(handle);
174 175 176
		webview.setName(value);
	}

177
	public $setIconPath(handle: extHostProtocol.WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void {
178
		const webview = this.getWebviewInput(handle);
179 180 181
		webview.iconPath = reviveWebviewIcon(value);
	}

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

187
	public $setOptions(handle: extHostProtocol.WebviewPanelHandle, options: modes.IWebviewOptions): void {
188
		const webview = this.getWebviewInput(handle);
M
Matt Bierner 已提交
189
		webview.webview.contentOptions = reviveWebviewOptions(options);
190 191
	}

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

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

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

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

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

226 227 228
				const handle = webviewInput.id;
				this._webviewInputs.add(handle, webviewInput);
				this.hookupWebviewEventDelegate(handle, webviewInput);
229

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

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

	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 已提交
257
	}
258

259
	public $registerEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: readonly extHostProtocol.WebviewEditorCapabilities[]): void {
260 261 262 263
		if (this._editorProviders.has(viewType)) {
			throw new Error(`Provider for ${viewType} already registered`);
		}

264
		const extension = reviveWebviewExtension(extensionData);
M
Matt Bierner 已提交
265

266
		this._editorProviders.set(viewType, this._webviewWorkbenchService.registerResolver({
267
			canResolve: (webviewInput) => {
M
Matt Bierner 已提交
268
				return webviewInput instanceof CustomFileEditorInput && webviewInput.viewType === viewType;
269
			},
270
			resolveWebview: async (webviewInput: CustomFileEditorInput) => {
271 272 273
				const handle = webviewInput.id;
				this._webviewInputs.add(handle, webviewInput);
				this.hookupWebviewEventDelegate(handle, webviewInput);
274

275
				webviewInput.webview.options = options;
276
				webviewInput.webview.extension = extension;
277
				const resource = webviewInput.getResource();
M
Matt Bierner 已提交
278

279
				const model = await this.retainCustomEditorModel(webviewInput, resource, viewType, capabilities);
280
				webviewInput.onDisposeWebview(() => {
281
					this.releaseCustomEditorModel(model);
282 283
				});

284 285
				try {
					await this._proxy.$resolveWebviewEditor(
286
						resource,
287 288
						handle,
						viewType,
289 290 291
						webviewInput.getTitle(),
						editorGroupToViewColumn(this._editorGroupService, webviewInput.group || 0),
						webviewInput.webview.options
292 293 294
					);
				} catch (error) {
					onUnexpectedError(error);
295
					webviewInput.webview.html = MainThreadWebviews.getDeserializationFailedContents(viewType);
M
Matt Bierner 已提交
296
					return;
297 298 299 300 301 302 303 304 305 306 307 308 309
				}
			}
		}));
	}

	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);
310 311

		this._customEditorService.models.disposeAllModelsForView(viewType);
312 313
	}

314
	private async retainCustomEditorModel(webviewInput: WebviewInput, resource: URI, viewType: string, capabilities: readonly extHostProtocol.WebviewEditorCapabilities[]) {
B
Benjamin Pasero 已提交
315
		const model = await this._customEditorService.models.resolve(webviewInput.getResource(), webviewInput.viewType);
316 317 318 319 320 321

		const existingEntry = this._customEditorModels.get(model);
		if (existingEntry) {
			++existingEntry.referenceCount;
			// no need to hook up listeners again
			return model;
322 323
		}

324
		this._customEditorModels.set(model, { referenceCount: 1 });
325 326

		const capabilitiesSet = new Set(capabilities);
327 328
		const isEditable = capabilitiesSet.has(extHostProtocol.WebviewEditorCapabilities.Editable);
		if (isEditable) {
329 330 331 332 333 334
			model.onUndo(e => {
				this._proxy.$undoEdits(resource, viewType, e.edits);
			});

			model.onDisposeEdits(e => {
				this._proxy.$disposeEdits(e.edits);
335
			});
336

337 338 339
			model.onApplyEdit(e => {
				if (e.trigger !== model) {
					this._proxy.$applyEdits(resource, viewType, e.edits);
340 341 342
				}
			});

343
			model.onWillSave(e => {
344 345
				e.waitUntil(this._proxy.$onSave(resource.toJSON(), viewType));
			});
346
		}
347 348 349 350 351 352 353 354 355 356

		// Save as should always be implemented even if the model is readonly
		model.onWillSaveAs(e => {
			if (isEditable) {
				e.waitUntil(this._proxy.$onSaveAs(e.resource.toJSON(), viewType, e.targetResource.toJSON()));
			} else {
				// Since the editor is readonly, just copy the file over
				e.waitUntil(this._fileService.copy(e.resource, e.targetResource, false /* overwrite */));
			}
		});
357 358

		if (capabilitiesSet.has(extHostProtocol.WebviewEditorCapabilities.SupportsHotExit)) {
M
Matt Bierner 已提交
359 360 361 362
			model.onBackup(() => {
				return createCancelablePromise(token =>
					this._proxy.$backup(model.resource.toJSON(), viewType, token));
			});
363 364
		}

365 366 367 368 369 370 371 372 373 374 375 376 377 378
		return model;
	}

	private async releaseCustomEditorModel(model: ICustomEditorModel) {
		const entry = this._customEditorModels.get(model);
		if (!entry) {
			return;
		}

		--entry.referenceCount;
		if (entry.referenceCount <= 0) {
			this._customEditorService.models.disposeModel(model);
			this._customEditorModels.delete(model);
		}
379 380
	}

381
	public $onEdit(resource: UriComponents, viewType: string, editId: number): void {
382
		const model = this._customEditorService.models.get(URI.revive(resource), viewType);
383 384 385 386
		if (!model) {
			throw new Error('Could not find model for webview editor');
		}

387
		model.pushEdit(editId, model);
388 389
	}

390
	private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewPanelHandle, input: WebviewInput) {
391 392
		const disposables = new DisposableStore();

393
		disposables.add(input.webview.onDidClickLink((uri) => this.onDidClickLink(handle, uri)));
394 395 396 397 398
		disposables.add(input.webview.onMessage((message: any) => { this._proxy.$onMessage(handle, message); }));
		disposables.add(input.webview.onMissingCsp((extension: ExtensionIdentifier) => this._proxy.$onMissingCsp(handle, extension.value)));

		input.onDispose(() => {
			disposables.dispose();
399
		});
400 401 402 403 404
		input.onDisposeWebview(() => {
			this._proxy.$onDidDisposeWebviewPanel(handle).finally(() => {
				this._webviewInputs.delete(handle);
			});
		});
A
Alex Dima 已提交
405
	}
406

407
	private updateWebviewViewStates() {
408
		if (!this._webviewInputs.size) {
409 410
			return;
		}
411

412
		const activeInput = this._editorService.activeControl && this._editorService.activeControl.input;
413
		const viewStates: extHostProtocol.WebviewPanelViewStateData = {};
414

415 416 417 418 419 420 421
		const updateViewStatesForInput = (group: IEditorGroup, topLevelInput: IEditorInput, editorInput: IEditorInput) => {
			if (!(editorInput instanceof WebviewInput)) {
				return;
			}

			editorInput.updateGroup(group.id);

422
			const handle = this._webviewInputs.getHandleForInput(editorInput);
423 424
			if (handle) {
				viewStates[handle] = {
425 426
					visible: topLevelInput === group.activeEditor,
					active: topLevelInput === activeInput,
427 428 429 430
					position: editorGroupToViewColumn(this._editorGroupService, group.id),
				};
			}
		};
431

432 433 434 435 436 437 438
		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);
439 440
				}
			}
441
		}
442 443 444 445

		if (Object.keys(viewStates).length) {
			this._proxy.$onDidChangeWebviewPanelViewStates(viewStates);
		}
446 447
	}

448
	private onDidClickLink(handle: extHostProtocol.WebviewPanelHandle, link: string): void {
449
		const webview = this.getWebviewInput(handle);
450
		if (this.isSupportedLink(webview, URI.parse(link))) {
451
			this._openerService.open(link, { fromUserGesture: true });
452 453 454
		}
	}

455
	private isSupportedLink(webview: WebviewInput, link: URI): boolean {
456 457 458
		if (MainThreadWebviews.standardSupportedLinkSchemes.has(link.scheme)) {
			return true;
		}
459
		if (!isWeb && this._productService.urlProtocol === link.scheme) {
460 461
			return true;
		}
M
Matt Bierner 已提交
462
		return !!webview.webview.contentOptions.enableCommandUris && link.scheme === Schemas.command;
A
Alex Dima 已提交
463
	}
464

465
	private getWebviewInput(handle: extHostProtocol.WebviewPanelHandle): WebviewInput {
466
		const webview = this.tryGetWebviewInput(handle);
467 468 469 470 471 472
		if (!webview) {
			throw new Error('Unknown webview handle:' + handle);
		}
		return webview;
	}

473
	private tryGetWebviewInput(handle: extHostProtocol.WebviewPanelHandle): WebviewInput | undefined {
474
		return this._webviewInputs.getInputForHandle(handle);
475 476
	}

477 478 479 480 481
	private static getDeserializationFailedContents(viewType: string) {
		return `<!DOCTYPE html>
		<html>
			<head>
				<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
482
				<meta http-equiv="Content-Security-Policy" content="default-src 'none';">
483 484 485
			</head>
			<body>${localize('errorMessage', "An error occurred while restoring view:{0}", viewType)}</body>
		</html>`;
A
Alex Dima 已提交
486
	}
487 488
}

489 490
function reviveWebviewExtension(extensionData: extHostProtocol.WebviewExtensionDescription): WebviewExtensionDescription {
	return { id: extensionData.id, location: URI.revive(extensionData.location) };
M
Matt Bierner 已提交
491 492
}

493
function reviveWebviewOptions(options: modes.IWebviewOptions): WebviewInputOptions {
494 495
	return {
		...options,
496
		allowScripts: options.enableScripts,
497 498 499 500 501
		localResourceRoots: Array.isArray(options.localResourceRoots) ? options.localResourceRoots.map(r => URI.revive(r)) : undefined,
	};
}

function reviveWebviewIcon(
M
Matt Bierner 已提交
502 503
	value: { light: UriComponents, dark: UriComponents; } | undefined
): { light: URI, dark: URI; } | undefined {
504 505
	if (!value) {
		return undefined;
A
Alex Dima 已提交
506
	}
507 508 509 510 511

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