mainThreadWebview.ts 31.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
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
7
import { CancellationToken } from 'vs/base/common/cancellation';
8
import { onUnexpectedError } from 'vs/base/common/errors';
9
import { Emitter, Event } from 'vs/base/common/event';
10
import { Disposable, DisposableStore, dispose, IDisposable, IReference } from 'vs/base/common/lifecycle';
11
import { Schemas } from 'vs/base/common/network';
12
import { basename } from 'vs/base/common/path';
13
import { isWeb } from 'vs/base/common/platform';
14
import { isEqual, isEqualOrParent } from 'vs/base/common/resources';
M
Matt Bierner 已提交
15
import { escape } from 'vs/base/common/strings';
16
import { URI, UriComponents } from 'vs/base/common/uri';
A
Alex Dima 已提交
17
import * as modes from 'vs/editor/common/modes';
18
import { localize } from 'vs/nls';
A
Alex Dima 已提交
19
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
20
import { IFileService } from 'vs/platform/files/common/files';
21 22
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILabelService } from 'vs/platform/label/common/label';
23
import { IOpenerService } from 'vs/platform/opener/common/opener';
24
import { IProductService } from 'vs/platform/product/common/productService';
25
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
26
import { IUndoRedoService, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo';
27
import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol';
28
import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor';
29
import { IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor';
30
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
31
import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput';
32
import { CustomDocumentBackupData } from 'vs/workbench/contrib/customEditor/browser/customEditorInputFactory';
33
import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor';
34
import { CustomTextEditorModel } from 'vs/workbench/contrib/customEditor/common/customTextEditorModel';
M
Matt Bierner 已提交
35
import { WebviewExtensionDescription, WebviewIcons } from 'vs/workbench/contrib/webview/browser/webview';
36
import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput';
37
import { ICreateWebViewShowOptions, IWebviewWorkbenchService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService';
38
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
39
import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
40
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
41
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
42
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
43
import { IWorkingCopy, IWorkingCopyBackup, IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService';
A
Alex Dima 已提交
44 45
import { extHostNamedCustomer } from '../common/extHostCustomers';

46 47 48
/**
 * Bi-directional map between webview handles and inputs.
 */
49
class WebviewInputStore {
50 51
	private readonly _handlesToInputs = new Map<string, WebviewInput>();
	private readonly _inputsToHandles = new Map<WebviewInput, string>();
52

53
	public add(handle: string, input: WebviewInput): void {
54 55 56 57
		this._handlesToInputs.set(handle, input);
		this._inputsToHandles.set(input, handle);
	}

58
	public getHandleForInput(input: WebviewInput): string | undefined {
59 60 61
		return this._inputsToHandles.get(input);
	}

62
	public getInputForHandle(handle: string): WebviewInput | undefined {
63 64 65 66 67 68 69 70 71 72 73 74 75 76
		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;
	}
77 78 79 80

	[Symbol.iterator](): Iterator<WebviewInput> {
		return this._handlesToInputs.values();
	}
81 82
}

M
Matt Bierner 已提交
83 84 85 86
class WebviewViewTypeTransformer {
	public constructor(
		public readonly prefix: string,
	) { }
M
Matt Bierner 已提交
87

M
Matt Bierner 已提交
88 89
	public fromExternal(viewType: string): string {
		return this.prefix + viewType;
M
Matt Bierner 已提交
90 91
	}

M
Matt Bierner 已提交
92
	public toExternal(viewType: string): string | undefined {
M
Matt Bierner 已提交
93
		return viewType.startsWith(this.prefix)
M
Matt Bierner 已提交
94
			? viewType.substr(this.prefix.length)
M
Matt Bierner 已提交
95 96 97 98
			: undefined;
	}
}

99 100 101 102 103
const enum ModelType {
	Custom,
	Text,
}

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

106 107
@extHostNamedCustomer(extHostProtocol.MainContext.MainThreadWebviews)
export class MainThreadWebviews extends Disposable implements extHostProtocol.MainThreadWebviewsShape {
108 109

	private static readonly standardSupportedLinkSchemes = new Set([
M
Matt Bierner 已提交
110 111 112 113
		Schemas.http,
		Schemas.https,
		Schemas.mailto,
		Schemas.vscode,
114 115 116
		'vscode-insider',
	]);

117
	private readonly _proxy: extHostProtocol.ExtHostWebviewsShape;
118
	private readonly _webviewInputs = new WebviewInputStore();
119
	private readonly _revivers = new Map<string, IDisposable>();
120
	private readonly _editorProviders = new Map<string, IDisposable>();
J
Jean Pierre 已提交
121
	private readonly _webviewFromDiffEditorHandles = new Set<string>();
122 123

	constructor(
124
		context: extHostProtocol.IExtHostContext,
125
		@IExtensionService extensionService: IExtensionService,
126 127
		@IWorkingCopyService workingCopyService: IWorkingCopyService,
		@IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService,
128
		@ICustomEditorService private readonly _customEditorService: ICustomEditorService,
129 130 131 132
		@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
		@IEditorService private readonly _editorService: IEditorService,
		@IOpenerService private readonly _openerService: IOpenerService,
		@IProductService private readonly _productService: IProductService,
133 134
		@ITelemetryService private readonly _telemetryService: ITelemetryService,
		@IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService,
135
		@IInstantiationService private readonly _instantiationService: IInstantiationService,
M
Matt Bierner 已提交
136
		@IBackupFileService private readonly _backupService: IBackupFileService,
137 138 139
	) {
		super();

140
		this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviews);
M
Matt Bierner 已提交
141

J
Jean Pierre 已提交
142
		this._register(_editorService.onDidActiveEditorChange(() => {
M
Matt Bierner 已提交
143
			const activeInput = this._editorService.activeEditor;
J
Jean Pierre 已提交
144 145 146 147 148 149
			if (activeInput instanceof DiffEditorInput && activeInput.master instanceof WebviewInput && activeInput.details instanceof WebviewInput) {
				this.registerWebviewFromDiffEditorListeners(activeInput);
			}

			this.updateWebviewViewStates(activeInput);
		}));
M
Matt Bierner 已提交
150 151 152 153

		this._register(_editorService.onDidVisibleEditorsChange(() => {
			this.updateWebviewViewStates(this._editorService.activeEditor);
		}));
154

155
		// This reviver's only job is to activate extensions.
156
		// This should trigger the real reviver to be registered from the extension host side.
157
		this._register(_webviewWorkbenchService.registerResolver({
158
			canResolve: (webview: WebviewInput) => {
159
				if (webview instanceof CustomEditorInput) {
160
					extensionService.activateByEvent(`onCustomEditor:${webview.viewType}`);
M
Matt Bierner 已提交
161 162 163
					return false;
				}

M
Matt Bierner 已提交
164
				const viewType = webviewPanelViewType.toExternal(webview.viewType);
M
Matt Bierner 已提交
165
				if (typeof viewType === 'string') {
166 167 168 169
					extensionService.activateByEvent(`onWebviewPanel:${viewType}`);
				}
				return false;
			},
170
			resolveWebview: () => { throw new Error('not implemented'); }
171
		}));
172 173 174 175 176 177 178 179 180 181 182 183 184 185

		workingCopyFileService.registerWorkingCopyProvider((editorResource) => {
			const matchedWorkingCopies: IWorkingCopy[] = [];

			for (const workingCopy of workingCopyService.workingCopies) {
				if (workingCopy instanceof MainThreadCustomEditorModel) {
					if (isEqualOrParent(editorResource, workingCopy.editorResource)) {
						matchedWorkingCopies.push(workingCopy);
					}
				}
			}
			return matchedWorkingCopies;

		});
186 187 188
	}

	public $createWebviewPanel(
189 190
		extensionData: extHostProtocol.WebviewExtensionDescription,
		handle: extHostProtocol.WebviewPanelHandle,
191 192
		viewType: string,
		title: string,
M
Matt Bierner 已提交
193
		showOptions: { viewColumn?: EditorViewColumn, preserveFocus?: boolean; },
194
		options: WebviewInputOptions
195 196 197 198 199 200 201
	): void {
		const mainThreadShowOptions: ICreateWebViewShowOptions = Object.create(null);
		if (showOptions) {
			mainThreadShowOptions.preserveFocus = !!showOptions.preserveFocus;
			mainThreadShowOptions.group = viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn);
		}

202
		const extension = reviveWebviewExtension(extensionData);
203
		const webview = this._webviewWorkbenchService.createWebview(handle, webviewPanelViewType.fromExternal(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), extension);
204
		this.hookupWebviewEventDelegate(handle, webview);
205

206
		this._webviewInputs.add(handle, webview);
207 208 209 210 211 212

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

216
	public $disposeWebview(handle: extHostProtocol.WebviewPanelHandle): void {
217
		const webview = this.getWebviewInput(handle);
218 219 220
		webview.dispose();
	}

221
	public $setTitle(handle: extHostProtocol.WebviewPanelHandle, value: string): void {
222
		const webview = this.getWebviewInput(handle);
223 224 225
		webview.setName(value);
	}

226
	public $setIconPath(handle: extHostProtocol.WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void {
227
		const webview = this.getWebviewInput(handle);
228 229 230
		webview.iconPath = reviveWebviewIcon(value);
	}

231
	public $setHtml(handle: extHostProtocol.WebviewPanelHandle, value: string): void {
232
		const webview = this.getWebviewInput(handle);
233
		webview.webview.html = value;
234 235
	}

236
	public $setOptions(handle: extHostProtocol.WebviewPanelHandle, options: modes.IWebviewOptions): void {
237
		const webview = this.getWebviewInput(handle);
M
Matt Bierner 已提交
238
		webview.webview.contentOptions = reviveWebviewOptions(options);
239 240
	}

241
	public $reveal(handle: extHostProtocol.WebviewPanelHandle, showOptions: extHostProtocol.WebviewPanelShowOptions): void {
242
		const webview = this.getWebviewInput(handle);
243 244 245 246 247 248
		if (webview.isDisposed()) {
			return;
		}

		const targetGroup = this._editorGroupService.getGroup(viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn)) || this._editorGroupService.getGroup(webview.group || 0);
		if (targetGroup) {
249
			this._webviewWorkbenchService.revealWebview(webview, targetGroup, !!showOptions.preserveFocus);
250 251 252
		}
	}

253
	public async $postMessage(handle: extHostProtocol.WebviewPanelHandle, message: any): Promise<boolean> {
254
		const webview = this.getWebviewInput(handle);
255
		webview.webview.sendMessage(message);
256
		return true;
A
Alex Dima 已提交
257
	}
258 259 260 261 262 263

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

264
		this._revivers.set(viewType, this._webviewWorkbenchService.registerResolver({
265 266
			canResolve: (webviewInput) => {
				return webviewInput.viewType === webviewPanelViewType.fromExternal(viewType);
267
			},
268 269
			resolveWebview: async (webviewInput): Promise<void> => {
				const viewType = webviewPanelViewType.toExternal(webviewInput.viewType);
M
Matt Bierner 已提交
270
				if (!viewType) {
271
					webviewInput.webview.html = MainThreadWebviews.getWebviewResolvedFailedContent(webviewInput.viewType);
M
Matt Bierner 已提交
272 273 274
					return;
				}

275 276 277
				const handle = webviewInput.id;
				this._webviewInputs.add(handle, webviewInput);
				this.hookupWebviewEventDelegate(handle, webviewInput);
278

279
				let state = undefined;
280
				if (webviewInput.webview.state) {
281
					try {
282
						state = JSON.parse(webviewInput.webview.state);
283 284 285 286 287 288
					} catch {
						// noop
					}
				}

				try {
289
					await this._proxy.$deserializeWebviewPanel(handle, viewType, webviewInput.getTitle(), state, editorGroupToViewColumn(this._editorGroupService, webviewInput.group || 0), webviewInput.webview.options);
290 291
				} catch (error) {
					onUnexpectedError(error);
292
					webviewInput.webview.html = MainThreadWebviews.getWebviewResolvedFailedContent(viewType);
293 294 295
				}
			}
		}));
A
Alex Dima 已提交
296
	}
297 298 299 300 301 302 303 304 305

	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 已提交
306
	}
307

308
	public $registerTextEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: extHostProtocol.CustomTextEditorCapabilities): void {
309
		this.registerEditorProvider(ModelType.Text, extensionData, viewType, options, capabilities, true);
310 311
	}

312 313
	public $registerCustomEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, supportsMultipleEditorsPerResource: boolean): void {
		this.registerEditorProvider(ModelType.Custom, extensionData, viewType, options, {}, supportsMultipleEditorsPerResource);
314 315
	}

