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

40 41 42
/**
 * Bi-directional map between webview handles and inputs.
 */
43
class WebviewInputStore {
44 45
	private readonly _handlesToInputs = new Map<string, WebviewInput>();
	private readonly _inputsToHandles = new Map<WebviewInput, string>();
46

47
	public add(handle: string, input: WebviewInput): void {
48 49 50 51
		this._handlesToInputs.set(handle, input);
		this._inputsToHandles.set(input, handle);
	}

52
	public getHandleForInput(input: WebviewInput): string | undefined {
53 54 55
		return this._inputsToHandles.get(input);
	}

56
	public getInputForHandle(handle: string): WebviewInput | undefined {
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
		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 已提交
73 74 75 76
class WebviewViewTypeTransformer {
	public constructor(
		public readonly prefix: string,
	) { }
M
Matt Bierner 已提交
77

M
Matt Bierner 已提交
78 79
	public fromExternal(viewType: string): string {
		return this.prefix + viewType;
M
Matt Bierner 已提交
80 81
	}

M
Matt Bierner 已提交
82
	public toExternal(viewType: string): string | undefined {
M
Matt Bierner 已提交
83
		return viewType.startsWith(this.prefix)
M
Matt Bierner 已提交
84
			? viewType.substr(this.prefix.length)
M
Matt Bierner 已提交
85 86 87 88
			: undefined;
	}
}

89 90 91 92 93
const enum ModelType {
	Custom,
	Text,
}

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

96 97
@extHostNamedCustomer(extHostProtocol.MainContext.MainThreadWebviews)
export class MainThreadWebviews extends Disposable implements extHostProtocol.MainThreadWebviewsShape {
98 99

	private static readonly standardSupportedLinkSchemes = new Set([
M
Matt Bierner 已提交
100 101 102 103
		Schemas.http,
		Schemas.https,
		Schemas.mailto,
		Schemas.vscode,
104 105 106
		'vscode-insider',
	]);

107
	private readonly _proxy: extHostProtocol.ExtHostWebviewsShape;
108
	private readonly _webviewInputs = new WebviewInputStore();
109
	private readonly _revivers = new Map<string, IDisposable>();
110
	private readonly _editorProviders = new Map<string, IDisposable>();
J
Jean Pierre 已提交
111
	private readonly _webviewFromDiffEditorHandles = new Set<string>();
112 113

	constructor(
114
		context: extHostProtocol.IExtHostContext,
115
		@IExtensionService extensionService: IExtensionService,
116
		@ICustomEditorService private readonly _customEditorService: ICustomEditorService,
117 118 119 120
		@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
		@IEditorService private readonly _editorService: IEditorService,
		@IOpenerService private readonly _openerService: IOpenerService,
		@IProductService private readonly _productService: IProductService,
121 122
		@ITelemetryService private readonly _telemetryService: ITelemetryService,
		@IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService,
123
		@IInstantiationService private readonly _instantiationService: IInstantiationService,
124 125 126
	) {
		super();

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

J
Jean Pierre 已提交
129
		this._register(_editorService.onDidActiveEditorChange(() => {
M
Matt Bierner 已提交
130
			const activeInput = this._editorService.activeEditor;
J
Jean Pierre 已提交
131 132 133 134 135 136
			if (activeInput instanceof DiffEditorInput && activeInput.master instanceof WebviewInput && activeInput.details instanceof WebviewInput) {
				this.registerWebviewFromDiffEditorListeners(activeInput);
			}

			this.updateWebviewViewStates(activeInput);
		}));
M
Matt Bierner 已提交
137 138 139 140

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

142
		// This reviver's only job is to activate webview panel extensions
143
		// This should trigger the real reviver to be registered from the extension host side.
144
		this._register(_webviewWorkbenchService.registerResolver({
145
			canResolve: (webview: WebviewInput) => {
146
				if (webview instanceof CustomEditorInput) {
147
					extensionService.activateByEvent(`onCustomEditor:${webview.viewType}`);
M
Matt Bierner 已提交
148 149 150
					return false;
				}

M
Matt Bierner 已提交
151
				const viewType = webviewPanelViewType.toExternal(webview.viewType);
M
Matt Bierner 已提交
152
				if (typeof viewType === 'string') {
153 154 155 156
					extensionService.activateByEvent(`onWebviewPanel:${viewType}`);
				}
				return false;
			},
157
			resolveWebview: () => { throw new Error('not implemented'); }
158 159 160 161
		}));
	}

	public $createWebviewPanel(
162 163
		extensionData: extHostProtocol.WebviewExtensionDescription,
		handle: extHostProtocol.WebviewPanelHandle,
164 165
		viewType: string,
		title: string,
M
Matt Bierner 已提交
166
		showOptions: { viewColumn?: EditorViewColumn, preserveFocus?: boolean; },
167
		options: WebviewInputOptions
168 169 170 171 172 173 174
	): void {
		const mainThreadShowOptions: ICreateWebViewShowOptions = Object.create(null);
		if (showOptions) {
			mainThreadShowOptions.preserveFocus = !!showOptions.preserveFocus;
			mainThreadShowOptions.group = viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn);
		}

175
		const extension = reviveWebviewExtension(extensionData);
176
		const webview = this._webviewWorkbenchService.createWebview(handle, webviewPanelViewType.fromExternal(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), extension);
177
		this.hookupWebviewEventDelegate(handle, webview);
178

179
		this._webviewInputs.add(handle, webview);
180 181 182 183 184 185

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

189
	public $disposeWebview(handle: extHostProtocol.WebviewPanelHandle): void {
190
		const webview = this.getWebviewInput(handle);
191 192 193
		webview.dispose();
	}

194
	public $setTitle(handle: extHostProtocol.WebviewPanelHandle, value: string): void {
195
		const webview = this.getWebviewInput(handle);
196 197 198
		webview.setName(value);
	}

199
	public $setIconPath(handle: extHostProtocol.WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void {
200
		const webview = this.getWebviewInput(handle);
201 202 203
		webview.iconPath = reviveWebviewIcon(value);
	}

204
	public $setHtml(handle: extHostProtocol.WebviewPanelHandle, value: string): void {
205
		const webview = this.getWebviewInput(handle);
206
		webview.webview.html = value;
207 208
	}

209
	public $setOptions(handle: extHostProtocol.WebviewPanelHandle, options: modes.IWebviewOptions): void {
210
		const webview = this.getWebviewInput(handle);
M
Matt Bierner 已提交
211
		webview.webview.contentOptions = reviveWebviewOptions(options);
212 213
	}

214
	public $reveal(handle: extHostProtocol.WebviewPanelHandle, showOptions: extHostProtocol.WebviewPanelShowOptions): void {
215
		const webview = this.getWebviewInput(handle);
216 217 218 219 220 221
		if (webview.isDisposed()) {
			return;
		}

		const targetGroup = this._editorGroupService.getGroup(viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn)) || this._editorGroupService.getGroup(webview.group || 0);
		if (targetGroup) {
222
			this._webviewWorkbenchService.revealWebview(webview, targetGroup, !!showOptions.preserveFocus);
223 224 225
		}
	}

226
	public async $postMessage(handle: extHostProtocol.WebviewPanelHandle, message: any): Promise<boolean> {
227
		const webview = this.getWebviewInput(handle);
228
		webview.webview.sendMessage(message);
229
		return true;
A
Alex Dima 已提交
230
	}
231 232 233 234 235 236

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

237
		this._revivers.set(viewType, this._webviewWorkbenchService.registerResolver({
238 239
			canResolve: (webviewInput) => {
				return webviewInput.viewType === webviewPanelViewType.fromExternal(viewType);
240
			},
241 242
			resolveWebview: async (webviewInput): Promise<void> => {
				const viewType = webviewPanelViewType.toExternal(webviewInput.viewType);
M
Matt Bierner 已提交
243
				if (!viewType) {
244
					webviewInput.webview.html = MainThreadWebviews.getDeserializationFailedContents(webviewInput.viewType);
M
Matt Bierner 已提交
245 246 247
					return;
				}

248 249 250
				const handle = webviewInput.id;
				this._webviewInputs.add(handle, webviewInput);
				this.hookupWebviewEventDelegate(handle, webviewInput);
251

252
				let state = undefined;
253
				if (webviewInput.webview.state) {
254
					try {
255
						state = JSON.parse(webviewInput.webview.state);
256 257 258 259 260 261
					} catch {
						// noop
					}
				}

				try {
262
					await this._proxy.$deserializeWebviewPanel(handle, viewType, webviewInput.getTitle(), state, editorGroupToViewColumn(this._editorGroupService, webviewInput.group || 0), webviewInput.webview.options);
263 264
				} catch (error) {
					onUnexpectedError(error);
265
					webviewInput.webview.html = MainThreadWebviews.getDeserializationFailedContents(viewType);
266 267 268
				}
			}
		}));
A
Alex Dima 已提交
269
	}
270 271 272 273 274 275 276 277 278

	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 已提交
279
	}
280

281 282 283 284 285 286 287 288
	public $registerTextEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions): void {
		return this.registerEditorProvider(ModelType.Text, extensionData, viewType, options);
	}

	public $registerCustomEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions): void {
		return this.registerEditorProvider(ModelType.Custom, extensionData, viewType, options);
	}

