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 { Schemas } from 'vs/base/common/network';
9
import { isWeb } from 'vs/base/common/platform';
10
import { startsWith } from 'vs/base/common/strings';
11
import { URI, UriComponents } from 'vs/base/common/uri';
A
Alex Dima 已提交
12
import * as modes from 'vs/editor/common/modes';
13
import { localize } from 'vs/nls';
A
Alex Dima 已提交
14
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
15
import { IOpenerService } from 'vs/platform/opener/common/opener';
16
import { IProductService } from 'vs/platform/product/common/productService';
17
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
18
import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol';
19
import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor';
20 21
import { IEditorInput } from 'vs/workbench/common/editor';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
22
import { CustomFileEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput';
23
import { WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview';
24
import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput';
25
import { ICreateWebViewShowOptions, IWebviewWorkbenchService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService';
26
import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
27
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
28
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
A
Alex Dima 已提交
29 30
import { extHostNamedCustomer } from '../common/extHostCustomers';

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

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

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

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

M
Matt Bierner 已提交
69 70
	public fromExternal(viewType: string): string {
		return this.prefix + viewType;
M
Matt Bierner 已提交
71 72
	}

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

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

82 83
@extHostNamedCustomer(extHostProtocol.MainContext.MainThreadWebviews)
export class MainThreadWebviews extends Disposable implements extHostProtocol.MainThreadWebviewsShape {
84 85

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

93
	private readonly _proxy: extHostProtocol.ExtHostWebviewsShape;
94
	private readonly _webviewInputs = new WebviewInputStore();
95
	private readonly _revivers = new Map<string, IDisposable>();
96
	private readonly _editorProviders = new Map<string, IDisposable>();
97 98

	constructor(
99
		context: extHostProtocol.IExtHostContext,
100 101 102 103 104
		@IExtensionService extensionService: IExtensionService,
		@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
		@IEditorService private readonly _editorService: IEditorService,
		@IOpenerService private readonly _openerService: IOpenerService,
		@IProductService private readonly _productService: IProductService,
105 106
		@ITelemetryService private readonly _telemetryService: ITelemetryService,
		@IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService,
107 108 109
	) {
		super();

110
		this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviews);
111 112
		this._register(_editorService.onDidActiveEditorChange(this.updateWebviewViewStates, this));
		this._register(_editorService.onDidVisibleEditorsChange(this.updateWebviewViewStates, this));
113

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

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

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

147
		const extension = reviveWebviewExtension(extensionData);
148
		const webview = this._webviewWorkbenchService.createWebview(handle, webviewPanelViewType.fromExternal(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), extension);
149
		this.hookupWebviewEventDelegate(handle, webview);
150

151
		this._webviewInputs.add(handle, webview);
152 153 154 155 156 157

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

161
	public $disposeWebview(handle: extHostProtocol.WebviewPanelHandle): void {
162
		const webview = this.getWebviewInput(handle);
163 164 165
		webview.dispose();
	}

166
	public $setTitle(handle: extHostProtocol.WebviewPanelHandle, value: string): void {
167
		const webview = this.getWebviewInput(handle);
168 169 170
		webview.setName(value);
	}

171
	public $setIconPath(handle: extHostProtocol.WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void {
172
		const webview = this.getWebviewInput(handle);
173 174 175
		webview.iconPath = reviveWebviewIcon(value);
	}

176
	public $setHtml(handle: extHostProtocol.WebviewPanelHandle, value: string): void {
177
		const webview = this.getWebviewInput(handle);
178
		webview.webview.html = value;
179 180
	}

181
	public $setOptions(handle: extHostProtocol.WebviewPanelHandle, options: modes.IWebviewOptions): void {
182
		const webview = this.getWebviewInput(handle);
M
Matt Bierner 已提交
183
		webview.webview.contentOptions = reviveWebviewOptions(options);
184 185
	}

186
	public $reveal(handle: extHostProtocol.WebviewPanelHandle, showOptions: extHostProtocol.WebviewPanelShowOptions): void {
187
		const webview = this.getWebviewInput(handle);
188 189 190 191 192 193
		if (webview.isDisposed()) {
			return;
		}

		const targetGroup = this._editorGroupService.getGroup(viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn)) || this._editorGroupService.getGroup(webview.group || 0);
		if (targetGroup) {
194
			this._webviewWorkbenchService.revealWebview(webview, targetGroup, !!showOptions.preserveFocus);
195 196 197
		}
	}

198
	public async $postMessage(handle: extHostProtocol.WebviewPanelHandle, message: any): Promise<boolean> {
199
		const webview = this.getWebviewInput(handle);
200
		webview.webview.sendMessage(message);
201
		return true;
A
Alex Dima 已提交
202
	}
203 204 205 206 207 208

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

209
		this._revivers.set(viewType, this._webviewWorkbenchService.registerResolver({
210 211
			canResolve: (webviewInput) => {
				return webviewInput.viewType === webviewPanelViewType.fromExternal(viewType);
212
			},
213 214
			resolveWebview: async (webviewInput): Promise<void> => {
				const viewType = webviewPanelViewType.toExternal(webviewInput.viewType);
M
Matt Bierner 已提交
215
				if (!viewType) {
216
					webviewInput.webview.html = MainThreadWebviews.getDeserializationFailedContents(webviewInput.viewType);
M
Matt Bierner 已提交
217 218 219
					return;
				}

220 221 222
				const handle = webviewInput.id;
				this._webviewInputs.add(handle, webviewInput);
				this.hookupWebviewEventDelegate(handle, webviewInput);
223

224
				let state = undefined;
225
				if (webviewInput.webview.state) {
226
					try {
227
						state = JSON.parse(webviewInput.webview.state);
228 229 230 231 232 233
					} catch {
						// noop
					}
				}

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

	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 已提交
251
	}
252

253
	public $registerEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions): void {
254 255 256 257
		if (this._editorProviders.has(viewType)) {
			throw new Error(`Provider for ${viewType} already registered`);
		}

258
		const extension = reviveWebviewExtension(extensionData);
M
Matt Bierner 已提交
259

260
		this._editorProviders.set(viewType, this._webviewWorkbenchService.registerResolver({
261
			canResolve: (webviewInput) => {
M
Matt Bierner 已提交
262
				return webviewInput instanceof CustomFileEditorInput && webviewInput.viewType === viewType;
263
			},
264 265 266 267
			resolveWebview: async (webviewInput) => {
				const handle = webviewInput.id;
				this._webviewInputs.add(handle, webviewInput);
				this.hookupWebviewEventDelegate(handle, webviewInput);
268

269
				webviewInput.webview.options = options;
270
				webviewInput.webview.extension = extension;
M
Matt Bierner 已提交
271

272 273
				try {
					await this._proxy.$resolveWebviewEditor(
274
						webviewInput.getResource(),
275 276
						handle,
						viewType,
277 278 279
						webviewInput.getTitle(),
						editorGroupToViewColumn(this._editorGroupService, webviewInput.group || 0),
						webviewInput.webview.options
280 281 282
					);
				} catch (error) {
					onUnexpectedError(error);
283
					webviewInput.webview.html = MainThreadWebviews.getDeserializationFailedContents(viewType);
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
				}
			}
		}));
	}

	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);
	}

299 300 301 302 303 304 305
	public $onEdit(handle: extHostProtocol.WebviewPanelHandle, editJson: string): void {
		const webview = this.getWebviewInput(handle);
		if (!(webview instanceof CustomFileEditorInput)) {
			throw new Error(`Webview is not a webview editor`);
		}
	}

306
	private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewPanelHandle, input: WebviewInput) {
307 308 309 310
		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(() => {
311
				this._webviewInputs.delete(handle);
312 313
			});
		});
314
		input.webview.onMissingCsp((extension: ExtensionIdentifier) => this._proxy.$onMissingCsp(handle, extension.value));
A
Alex Dima 已提交
315
	}
316

317
	private updateWebviewViewStates() {
318
		if (!this._webviewInputs.size) {
319 320
			return;
		}
321

322
		const activeInput = this._editorService.activeControl && this._editorService.activeControl.input;
323
		const viewStates: extHostProtocol.WebviewPanelViewStateData = {};
324

325 326 327 328 329 330 331
		const updateViewStatesForInput = (group: IEditorGroup, topLevelInput: IEditorInput, editorInput: IEditorInput) => {
			if (!(editorInput instanceof WebviewInput)) {
				return;
			}

			editorInput.updateGroup(group.id);

332
			const handle = this._webviewInputs.getHandleForInput(editorInput);
333 334
			if (handle) {
				viewStates[handle] = {
335 336
					visible: topLevelInput === group.activeEditor,
					active: topLevelInput === activeInput,
337 338 339 340
					position: editorGroupToViewColumn(this._editorGroupService, group.id),
				};
			}
		};
341

342 343 344 345 346 347 348
		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);
349 350
				}
			}