316
	private registerEditorProvider(
317 318 319 320
		modelType: ModelType,
		extensionData: extHostProtocol.WebviewExtensionDescription,
		viewType: string,
		options: modes.IWebviewPanelOptions,
321
		capabilities: extHostProtocol.CustomTextEditorCapabilities,
322
		supportsMultipleEditorsPerResource: boolean,
323
	): DisposableStore {
324 325 326 327
		if (this._editorProviders.has(viewType)) {
			throw new Error(`Provider for ${viewType} already registered`);
		}

328 329 330 331
		this._customEditorService.registerCustomEditorCapabilities(viewType, {
			supportsMultipleEditorsPerResource
		});

332
		const extension = reviveWebviewExtension(extensionData);
M
Matt Bierner 已提交
333

334 335
		const disposables = new DisposableStore();
		disposables.add(this._webviewWorkbenchService.registerResolver({
336
			canResolve: (webviewInput) => {
337
				return webviewInput instanceof CustomEditorInput && webviewInput.viewType === viewType;
338
			},
339
			resolveWebview: async (webviewInput: CustomEditorInput, cancellation: CancellationToken) => {
340
				const handle = webviewInput.id;
341
				const resource = webviewInput.resource;
342

343
				this._webviewInputs.add(handle, webviewInput);
344
				this.hookupWebviewEventDelegate(handle, webviewInput);
345
				webviewInput.webview.options = options;
346
				webviewInput.webview.extension = extension;
347

348 349
				let modelRef: IReference<ICustomEditorModel>;
				try {
350
					modelRef = await this.getOrCreateCustomEditorModel(modelType, resource, viewType, { backupId: webviewInput.backupId }, cancellation);
351 352 353 354 355 356
				} catch (error) {
					onUnexpectedError(error);
					webviewInput.webview.html = MainThreadWebviews.getWebviewResolvedFailedContent(viewType);
					return;
				}

357 358 359 360
				if (cancellation.isCancellationRequested) {
					modelRef.dispose();
					return;
				}
M
Matt Bierner 已提交
361

362
				webviewInput.webview.onDispose(() => {
363 364
					modelRef.dispose();
				});
365

366 367 368
				if (capabilities.supportsMove) {
					webviewInput.onMove(async (newResource: URI) => {
						const oldModel = modelRef;
369
						modelRef = await this.getOrCreateCustomEditorModel(modelType, newResource, viewType, {}, CancellationToken.None);
370 371 372 373 374
						this._proxy.$onMoveCustomEditor(handle, newResource, viewType);
						oldModel.dispose();
					});
				}

375
				try {
376
					await this._proxy.$resolveWebviewEditor(resource, handle, viewType, webviewInput.getTitle(), editorGroupToViewColumn(this._editorGroupService, webviewInput.group || 0), webviewInput.webview.options, cancellation);
377 378
				} catch (error) {
					onUnexpectedError(error);
379 380
					webviewInput.webview.html = MainThreadWebviews.getWebviewResolvedFailedContent(viewType);
					modelRef.dispose();
M
Matt Bierner 已提交
381
					return;
382 383 384
				}
			}
		}));
385 386 387 388

		this._editorProviders.set(viewType, disposables);

		return disposables;
389 390 391 392 393 394 395 396 397 398
	}

	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);