289
	private registerEditorProvider(
290 291 292 293 294
		modelType: ModelType,
		extensionData: extHostProtocol.WebviewExtensionDescription,
		viewType: string,
		options: modes.IWebviewPanelOptions,
	): void {
295 296 297 298
		if (this._editorProviders.has(viewType)) {
			throw new Error(`Provider for ${viewType} already registered`);
		}

299
		const extension = reviveWebviewExtension(extensionData);
M
Matt Bierner 已提交
300

301
		this._editorProviders.set(viewType, this._webviewWorkbenchService.registerResolver({
302
			canResolve: (webviewInput) => {
303
				return webviewInput instanceof CustomEditorInput && webviewInput.viewType === viewType;
304
			},
305
			resolveWebview: async (webviewInput: CustomEditorInput) => {
306 307 308
				const handle = webviewInput.id;
				this._webviewInputs.add(handle, webviewInput);
				this.hookupWebviewEventDelegate(handle, webviewInput);
309

310
				webviewInput.webview.options = options;
311
				webviewInput.webview.extension = extension;
312

313
				const resource = webviewInput.resource;
M
Matt Bierner 已提交
314

315
				const modelRef = await this.getOrCreateCustomEditorModel(modelType, webviewInput, resource, viewType);
316
				webviewInput.webview.onDispose(() => {
317 318
					modelRef.dispose();
				});
319

320 321
				try {
					await this._proxy.$resolveWebviewEditor(
322
						resource,
323 324
						handle,
						viewType,
325 326 327
						webviewInput.getTitle(),
						editorGroupToViewColumn(this._editorGroupService, webviewInput.group || 0),
						webviewInput.webview.options
328 329 330
					);
				} catch (error) {
					onUnexpectedError(error);
331
					webviewInput.webview.html = MainThreadWebviews.getDeserializationFailedContents(viewType);
M
Matt Bierner 已提交
332
					return;
333 334 335 336 337 338 339 340 341 342 343 344 345
				}
			}
		}));
	}

	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);
