mainThreadWebview.ts 14.7 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 8 9
import { onUnexpectedError } from 'vs/base/common/errors';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import * as map from 'vs/base/common/map';
import { URI, UriComponents } from 'vs/base/common/uri';
A
Alex Dima 已提交
10
import * as modes from 'vs/editor/common/modes';
11
import { localize } from 'vs/nls';
A
Alex Dima 已提交
12
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
13 14 15 16 17 18 19 20 21
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ExtHostContext, ExtHostWebviewsShape, IExtHostContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle, WebviewPanelShowOptions } from 'vs/workbench/api/common/extHost.protocol';
import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor';
import { WebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput';
import { ICreateWebViewShowOptions, IWebviewEditorService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewEditorService';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { ACTIVE_GROUP, IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
A
Alex Dima 已提交
22
import { extHostNamedCustomer } from '../common/extHostCustomers';
23
import { IProductService } from 'vs/platform/product/common/product';
M
Matt Bierner 已提交
24
import { startsWith } from 'vs/base/common/strings';
M
Matt Bierner 已提交
25
import { Webview } from 'vs/workbench/contrib/webview/browser/webview';
A
Alex Dima 已提交
26

M
Matt Bierner 已提交
27
interface OldMainThreadWebviewState {
M
Matt Bierner 已提交
28 29 30 31
	readonly viewType: string;
	state: any;
}

A
Alex Dima 已提交
32 33
@extHostNamedCustomer(MainContext.MainThreadWebviews)
export class MainThreadWebviews extends Disposable implements MainThreadWebviewsShape {
34 35 36 37 38 39 40 41 42 43 44 45

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

	private static revivalPool = 0;

	private readonly _proxy: ExtHostWebviewsShape;
46 47
	private readonly _webviewEditorInputs = new Map<string, WebviewEditorInput>();
	private readonly _webviews = new Map<string, Webview>();
48 49 50 51 52 53 54 55 56
	private readonly _revivers = new Map<string, IDisposable>();

	private _activeWebview: WebviewPanelHandle | undefined = undefined;

	constructor(
		context: IExtHostContext,
		@IExtensionService extensionService: IExtensionService,
		@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
		@IEditorService private readonly _editorService: IEditorService,
M
Matt Bierner 已提交
57
		@IWebviewEditorService private readonly _webviewEditorService: IWebviewEditorService,
58 59 60 61 62 63 64 65 66 67 68 69
		@IOpenerService private readonly _openerService: IOpenerService,
		@ITelemetryService private readonly _telemetryService: ITelemetryService,
		@IProductService private readonly _productService: IProductService,
	) {
		super();

		this._proxy = context.getProxy(ExtHostContext.ExtHostWebviews);
		this._register(_editorService.onDidActiveEditorChange(this.onActiveEditorChanged, this));
		this._register(_editorService.onDidVisibleEditorsChange(this.onVisibleEditorsChanged, this));

		// This reviver's only job is to activate webview extensions
		// This should trigger the real reviver to be registered from the extension host side.
M
Matt Bierner 已提交
70
		this._register(_webviewEditorService.registerReviver({
M
Matt Bierner 已提交
71
			canRevive: (webview: WebviewEditorInput) => {
72
				if (!webview.webview.state) {
M
Matt Bierner 已提交
73 74 75 76
					return false;
				}

				const viewType = this.fromInternalWebviewViewType(webview.viewType);
M
Matt Bierner 已提交
77
				if (typeof viewType === 'string') {
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
					extensionService.activateByEvent(`onWebviewPanel:${viewType}`);
				}
				return false;
			},
			reviveWebview: () => { throw new Error('not implemented'); }
		}));
	}

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

101
		const webview = this._webviewEditorService.createWebview(handle, this.getInternalWebviewViewType(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), {
102 103
			location: URI.revive(extensionLocation),
			id: extensionId
104 105
		});
		this.hookupWebviewEventDelegate(handle, webview);
106

107 108
		this._webviewEditorInputs.set(handle, webview);
		this._webviews.set(handle, webview.webview);
109 110 111 112 113 114 115 116 117 118

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

	public $disposeWebview(handle: WebviewPanelHandle): void {
119
		const webview = this.getWebviewEditorInput(handle);
120 121 122 123
		webview.dispose();
	}

	public $setTitle(handle: WebviewPanelHandle, value: string): void {
124
		const webview = this.getWebviewEditorInput(handle);
125 126 127 128
		webview.setName(value);
	}

	public $setIconPath(handle: WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents } | undefined): void {
129
		const webview = this.getWebviewEditorInput(handle);
130 131 132 133 134
		webview.iconPath = reviveWebviewIcon(value);
	}

	public $setHtml(handle: WebviewPanelHandle, value: string): void {
		const webview = this.getWebview(handle);
135
		webview.html = value;
136 137 138 139
	}

	public $setOptions(handle: WebviewPanelHandle, options: modes.IWebviewOptions): void {
		const webview = this.getWebview(handle);
140
		webview.contentOptions = reviveWebviewOptions(options as any /*todo@mat */);
141 142 143
	}

	public $reveal(handle: WebviewPanelHandle, showOptions: WebviewPanelShowOptions): void {
144
		const webview = this.getWebviewEditorInput(handle);
145 146 147 148 149 150
		if (webview.isDisposed()) {
			return;
		}

		const targetGroup = this._editorGroupService.getGroup(viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn)) || this._editorGroupService.getGroup(webview.group || 0);
		if (targetGroup) {
M
Matt Bierner 已提交
151
			this._webviewEditorService.revealWebview(webview, targetGroup, !!showOptions.preserveFocus);
152 153 154 155 156
		}
	}

	public async $postMessage(handle: WebviewPanelHandle, message: any): Promise<boolean> {
		const webview = this.getWebview(handle);
157
		webview.sendMessage(message);
158
		return true;
A
Alex Dima 已提交
159
	}
160 161 162 163 164 165

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

M
Matt Bierner 已提交
166
		this._revivers.set(viewType, this._webviewEditorService.registerReviver({
167 168
			canRevive: (webviewEditorInput) => {
				return !!webviewEditorInput.webview.state && webviewEditorInput.viewType === this.getInternalWebviewViewType(viewType);
169
			},
170 171
			reviveWebview: async (webviewEditorInput): Promise<void> => {
				const viewType = this.fromInternalWebviewViewType(webviewEditorInput.viewType);
M
Matt Bierner 已提交
172
				if (!viewType) {
173
					webviewEditorInput.webview.html = MainThreadWebviews.getDeserializationFailedContents(webviewEditorInput.viewType);
M
Matt Bierner 已提交
174 175 176 177
					return;
				}

				const handle = `revival-${MainThreadWebviews.revivalPool++}`;
178 179
				this._webviewEditorInputs.set(handle, webviewEditorInput);
				this._webviews.set(handle, webviewEditorInput.webview);
180
				this.hookupWebviewEventDelegate(handle, webviewEditorInput);
181

182
				let state = undefined;
183
				if (webviewEditorInput.webview.state) {
184
					try {
M
Matt Bierner 已提交
185 186 187
						// Check for old-style webview state first which stored state inside another state object
						// TODO: remove this after 1.37 ships.
						if (
188 189
							typeof (webviewEditorInput.webview.state as unknown as OldMainThreadWebviewState).viewType === 'string' &&
							'state' in (webviewEditorInput.webview.state as unknown as OldMainThreadWebviewState)
M
Matt Bierner 已提交
190
						) {
191
							state = JSON.parse((webviewEditorInput.webview.state as any).state);
M
Matt Bierner 已提交
192
						} else {
193
							state = JSON.parse(webviewEditorInput.webview.state);
M
Matt Bierner 已提交
194
						}
195 196 197 198 199 200
					} catch {
						// noop
					}
				}

				try {
201
					await this._proxy.$deserializeWebviewPanel(handle, viewType, webviewEditorInput.getTitle(), state, editorGroupToViewColumn(this._editorGroupService, webviewEditorInput.group || 0), webviewEditorInput.webview.options);
202 203
				} catch (error) {
					onUnexpectedError(error);
204
					webviewEditorInput.webview.html = MainThreadWebviews.getDeserializationFailedContents(viewType);
205 206 207
				}
			}
		}));
A
Alex Dima 已提交
208
	}