399 400

		this._customEditorService.models.disposeAllModelsForView(viewType);
401 402
	}

403 404 405 406
	private async getOrCreateCustomEditorModel(
		modelType: ModelType,
		resource: URI,
		viewType: string,
407
		options: { backupId?: string },
408
		cancellation: CancellationToken,
409
	): Promise<IReference<ICustomEditorModel>> {
410
		const existingModel = this._customEditorService.models.tryRetain(resource, viewType);
411 412
		if (existingModel) {
			return existingModel;
413
		}
414

415 416 417 418 419 420 421 422 423 424 425 426 427 428 429
		switch (modelType) {
			case ModelType.Text:
				{
					const model = CustomTextEditorModel.create(this._instantiationService, viewType, resource);
					return this._customEditorService.models.add(resource, viewType, model);
				}
			case ModelType.Custom:
				{
					const model = MainThreadCustomEditorModel.create(this._instantiationService, this._proxy, viewType, resource, options, () => {
						return Array.from(this._webviewInputs)
							.filter(editor => editor instanceof CustomEditorInput && isEqual(editor.resource, resource)) as CustomEditorInput[];
					}, cancellation, this._backupService);
					return this._customEditorService.models.add(resource, viewType, model);
				}
		}
430 431
	}

432
	public async $onDidEdit(resourceComponents: UriComponents, viewType: string, editId: number, label: string | undefined): Promise<void> {
433
		const model = await this.getCustomEditorModel(resourceComponents, viewType);
434
		model.pushEdit(editId, label);
435 436
	}