346 347

		this._customEditorService.models.disposeAllModelsForView(viewType);
348 349
	}

350 351 352 353 354 355 356 357 358
	private async getOrCreateCustomEditorModel(
		modelType: ModelType,
		webviewInput: WebviewInput,
		resource: URI,
		viewType: string,
	): Promise<IReference<ICustomEditorModel>> {
		const existingModel = this._customEditorService.models.tryRetain(webviewInput.resource, webviewInput.viewType);
		if (existingModel) {
			return existingModel;
359
		}
360

361 362 363
		const model = modelType === ModelType.Text
			? CustomTextEditorModel.create(this._instantiationService, viewType, resource)
			: MainThreadCustomEditorModel.create(this._instantiationService, this._proxy, viewType, resource);
364

365
		return this._customEditorService.models.add(resource, viewType, model);
366 367
	}

368 369 370
	public async $onDidChangeCustomDocumentState(resource: UriComponents, viewType: string, state: { dirty: boolean }) {
		const model = await this._customEditorService.models.get(URI.revive(resource), viewType);
		if (!model || !(model instanceof MainThreadCustomEditorModel)) {
371 372
			throw new Error('Could not find model for webview editor');
		}
373
		model.setDirty(state.dirty);
374 375
	}

376
	private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewPanelHandle, input: WebviewInput) {
377 378
		const disposables = new DisposableStore();

379
		disposables.add(input.webview.onDidClickLink((uri) => this.onDidClickLink(handle, uri)));
380 381 382 383 384
		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();
385
		});