209 210 211 212 213 214 215 216 217

	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 已提交
218
	}
219

220
	private getInternalWebviewViewType(viewType: string): string {
221
		return `mainThreadWebview-${viewType}`;
A
Alex Dima 已提交
222
	}
223

M
Matt Bierner 已提交
224 225 226 227 228 229 230
	private fromInternalWebviewViewType(viewType: string): string | undefined {
		if (!startsWith(viewType, 'mainThreadWebview-')) {
			return undefined;
		}
		return viewType.replace(/^mainThreadWebview-/, '');
	}

231 232 233 234 235
	private hookupWebviewEventDelegate(handle: WebviewPanelHandle, input: WebviewEditorInput) {
		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(() => {
236
				this._webviewEditorInputs.delete(handle);
237 238 239 240
				this._webviews.delete(handle);
			});
		});
		input.webview.onDidUpdateState((newState: any) => {
241
			const webview = this.tryGetWebviewEditorInput(handle);
242 243
			if (!webview || webview.isDisposed()) {
				return;
244
			}
245 246
			webview.webview.state = newState;
		});
A
Alex Dima 已提交
247
	}
248 249 250 251 252

	private onActiveEditorChanged() {
		const activeEditor = this._editorService.activeControl;
		let newActiveWebview: { input: WebviewEditorInput, handle: WebviewPanelHandle } | undefined = undefined;
		if (activeEditor && activeEditor.input instanceof WebviewEditorInput) {
253 254
			for (const handle of map.keys(this._webviewEditorInputs)) {
				const input = this._webviewEditorInputs.get(handle)!;
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
				if (input.matches(activeEditor.input)) {
					newActiveWebview = { input, handle };
					break;
				}
			}
		}

		if (newActiveWebview && newActiveWebview.handle === this._activeWebview) {
			// Webview itself unchanged but position may have changed
			this._proxy.$onDidChangeWebviewPanelViewState(newActiveWebview.handle, {
				active: true,
				visible: true,
				position: editorGroupToViewColumn(this._editorGroupService, newActiveWebview.input.group || 0)
			});
			return;
		}

		// Broadcast view state update for currently active
		if (typeof this._activeWebview !== 'undefined') {
274
			const oldActiveWebview = this._webviewEditorInputs.get(this._activeWebview);
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
			if (oldActiveWebview) {
				this._proxy.$onDidChangeWebviewPanelViewState(this._activeWebview, {
					active: false,
					visible: this._editorService.visibleControls.some(editor => !!editor.input && editor.input.matches(oldActiveWebview)),
					position: editorGroupToViewColumn(this._editorGroupService, oldActiveWebview.group || 0),
				});
			}
		}

		// Then for newly active
		if (newActiveWebview) {
			this._proxy.$onDidChangeWebviewPanelViewState(newActiveWebview.handle, {
				active: true,
				visible: true,
				position: editorGroupToViewColumn(this._editorGroupService, activeEditor ? activeEditor.group : ACTIVE_GROUP),
			});
			this._activeWebview = newActiveWebview.handle;
		} else {
			this._activeWebview = undefined;
		}
A
Alex Dima 已提交
295
	}