437 438 439 440 441
	public async $onContentChange(resourceComponents: UriComponents, viewType: string): Promise<void> {
		const model = await this.getCustomEditorModel(resourceComponents, viewType);
		model.changeContent();
	}

442
	private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewPanelHandle, input: WebviewInput) {
443 444
		const disposables = new DisposableStore();

445
		disposables.add(input.webview.onDidClickLink((uri) => this.onDidClickLink(handle, uri)));
446 447 448
		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)));

449
		disposables.add(input.webview.onDispose(() => {
450
			disposables.dispose();
451

452 453 454
			this._proxy.$onDidDisposeWebviewPanel(handle).finally(() => {
				this._webviewInputs.delete(handle);
			});
455
		}));
A
Alex Dima 已提交
456
	}
457

J
Jean Pierre 已提交
458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
	private registerWebviewFromDiffEditorListeners(diffEditorInput: DiffEditorInput): void {
		const master = diffEditorInput.master as WebviewInput;
		const details = diffEditorInput.details as WebviewInput;

		if (this._webviewFromDiffEditorHandles.has(master.id) || this._webviewFromDiffEditorHandles.has(details.id)) {
			return;
		}

		this._webviewFromDiffEditorHandles.add(master.id);
		this._webviewFromDiffEditorHandles.add(details.id);

		const disposables = new DisposableStore();
		disposables.add(master.webview.onDidFocus(() => this.updateWebviewViewStates(master)));
		disposables.add(details.webview.onDidFocus(() => this.updateWebviewViewStates(details)));
		disposables.add(diffEditorInput.onDispose(() => {
			this._webviewFromDiffEditorHandles.delete(master.id);
			this._webviewFromDiffEditorHandles.delete(details.id);
			dispose(disposables);
		}));
	}

	private updateWebviewViewStates(activeEditorInput: IEditorInput | undefined) {
480
		if (!this._webviewInputs.size) {
481 482
			return;
		}
483

484
		const viewStates: extHostProtocol.WebviewPanelViewStateData = {};
485

486 487 488 489 490 491 492
		const updateViewStatesForInput = (group: IEditorGroup, topLevelInput: IEditorInput, editorInput: IEditorInput) => {
			if (!(editorInput instanceof WebviewInput)) {
				return;
			}

			editorInput.updateGroup(group.id);

493
			const handle = this._webviewInputs.getHandleForInput(editorInput);
494 495
			if (handle) {
				viewStates[handle] = {
496
					visible: topLevelInput === group.activeEditor,
J
Jean Pierre 已提交
497
					active: editorInput === activeEditorInput,
498 499 500 501
					position: editorGroupToViewColumn(this._editorGroupService, group.id),
				};
			}
		};
502

503 504 505 506 507 508 509
		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);