386
		input.webview.onDispose(() => {
387 388 389 390
			this._proxy.$onDidDisposeWebviewPanel(handle).finally(() => {
				this._webviewInputs.delete(handle);
			});
		});
A
Alex Dima 已提交
391
	}
392

J
Jean Pierre 已提交
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414
	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) {
415
		if (!this._webviewInputs.size) {
416 417
			return;
		}
418

419
		const viewStates: extHostProtocol.WebviewPanelViewStateData = {};
420

421 422 423 424 425 426 427
		const updateViewStatesForInput = (group: IEditorGroup, topLevelInput: IEditorInput, editorInput: IEditorInput) => {
			if (!(editorInput instanceof WebviewInput)) {
				return;
			}

			editorInput.updateGroup(group.id);

428
			const handle = this._webviewInputs.getHandleForInput(editorInput);
429 430
			if (handle) {
				viewStates[handle] = {
431
					visible: topLevelInput === group.activeEditor,
J
Jean Pierre 已提交
432
					active: editorInput === activeEditorInput,
433 434 435 436
					position: editorGroupToViewColumn(this._editorGroupService, group.id),
				};
			}
		};
437

438 439 440 441 442 443 444
		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);
445 446
				}
			}
447
		}
448 449 450 451

		if (Object.keys(viewStates).length) {
			this._proxy.$onDidChangeWebviewPanelViewStates(viewStates);
		}
452 453
	}

454
	private onDidClickLink(handle: extHostProtocol.WebviewPanelHandle, link: string): void {
455
		const webview = this.getWebviewInput(handle);
456
		if (this.isSupportedLink(webview, URI.parse(link))) {
457
			this._openerService.open(link, { fromUserGesture: true });
458 459 460
		}
	}

461
	private isSupportedLink(webview: WebviewInput, link: URI): boolean {
462 463 464
		if (MainThreadWebviews.standardSupportedLinkSchemes.has(link.scheme)) {
			return true;
		}
465
		if (!isWeb && this._productService.urlProtocol === link.scheme) {
466 467
			return true;
		}
M
Matt Bierner 已提交
468
		return !!webview.webview.contentOptions.enableCommandUris && link.scheme === Schemas.command;
A
Alex Dima 已提交
469
	}
470

471
	private getWebviewInput(handle: extHostProtocol.WebviewPanelHandle): WebviewInput {
472
		const webview = this.tryGetWebviewInput(handle);
473
		if (!webview) {
M
Matt Bierner 已提交
474
			throw new Error(`Unknown webview handle:${handle}`);
475 476 477 478
		}
		return webview;
	}

479
	private tryGetWebviewInput(handle: extHostProtocol.WebviewPanelHandle): WebviewInput | undefined {
480
		return this._webviewInputs.getInputForHandle(handle);
481 482
	}

483 484 485 486 487
	private static getDeserializationFailedContents(viewType: string) {
		return `<!DOCTYPE html>
		<html>
			<head>
				<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
488
				<meta http-equiv="Content-Security-Policy" content="default-src 'none';">
489
			</head>
M
Matt Bierner 已提交
490
			<body>${localize('errorMessage', "An error occurred while restoring view:{0}", escape(viewType))}</body>
491
		</html>`;
A
Alex Dima 已提交
492
	}
493 494
}

495 496
function reviveWebviewExtension(extensionData: extHostProtocol.WebviewExtensionDescription): WebviewExtensionDescription {
	return { id: extensionData.id, location: URI.revive(extensionData.location) };
M
Matt Bierner 已提交
497 498
}

499
function reviveWebviewOptions(options: modes.IWebviewOptions): WebviewInputOptions {
500 501
	return {
		...options,
502
		allowScripts: options.enableScripts,
503 504 505 506 507
		localResourceRoots: Array.isArray(options.localResourceRoots) ? options.localResourceRoots.map(r => URI.revive(r)) : undefined,
	};
}

function reviveWebviewIcon(
M
Matt Bierner 已提交
508
	value: { light: UriComponents, dark: UriComponents; } | undefined
M
Matt Bierner 已提交
509 510 511 512
): WebviewIcons | undefined {
	return value
		? { light: URI.revive(value.light), dark: URI.revive(value.dark) }
		: undefined;
A
Alex Dima 已提交
513
}
514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540

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(
			public readonly operation: CancelablePromise<void>,
		) { }
	}

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