296 297

	private onVisibleEditorsChanged(): void {
298
		this._webviewEditorInputs.forEach((input, handle) => {
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319
			for (const workbenchEditor of this._editorService.visibleControls) {
				if (workbenchEditor.input && workbenchEditor.input.matches(input)) {
					const editorPosition = editorGroupToViewColumn(this._editorGroupService, workbenchEditor.group!);

					input.updateGroup(workbenchEditor.group!.id);
					this._proxy.$onDidChangeWebviewPanelViewState(handle, {
						active: handle === this._activeWebview,
						visible: true,
						position: editorPosition
					});
					break;
				}
			}
		});
	}

	private onDidClickLink(handle: WebviewPanelHandle, link: URI): void {
		if (!link) {
			return;
		}

320
		const webview = this.getWebviewEditorInput(handle);
321 322 323 324 325 326 327 328 329
		if (this.isSupportedLink(webview, link)) {
			this._openerService.open(link);
		}
	}

	private isSupportedLink(webview: WebviewEditorInput, link: URI): boolean {
		if (MainThreadWebviews.standardSupportedLinkSchemes.has(link.scheme)) {
			return true;
		}
330
		if (this._productService.urlProtocol === link.scheme) {
331 332
			return true;
		}
333
		return !!webview.webview.contentOptions.enableCommandUris && link.scheme === 'command';
A
Alex Dima 已提交
334
	}
335

336 337 338 339 340 341 342 343 344 345 346 347 348
	private getWebviewEditorInput(handle: WebviewPanelHandle): WebviewEditorInput {
		const webview = this.tryGetWebviewEditorInput(handle);
		if (!webview) {
			throw new Error('Unknown webview handle:' + handle);
		}
		return webview;
	}

	private tryGetWebviewEditorInput(handle: WebviewPanelHandle): WebviewEditorInput | undefined {
		return this._webviewEditorInputs.get(handle);
	}

	private getWebview(handle: WebviewPanelHandle): Webview {
M
Matt Bierner 已提交
349
		const webview = this.tryGetWebview(handle);
350 351 352 353
		if (!webview) {
			throw new Error('Unknown webview handle:' + handle);
		}
		return webview;
A
Alex Dima 已提交
354
	}
355

356
	private tryGetWebview(handle: WebviewPanelHandle): Webview | undefined {
M
Matt Bierner 已提交
357 358 359
		return this._webviews.get(handle);
	}

360 361 362 363 364 365
	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">
366
				<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https: data:; media-src https:; script-src 'none'; style-src vscode-resource: https: 'unsafe-inline'; child-src 'none'; frame-src 'none';">
367 368 369
			</head>
			<body>${localize('errorMessage', "An error occurred while restoring view:{0}", viewType)}</body>
		</html>`;
A
Alex Dima 已提交
370
	}
371 372
}

373
function reviveWebviewOptions(options: modes.IWebviewOptions): WebviewInputOptions {
374 375
	return {
		...options,
376
		allowScripts: options.enableScripts,
377 378 379 380
		localResourceRoots: Array.isArray(options.localResourceRoots) ? options.localResourceRoots.map(r => URI.revive(r)) : undefined,
	};
}

381

382 383 384 385 386
function reviveWebviewIcon(
	value: { light: UriComponents, dark: UriComponents } | undefined
): { light: URI, dark: URI } | undefined {
	if (!value) {
		return undefined;
A
Alex Dima 已提交
387
	}
388 389 390 391 392

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