510 511
				}
			}
512
		}
513 514 515 516

		if (Object.keys(viewStates).length) {
			this._proxy.$onDidChangeWebviewPanelViewStates(viewStates);
		}
517 518
	}

519
	private onDidClickLink(handle: extHostProtocol.WebviewPanelHandle, link: string): void {
520
		const webview = this.getWebviewInput(handle);
521
		if (this.isSupportedLink(webview, URI.parse(link))) {
522
			this._openerService.open(link, { fromUserGesture: true });
523 524 525
		}
	}

526
	private isSupportedLink(webview: WebviewInput, link: URI): boolean {
527 528 529
		if (MainThreadWebviews.standardSupportedLinkSchemes.has(link.scheme)) {
			return true;
		}
530
		if (!isWeb && this._productService.urlProtocol === link.scheme) {
531 532
			return true;
		}
M
Matt Bierner 已提交
533
		return !!webview.webview.contentOptions.enableCommandUris && link.scheme === Schemas.command;
A
Alex Dima 已提交
534
	}
535

536
	private getWebviewInput(handle: extHostProtocol.WebviewPanelHandle): WebviewInput {
537
		const webview = this.tryGetWebviewInput(handle);
538
		if (!webview) {
M
Matt Bierner 已提交
539
			throw new Error(`Unknown webview handle:${handle}`);
540 541 542 543
		}
		return webview;
	}

544
	private tryGetWebviewInput(handle: extHostProtocol.WebviewPanelHandle): WebviewInput | undefined {
545
		return this._webviewInputs.getInputForHandle(handle);
546 547
	}

548 549 550 551 552 553 554 555 556
	private async getCustomEditorModel(resourceComponents: UriComponents, viewType: string) {
		const resource = URI.revive(resourceComponents);
		const model = await this._customEditorService.models.get(resource, viewType);
		if (!model || !(model instanceof MainThreadCustomEditorModel)) {
			throw new Error('Could not find model for webview editor');
		}
		return model;
	}