class MainThreadCustomEditorModel extends Disposable implements ICustomEditorModel, IWorkingCopy {

	private _hotExitState: HotExitState.State = HotExitState.Allowed;
	private _dirty = false;

M
Matt Bierner 已提交
541 542 543 544 545 546
	public static async create(
		instantiationService: IInstantiationService,
		proxy: extHostProtocol.ExtHostWebviewsShape,
		viewType: string,
		resource: URI
	) {
547 548 549 550 551 552
		const { editable } = await proxy.$createWebviewCustomEditorDocument(resource, viewType);
		return instantiationService.createInstance(MainThreadCustomEditorModel, proxy, viewType, resource, editable);
	}

	constructor(
		private readonly _proxy: extHostProtocol.ExtHostWebviewsShape,
M
Matt Bierner 已提交
553
		private readonly _viewType: string,
554 555 556 557 558 559 560 561 562 563 564
		private readonly _resource: URI,
		private readonly _editable: boolean,
		@IWorkingCopyService workingCopyService: IWorkingCopyService,
		@ILabelService private readonly _labelService: ILabelService,
		@IFileService private readonly _fileService: IFileService,
	) {
		super();
		this._register(workingCopyService.registerWorkingCopy(this));
	}

	dispose() {
M
Matt Bierner 已提交
565
		this._proxy.$disposeWebviewCustomEditorDocument(this.resource, this._viewType);
566 567 568 569 570
		super.dispose();
	}

	//#region IWorkingCopy

M
Matt Bierner 已提交
571
	public get resource() { return this._resource; }
572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592

	public get name() {
		return basename(this._labelService.getUriLabel(this._resource));
	}

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

	public isDirty(): boolean {
		return this._dirty;
	}

	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 已提交
593 594 595 596
	public get viewType() {
		return this._viewType;
	}

597 598 599 600 601 602 603 604 605 606
	public setDirty(dirty: boolean): void {
		this._onDidChangeContent.fire();

		if (this._dirty !== dirty) {
			this._dirty = dirty;
			this._onDidChangeDirty.fire();
		}
	}

	public async revert(_options?: IRevertOptions) {
607 608 609
		if (this._editable) {
			this._proxy.$revert(this.resource, this.viewType);
		}
610 611 612
	}

	public undo() {
613 614 615
		if (this._editable) {
			this._proxy.$undo(this.resource, this.viewType);
		}
616 617 618
	}

	public redo() {
619 620 621
		if (this._editable) {
			this._proxy.$redo(this.resource, this.viewType);
		}
622 623 624
	}

	public async save(_options?: ISaveOptions): Promise<boolean> {
625 626 627
		if (!this._editable) {
			return false;
		}
628
		await createCancelablePromise(token => this._proxy.$onSave(this.resource, this.viewType, token));
629 630 631 632 633
		this.setDirty(false);
		return true;
	}

	public async saveAs(resource: URI, targetResource: URI, _options?: ISaveOptions): Promise<boolean> {
M
Matt Bierner 已提交
634 635 636 637 638
		if (this._editable) {
			await this._proxy.$onSaveAs(this.resource, this.viewType, targetResource);
			this.setDirty(false);
			return true;
		} else {
639 640 641 642 643 644 645
			// 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> {
646 647 648 649 650 651 652 653 654 655
		const backupData: IWorkingCopyBackup = {
			meta: {
				viewType: this.viewType,
			}
		};

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

656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678
		if (this._hotExitState.type === HotExitState.Type.Pending) {
			this._hotExitState.operation.cancel();
		}

		const pendingState = new HotExitState.Pending(
			createCancelablePromise(token =>
				this._proxy.$backup(this.resource.toJSON(), this.viewType, token)));
		this._hotExitState = pendingState;

		try {
			await pendingState.operation;
			// Make sure state has not changed in the meantime
			if (this._hotExitState === pendingState) {
				this._hotExitState = HotExitState.Allowed;
			}
		} catch (e) {
			// Make sure state has not changed in the meantime
			if (this._hotExitState === pendingState) {
				this._hotExitState = HotExitState.NotAllowed;
			}
		}

		if (this._hotExitState === HotExitState.Allowed) {
679
			return backupData;
680 681 682 683 684
		}

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