351
		}
352 353 354 355

		if (Object.keys(viewStates).length) {
			this._proxy.$onDidChangeWebviewPanelViewStates(viewStates);
		}
356 357
	}

358
	private onDidClickLink(handle: extHostProtocol.WebviewPanelHandle, link: URI): void {
359
		const webview = this.getWebviewInput(handle);
360
		if (this.isSupportedLink(webview, link)) {
361
			this._openerService.open(link, { fromUserGesture: true });
362 363 364
		}
	}

365
	private isSupportedLink(webview: WebviewInput, link: URI): boolean {
366 367 368
		if (MainThreadWebviews.standardSupportedLinkSchemes.has(link.scheme)) {
			return true;
		}
369
		if (!isWeb && this._productService.urlProtocol === link.scheme) {
370 371
			return true;
		}
M
Matt Bierner 已提交
372
		return !!webview.webview.contentOptions.enableCommandUris && link.scheme === Schemas.command;
A
Alex Dima 已提交
373
	}
374

375
	private getWebviewInput(handle: extHostProtocol.WebviewPanelHandle): WebviewInput {
376
		const webview = this.tryGetWebviewInput(handle);
377 378 379 380 381 382
		if (!webview) {
			throw new Error('Unknown webview handle:' + handle);
		}
		return webview;
	}

383
	private tryGetWebviewInput(handle: extHostProtocol.WebviewPanelHandle): WebviewInput | undefined {
384
		return this._webviewInputs.getInputForHandle(handle);
385 386
	}

387 388 389 390 391
	private static getDeserializationFailedContents(viewType: string) {
		return `<!DOCTYPE html>
		<html>
			<head>
				<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
392
				<meta http-equiv="Content-Security-Policy" content="default-src 'none';">
393 394 395
			</head>
			<body>${localize('errorMessage', "An error occurred while restoring view:{0}", viewType)}</body>
		</html>`;
A
Alex Dima 已提交
396
	}
397 398
}

399 400
function reviveWebviewExtension(extensionData: extHostProtocol.WebviewExtensionDescription): WebviewExtensionDescription {
	return { id: extensionData.id, location: URI.revive(extensionData.location) };
M
Matt Bierner 已提交
401 402
}

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

function reviveWebviewIcon(
M
Matt Bierner 已提交
412 413
	value: { light: UriComponents, dark: UriComponents; } | undefined
): { light: URI, dark: URI; } | undefined {
414 415
	if (!value) {
		return undefined;
A
Alex Dima 已提交
416
	}
417 418 419 420 421

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