557
	private static getWebviewResolvedFailedContent(viewType: string) {
558 559 560 561
		return `<!DOCTYPE html>
		<html>
			<head>
				<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
562
				<meta http-equiv="Content-Security-Policy" content="default-src 'none';">
563
			</head>
564
			<body>${localize('errorMessage', "An error occurred while loading view: {0}", escape(viewType))}</body>
565
		</html>`;
A
Alex Dima 已提交
566
	}
567 568
}

569 570
function reviveWebviewExtension(extensionData: extHostProtocol.WebviewExtensionDescription): WebviewExtensionDescription {
	return { id: extensionData.id, location: URI.revive(extensionData.location) };
M
Matt Bierner 已提交
571 572
}

573
function reviveWebviewOptions(options: modes.IWebviewOptions): WebviewInputOptions {
574 575
	return {
		...options,
576
		allowScripts: options.enableScripts,
577 578 579 580 581
		localResourceRoots: Array.isArray(options.localResourceRoots) ? options.localResourceRoots.map(r => URI.revive(r)) : undefined,
	};
}

function reviveWebviewIcon(
M
Matt Bierner 已提交
582
	value: { light: UriComponents, dark: UriComponents; } | undefined
M
Matt Bierner 已提交
583 584 585 586
): WebviewIcons | undefined {
	return value
		? { light: URI.revive(value.light), dark: URI.revive(value.dark) }
		: undefined;
A
Alex Dima 已提交
587
}
588 589 590 591 592 593 594 595 596 597 598 599 600 601 602

namespace HotExitState {
	export const enum Type {
		Allowed,
		NotAllowed,
		Pending,
	}

	export const Allowed = Object.freeze({ type: Type.Allowed } as const);
	export const NotAllowed = Object.freeze({ type: Type.NotAllowed } as const);

	export class Pending {
		readonly type = Type.Pending;

		constructor(
M
Matt Bierner 已提交
603
			public readonly operation: CancelablePromise<string>,
604 605 606 607 608 609
		) { }
	}

	export type State = typeof Allowed | typeof NotAllowed | Pending;
}

610

611 612 613
class MainThreadCustomEditorModel extends Disposable implements ICustomEditorModel, IWorkingCopy {

	private _hotExitState: HotExitState.State = HotExitState.Allowed;
614
	private readonly _fromBackup: boolean = false;
615

616 617 618
	private _currentEditIndex: number = -1;
	private _savePoint: number = -1;
	private readonly _edits: Array<number> = [];
619
	private _isDirtyFromContentChange = false;
620 621

	private _ongoingSave?: CancelablePromise<void>;
622

M
Matt Bierner 已提交
623 624 625 626
	public static async create(
		instantiationService: IInstantiationService,
		proxy: extHostProtocol.ExtHostWebviewsShape,
		viewType: string,
627
		resource: URI,
628
		options: { backupId?: string },
629
		getEditors: () => CustomEditorInput[],
630
		cancellation: CancellationToken,
631
		_backupFileService: IBackupFileService,
M
Matt Bierner 已提交
632
	) {
633 634
		const { editable } = await proxy.$createCustomDocument(resource, viewType, options.backupId, cancellation);
		return instantiationService.createInstance(MainThreadCustomEditorModel, proxy, viewType, resource, !!options.backupId, editable, getEditors);
635 636 637 638
	}

	constructor(
		private readonly _proxy: extHostProtocol.ExtHostWebviewsShape,
M
Matt Bierner 已提交
639
		private readonly _viewType: string,
640
		private readonly _editorResource: URI,
641
		fromBackup: boolean,
642
		private readonly _editable: boolean,
643
		private readonly _getEditors: () => CustomEditorInput[],
644 645 646
		@IWorkingCopyService workingCopyService: IWorkingCopyService,
		@ILabelService private readonly _labelService: ILabelService,
		@IFileService private readonly _fileService: IFileService,
647
		@IUndoRedoService private readonly _undoService: IUndoRedoService,
648 649
	) {
		super();
650

651 652 653
		if (_editable) {
			this._register(workingCopyService.registerWorkingCopy(this));
		}
654
		this._fromBackup = fromBackup;
655 656
	}

657 658 659 660
	get editorResource() {
		return this._editorResource;
	}

661
	dispose() {
662
		if (this._editable) {
663
			this._undoService.removeElements(this._editorResource);
664
		}
M
Matt Bierner 已提交
665
		this._proxy.$disposeCustomDocument(this._editorResource, this._viewType);
666 667 668 669 670
		super.dispose();
	}

	//#region IWorkingCopy

671 672
	public get resource() {
		// Make sure each custom editor has a unique resource for backup and edits
M
Matt Bierner 已提交
673 674 675 676
		return MainThreadCustomEditorModel.toWorkingCopyResource(this._viewType, this._editorResource);
	}

	private static toWorkingCopyResource(viewType: string, resource: URI) {
677
		return URI.from({
678
			scheme: Schemas.vscodeCustomEditor,
M
Matt Bierner 已提交
679 680 681
			authority: viewType,
			path: resource.path,
			query: JSON.stringify(resource.toJSON()),
682 683
		});
	}
684 685

	public get name() {
686
		return basename(this._labelService.getUriLabel(this._editorResource));
687 688 689 690 691 692 693
	}

	public get capabilities(): WorkingCopyCapabilities {
		return 0;
	}

	public isDirty(): boolean {
694 695 696
		if (this._isDirtyFromContentChange) {
			return true;
		}
697 698 699 700
		if (this._edits.length > 0) {
			return this._savePoint !== this._currentEditIndex;
		}
		return this._fromBackup;
701 702 703 704 705 706 707 708 709
	}

	private readonly _onDidChangeDirty: Emitter<void> = this._register(new Emitter<void>());
	readonly onDidChangeDirty: Event<void> = this._onDidChangeDirty.event;

	private readonly _onDidChangeContent: Emitter<void> = this._register(new Emitter<void>());
	readonly onDidChangeContent: Event<void> = this._onDidChangeContent.event;

	//#endregion
M
Matt Bierner 已提交
710

711 712 713 714
	public isReadonly() {
		return this._editable;
	}

M
Matt Bierner 已提交
715 716 717 718
	public get viewType() {
		return this._viewType;
	}

719
	public pushEdit(editId: number, label: string | undefined) {
720 721 722 723 724 725 726 727 728 729 730
		if (!this._editable) {
			throw new Error('Document is not editable');
		}

		this.change(() => {
			this.spliceEdits(editId);
			this._currentEditIndex = this._edits.length - 1;
		});

		this._undoService.pushElement({
			type: UndoRedoElementType.Resource,
731
			resource: this._editorResource,
732
			label: label ?? localize('defaultEditLabel', "Edit"),
M
Matt Bierner 已提交
733 734 735 736
			undo: () => this.undo(),
			redo: () => this.redo(),
		});
	}
737

738 739 740 741 742 743
	public changeContent() {
		this.change(() => {
			this._isDirtyFromContentChange = true;
		});
	}

M
Matt Bierner 已提交
744 745 746 747
	private async undo(): Promise<void> {
		if (!this._editable) {
			return;
		}
748

M
Matt Bierner 已提交
749 750 751 752
		if (this._currentEditIndex < 0) {
			// nothing to undo
			return;
		}
753

M
Matt Bierner 已提交
754 755 756 757
		const undoneEdit = this._edits[this._currentEditIndex];
		this.change(() => {
			--this._currentEditIndex;
		});
M
Matt Bierner 已提交
758
		await this._proxy.$undo(this._editorResource, this.viewType, undoneEdit, this.isDirty());
M
Matt Bierner 已提交
759 760
	}

M
Matt Bierner 已提交
761 762 763 764 765 766 767 768 769 770 771 772 773
	private async redo(): Promise<void> {
		if (!this._editable) {
			return;
		}

		if (this._currentEditIndex >= this._edits.length - 1) {
			// nothing to redo
			return;
		}

		const redoneEdit = this._edits[this._currentEditIndex + 1];
		this.change(() => {
			++this._currentEditIndex;
774
		});
M
Matt Bierner 已提交
775
		await this._proxy.$redo(this._editorResource, this.viewType, redoneEdit, this.isDirty());
776 777 778 779 780 781 782 783 784 785 786
	}

	private spliceEdits(editToInsert?: number) {
		const start = this._currentEditIndex + 1;
		const toRemove = this._edits.length - this._currentEditIndex;

		const removedEdits = typeof editToInsert === 'number'
			? this._edits.splice(start, toRemove, editToInsert)
			: this._edits.splice(start, toRemove);

		if (removedEdits.length) {
787
			this._proxy.$disposeEdits(this._editorResource, this._viewType, removedEdits);
788 789 790 791 792 793
		}
	}

	private change(makeEdit: () => void): void {
		const wasDirty = this.isDirty();
		makeEdit();
794 795
		this._onDidChangeContent.fire();

796
		if (this.isDirty() !== wasDirty) {
797 798 799 800 801
			this._onDidChangeDirty.fire();
		}
	}

	public async revert(_options?: IRevertOptions) {
802 803
		if (!this._editable) {
			return;
804
		}
805

806
		if (this._currentEditIndex === this._savePoint && !this._isDirtyFromContentChange) {
807
			return;
808
		}
809

M
Matt Bierner 已提交
810
		this._proxy.$revert(this._editorResource, this.viewType, CancellationToken.None);
811
		this.change(() => {
812
			this._isDirtyFromContentChange = false;
813 814 815
			this._currentEditIndex = this._savePoint;
			this.spliceEdits();
		});
816 817
	}

818 819 820 821 822
	public async save(options?: ISaveOptions): Promise<boolean> {
		return !!await this.saveCustomEditor(options);
	}

	public async saveCustomEditor(_options?: ISaveOptions): Promise<URI | undefined> {
823
		if (!this._editable) {
824
			return undefined;
825
		}
826
		// TODO: handle save untitled case
827 828 829 830 831

		const savePromise = createCancelablePromise(token => this._proxy.$onSave(this._editorResource, this.viewType, token));
		this._ongoingSave?.cancel();
		this._ongoingSave = savePromise;

832
		this.change(() => {
833
			this._isDirtyFromContentChange = false;
834 835
			this._savePoint = this._currentEditIndex;
		});
836 837 838 839 840 841 842 843

		try {
			await savePromise;
		} finally {
			if (this._ongoingSave === savePromise) {
				this._ongoingSave = undefined;
			}
		}
844
		return this._editorResource;
845 846
	}

847
	public async saveCustomEditorAs(resource: URI, targetResource: URI, _options?: ISaveOptions): Promise<boolean> {
M
Matt Bierner 已提交
848
		if (this._editable) {
M
Matt Bierner 已提交
849 850
			// TODO: handle cancellation
			await createCancelablePromise(token => this._proxy.$onSaveAs(this._editorResource, this.viewType, targetResource, token));
851 852 853
			this.change(() => {
				this._savePoint = this._currentEditIndex;
			});
M
Matt Bierner 已提交
854 855
			return true;
		} else {
856 857 858 859 860 861 862
			// Since the editor is readonly, just copy the file over
			await this._fileService.copy(resource, targetResource, false /* overwrite */);
			return true;
		}
	}

	public async backup(): Promise<IWorkingCopyBackup> {
863 864 865 866 867 868 869
		const editors = this._getEditors();
		if (!editors.length) {
			throw new Error('No editors found for resource, cannot back up');
		}
		const primaryEditor = editors[0];

		const backupData: IWorkingCopyBackup<CustomDocumentBackupData> = {
870 871
			meta: {
				viewType: this.viewType,
872
				editorResource: this._editorResource,
M
Matt Bierner 已提交
873
				backupId: '',
874 875 876 877 878 879 880 881 882
				extension: primaryEditor.extension ? {
					id: primaryEditor.extension.id.value,
					location: primaryEditor.extension.location,
				} : undefined,
				webview: {
					id: primaryEditor.id,
					options: primaryEditor.webview.options,
					state: primaryEditor.webview.state,
				}
883 884 885 886 887 888 889
			}
		};

		if (!this._editable) {
			return backupData;
		}

890 891 892 893 894 895
		if (this._hotExitState.type === HotExitState.Type.Pending) {
			this._hotExitState.operation.cancel();
		}

		const pendingState = new HotExitState.Pending(
			createCancelablePromise(token =>
896
				this._proxy.$backup(this._editorResource.toJSON(), this.viewType, token)));
897 898 899
		this._hotExitState = pendingState;

		try {
M
Matt Bierner 已提交
900
			const backupId = await pendingState.operation;
901 902 903
			// Make sure state has not changed in the meantime
			if (this._hotExitState === pendingState) {
				this._hotExitState = HotExitState.Allowed;
M
Matt Bierner 已提交
904
				backupData.meta!.backupId = backupId;
905 906 907 908 909 910 911 912 913
			}
		} catch (e) {
			// Make sure state has not changed in the meantime
			if (this._hotExitState === pendingState) {
				this._hotExitState = HotExitState.NotAllowed;
			}
		}

		if (this._hotExitState === HotExitState.Allowed) {
914
			return backupData;
915 916 917 918 919
		}

		throw new Error('Cannot back up in this state');
	}
}