Path: blob/main/src/vs/workbench/api/browser/mainThreadCustomEditors.ts
5240 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { multibyteAwareBtoa } from '../../../base/common/strings.js';6import { CancelablePromise, createCancelablePromise } from '../../../base/common/async.js';7import { VSBuffer } from '../../../base/common/buffer.js';8import { CancellationToken } from '../../../base/common/cancellation.js';9import { isCancellationError, onUnexpectedError } from '../../../base/common/errors.js';10import { Emitter, Event } from '../../../base/common/event.js';11import { Disposable, DisposableMap, DisposableStore, IReference } from '../../../base/common/lifecycle.js';12import { Schemas } from '../../../base/common/network.js';13import { basename } from '../../../base/common/path.js';14import { isEqual, isEqualOrParent, toLocalResource } from '../../../base/common/resources.js';15import { URI, UriComponents } from '../../../base/common/uri.js';16import { generateUuid } from '../../../base/common/uuid.js';17import { localize } from '../../../nls.js';18import { IFileDialogService } from '../../../platform/dialogs/common/dialogs.js';19import { FileOperation, IFileService } from '../../../platform/files/common/files.js';20import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';21import { ILabelService } from '../../../platform/label/common/label.js';22import { IStorageService } from '../../../platform/storage/common/storage.js';23import { IUndoRedoService, UndoRedoElementType } from '../../../platform/undoRedo/common/undoRedo.js';24import { MainThreadWebviewPanels } from './mainThreadWebviewPanels.js';25import { MainThreadWebviews, reviveWebviewExtension } from './mainThreadWebviews.js';26import * as extHostProtocol from '../common/extHost.protocol.js';27import { IRevertOptions, ISaveOptions } from '../../common/editor.js';28import { CustomEditorInput } from '../../contrib/customEditor/browser/customEditorInput.js';29import { CustomDocumentBackupData } from '../../contrib/customEditor/browser/customEditorInputFactory.js';30import { ICustomEditorModel, ICustomEditorService } from '../../contrib/customEditor/common/customEditor.js';31import { CustomTextEditorModel } from '../../contrib/customEditor/common/customTextEditorModel.js';32import { ExtensionKeyedWebviewOriginStore, WebviewExtensionDescription } from '../../contrib/webview/browser/webview.js';33import { WebviewInput } from '../../contrib/webviewPanel/browser/webviewEditorInput.js';34import { IWebviewWorkbenchService } from '../../contrib/webviewPanel/browser/webviewWorkbenchService.js';35import { editorGroupToColumn } from '../../services/editor/common/editorGroupColumn.js';36import { IEditorGroupsService } from '../../services/editor/common/editorGroupsService.js';37import { IEditorService } from '../../services/editor/common/editorService.js';38import { IWorkbenchEnvironmentService } from '../../services/environment/common/environmentService.js';39import { IExtensionService } from '../../services/extensions/common/extensions.js';40import { IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';41import { IPathService } from '../../services/path/common/pathService.js';42import { ResourceWorkingCopy } from '../../services/workingCopy/common/resourceWorkingCopy.js';43import { IWorkingCopy, IWorkingCopyBackup, IWorkingCopySaveEvent, NO_TYPE_ID, WorkingCopyCapabilities } from '../../services/workingCopy/common/workingCopy.js';44import { IWorkingCopyFileService, WorkingCopyFileEvent } from '../../services/workingCopy/common/workingCopyFileService.js';45import { IWorkingCopyService } from '../../services/workingCopy/common/workingCopyService.js';46import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js';4748const enum CustomEditorModelType {49Custom,50Text,51}5253export class MainThreadCustomEditors extends Disposable implements extHostProtocol.MainThreadCustomEditorsShape {5455private readonly _proxyCustomEditors: extHostProtocol.ExtHostCustomEditorsShape;5657private readonly _editorProviders = this._register(new DisposableMap<string>());5859private readonly _editorRenameBackups = new Map<string, CustomDocumentBackupData>();6061private readonly _webviewOriginStore: ExtensionKeyedWebviewOriginStore;6263constructor(64context: IExtHostContext,65private readonly mainThreadWebview: MainThreadWebviews,66private readonly mainThreadWebviewPanels: MainThreadWebviewPanels,67@IExtensionService extensionService: IExtensionService,68@IStorageService storageService: IStorageService,69@IWorkingCopyService workingCopyService: IWorkingCopyService,70@IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService,71@ICustomEditorService private readonly _customEditorService: ICustomEditorService,72@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,73@IEditorService private readonly _editorService: IEditorService,74@IInstantiationService private readonly _instantiationService: IInstantiationService,75@IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService,76@IUriIdentityService private readonly _uriIdentityService: IUriIdentityService,77) {78super();7980this._webviewOriginStore = new ExtensionKeyedWebviewOriginStore('mainThreadCustomEditors.origins', storageService);8182this._proxyCustomEditors = context.getProxy(extHostProtocol.ExtHostContext.ExtHostCustomEditors);8384this._register(workingCopyFileService.registerWorkingCopyProvider((editorResource) => {85const matchedWorkingCopies: IWorkingCopy[] = [];8687for (const workingCopy of workingCopyService.workingCopies) {88if (workingCopy instanceof MainThreadCustomEditorModel) {89if (isEqualOrParent(editorResource, workingCopy.editorResource)) {90matchedWorkingCopies.push(workingCopy);91}92}93}94return matchedWorkingCopies;95}));9697// This reviver's only job is to activate custom editor extensions.98this._register(_webviewWorkbenchService.registerResolver({99canResolve: (webview: WebviewInput) => {100if (webview instanceof CustomEditorInput) {101extensionService.activateByEvent(`onCustomEditor:${webview.viewType}`);102}103return false;104},105resolveWebview: () => { throw new Error('not implemented'); }106}));107108// Working copy operations109this._register(workingCopyFileService.onWillRunWorkingCopyFileOperation(async e => this.onWillRunWorkingCopyFileOperation(e)));110}111112public $registerTextEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: extHostProtocol.IWebviewPanelOptions, capabilities: extHostProtocol.CustomTextEditorCapabilities, serializeBuffersForPostMessage: boolean): void {113this.registerEditorProvider(CustomEditorModelType.Text, reviveWebviewExtension(extensionData), viewType, options, capabilities, true, serializeBuffersForPostMessage);114}115116public $registerCustomEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: extHostProtocol.IWebviewPanelOptions, supportsMultipleEditorsPerDocument: boolean, serializeBuffersForPostMessage: boolean): void {117this.registerEditorProvider(CustomEditorModelType.Custom, reviveWebviewExtension(extensionData), viewType, options, {}, supportsMultipleEditorsPerDocument, serializeBuffersForPostMessage);118}119120private registerEditorProvider(121modelType: CustomEditorModelType,122extension: WebviewExtensionDescription,123viewType: string,124options: extHostProtocol.IWebviewPanelOptions,125capabilities: extHostProtocol.CustomTextEditorCapabilities,126supportsMultipleEditorsPerDocument: boolean,127serializeBuffersForPostMessage: boolean,128): void {129if (this._editorProviders.has(viewType)) {130throw new Error(`Provider for ${viewType} already registered`);131}132133const disposables = new DisposableStore();134135disposables.add(this._customEditorService.registerCustomEditorCapabilities(viewType, {136supportsMultipleEditorsPerDocument137}));138139disposables.add(this._webviewWorkbenchService.registerResolver({140canResolve: (webviewInput) => {141return webviewInput instanceof CustomEditorInput && webviewInput.viewType === viewType;142},143resolveWebview: async (webviewInput: CustomEditorInput, cancellation: CancellationToken) => {144const handle = generateUuid();145const resource = webviewInput.resource;146147webviewInput.webview.origin = this._webviewOriginStore.getOrigin(viewType, extension.id);148149this.mainThreadWebviewPanels.addWebviewInput(handle, webviewInput, { serializeBuffersForPostMessage });150webviewInput.webview.options = options;151webviewInput.webview.extension = extension;152153// If there's an old resource this was a move and we must resolve the backup at the same time as the webview154// This is because the backup must be ready upon model creation, and the input resolve method comes after155let backupId = webviewInput.backupId;156if (webviewInput.oldResource && !webviewInput.backupId) {157const backup = this._editorRenameBackups.get(webviewInput.oldResource.toString());158backupId = backup?.backupId;159this._editorRenameBackups.delete(webviewInput.oldResource.toString());160}161162let modelRef: IReference<ICustomEditorModel>;163try {164modelRef = await this.getOrCreateCustomEditorModel(modelType, resource, viewType, { backupId }, cancellation);165} catch (error) {166onUnexpectedError(error);167webviewInput.webview.setHtml(this.mainThreadWebview.getWebviewResolvedFailedContent(viewType));168return;169}170171if (cancellation.isCancellationRequested) {172modelRef.dispose();173return;174}175176const disposeSub = webviewInput.webview.onDidDispose(() => {177disposeSub.dispose();178inputDisposeSub.dispose();179180// If the model is still dirty, make sure we have time to save it181if (modelRef.object.isDirty()) {182const sub = modelRef.object.onDidChangeDirty(() => {183if (!modelRef.object.isDirty()) {184sub.dispose();185modelRef.dispose();186}187});188return;189}190191modelRef.dispose();192});193194// Also listen for when the input is disposed (e.g., during SaveAs when the webview is transferred to a new editor).195// In this case, webview.onDidDispose won't fire because the webview is reused.196const inputDisposeSub = webviewInput.onWillDispose(() => {197inputDisposeSub.dispose();198disposeSub.dispose();199modelRef.dispose();200});201202if (capabilities.supportsMove) {203webviewInput.onMove(async (newResource: URI) => {204const oldModel = modelRef;205modelRef = await this.getOrCreateCustomEditorModel(modelType, newResource, viewType, {}, CancellationToken.None);206this._proxyCustomEditors.$onMoveCustomEditor(handle, newResource, viewType);207oldModel.dispose();208});209}210211try {212const actualResource = modelType === CustomEditorModelType.Text ? this._uriIdentityService.asCanonicalUri(resource) : resource;213await this._proxyCustomEditors.$resolveCustomEditor(actualResource, handle, viewType, {214title: webviewInput.getTitle(),215contentOptions: webviewInput.webview.contentOptions,216options: webviewInput.webview.options,217active: webviewInput === this._editorService.activeEditor,218}, editorGroupToColumn(this._editorGroupService, webviewInput.group || 0), cancellation);219} catch (error) {220onUnexpectedError(error);221webviewInput.webview.setHtml(this.mainThreadWebview.getWebviewResolvedFailedContent(viewType));222modelRef.dispose();223return;224}225}226}));227228this._editorProviders.set(viewType, disposables);229}230231public $unregisterEditorProvider(viewType: string): void {232if (!this._editorProviders.has(viewType)) {233throw new Error(`No provider for ${viewType} registered`);234}235236this._editorProviders.deleteAndDispose(viewType);237238this._customEditorService.models.disposeAllModelsForView(viewType);239}240241private async getOrCreateCustomEditorModel(242modelType: CustomEditorModelType,243resource: URI,244viewType: string,245options: { backupId?: string },246cancellation: CancellationToken,247): Promise<IReference<ICustomEditorModel>> {248const existingModel = this._customEditorService.models.tryRetain(resource, viewType);249if (existingModel) {250return existingModel;251}252253switch (modelType) {254case CustomEditorModelType.Text:255{256const model = CustomTextEditorModel.create(this._instantiationService, viewType, resource);257return this._customEditorService.models.add(resource, viewType, model);258}259case CustomEditorModelType.Custom:260{261const model = MainThreadCustomEditorModel.create(this._instantiationService, this._proxyCustomEditors, viewType, resource, options, () => {262return Array.from(this.mainThreadWebviewPanels.webviewInputs)263.filter(editor => editor instanceof CustomEditorInput && isEqual(editor.resource, resource)) as CustomEditorInput[];264}, cancellation);265return this._customEditorService.models.add(resource, viewType, model);266}267}268}269270public async $onDidEdit(resourceComponents: UriComponents, viewType: string, editId: number, label: string | undefined): Promise<void> {271const model = await this.getCustomEditorModel(resourceComponents, viewType);272model.pushEdit(editId, label);273}274275public async $onContentChange(resourceComponents: UriComponents, viewType: string): Promise<void> {276const model = await this.getCustomEditorModel(resourceComponents, viewType);277model.changeContent();278}279280private async getCustomEditorModel(resourceComponents: UriComponents, viewType: string) {281const resource = URI.revive(resourceComponents);282const model = await this._customEditorService.models.get(resource, viewType);283if (!model || !(model instanceof MainThreadCustomEditorModel)) {284throw new Error('Could not find model for webview editor');285}286return model;287}288289//#region Working Copy290private async onWillRunWorkingCopyFileOperation(e: WorkingCopyFileEvent) {291if (e.operation !== FileOperation.MOVE) {292return;293}294e.waitUntil((async () => {295const models = [];296for (const file of e.files) {297if (file.source) {298models.push(...(await this._customEditorService.models.getAllModels(file.source)));299}300}301for (const model of models) {302if (model instanceof MainThreadCustomEditorModel && model.isDirty()) {303const workingCopy = await model.backup(CancellationToken.None);304if (workingCopy.meta) {305// This cast is safe because we do an instanceof check above and a custom document backup data is always returned306this._editorRenameBackups.set(model.editorResource.toString(), workingCopy.meta as CustomDocumentBackupData);307}308}309}310})());311}312//#endregion313}314315namespace HotExitState {316export const enum Type {317Allowed,318NotAllowed,319Pending,320}321322export const Allowed = Object.freeze({ type: Type.Allowed } as const);323export const NotAllowed = Object.freeze({ type: Type.NotAllowed } as const);324325export class Pending {326readonly type = Type.Pending;327328constructor(329public readonly operation: CancelablePromise<string>,330) { }331}332333export type State = typeof Allowed | typeof NotAllowed | Pending;334}335336337class MainThreadCustomEditorModel extends ResourceWorkingCopy implements ICustomEditorModel {338339private _fromBackup: boolean = false;340private _hotExitState: HotExitState.State = HotExitState.Allowed;341private _backupId: string | undefined;342343private _currentEditIndex: number = -1;344private _savePoint: number = -1;345private readonly _edits: Array<number> = [];346private _isDirtyFromContentChange: boolean;347348private _ongoingSave?: CancelablePromise<void>;349350// TODO@mjbvz consider to enable a `typeId` that is specific for custom351// editors. Using a distinct `typeId` allows the working copy to have352// any resource (including file based resources) even if other working353// copies exist with the same resource.354//355// IMPORTANT: changing the `typeId` has an impact on backups for this356// working copy. Any value that is not the empty string will be used357// as seed to the backup. Only change the `typeId` if you have implemented358// a fallback solution to resolve any existing backups that do not have359// this seed.360readonly typeId = NO_TYPE_ID;361362public static async create(363instantiationService: IInstantiationService,364proxy: extHostProtocol.ExtHostCustomEditorsShape,365viewType: string,366resource: URI,367options: { backupId?: string },368getEditors: () => CustomEditorInput[],369cancellation: CancellationToken,370): Promise<MainThreadCustomEditorModel> {371const editors = getEditors();372let untitledDocumentData: VSBuffer | undefined;373if (editors.length !== 0) {374untitledDocumentData = editors[0].untitledDocumentData;375}376const { editable } = await proxy.$createCustomDocument(resource, viewType, options.backupId, untitledDocumentData, cancellation);377return instantiationService.createInstance(MainThreadCustomEditorModel, proxy, viewType, resource, !!options.backupId, editable, !!untitledDocumentData, getEditors);378}379380constructor(381private readonly _proxy: extHostProtocol.ExtHostCustomEditorsShape,382private readonly _viewType: string,383private readonly _editorResource: URI,384fromBackup: boolean,385private readonly _editable: boolean,386startDirty: boolean,387private readonly _getEditors: () => CustomEditorInput[],388@IFileDialogService private readonly _fileDialogService: IFileDialogService,389@IFileService fileService: IFileService,390@ILabelService private readonly _labelService: ILabelService,391@IUndoRedoService private readonly _undoService: IUndoRedoService,392@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,393@IWorkingCopyService workingCopyService: IWorkingCopyService,394@IPathService private readonly _pathService: IPathService,395@IExtensionService extensionService: IExtensionService,396) {397super(MainThreadCustomEditorModel.toWorkingCopyResource(_viewType, _editorResource), fileService);398399this._fromBackup = fromBackup;400401// Normally means we're re-opening an untitled file (set this before registering the working copy402// so that dirty state is correct when first queried).403this._isDirtyFromContentChange = startDirty;404405if (_editable) {406this._register(workingCopyService.registerWorkingCopy(this));407408this._register(extensionService.onWillStop(e => {409e.veto(true, localize('vetoExtHostRestart', "An extension provided editor for '{0}' is still open that would close otherwise.", this.name));410}));411}412}413414get editorResource() {415return this._editorResource;416}417418override dispose() {419if (this._editable) {420this._undoService.removeElements(this._editorResource);421}422423this._proxy.$disposeCustomDocument(this._editorResource, this._viewType);424425super.dispose();426}427428//#region IWorkingCopy429430// Make sure each custom editor has a unique resource for backup and edits431private static toWorkingCopyResource(viewType: string, resource: URI) {432const authority = viewType.replace(/[^a-z0-9\-_]/gi, '-');433const path = `/${multibyteAwareBtoa(resource.with({ query: null, fragment: null }).toString(true))}`;434return URI.from({435scheme: Schemas.vscodeCustomEditor,436authority: authority,437path: path,438query: JSON.stringify(resource.toJSON()),439});440}441442public get name() {443return basename(this._labelService.getUriLabel(this._editorResource));444}445446public get capabilities(): WorkingCopyCapabilities {447return this.isUntitled() ? WorkingCopyCapabilities.Untitled : WorkingCopyCapabilities.None;448}449450public isDirty(): boolean {451if (this._isDirtyFromContentChange) {452return true;453}454if (this._edits.length > 0) {455return this._savePoint !== this._currentEditIndex;456}457return this._fromBackup;458}459460private isUntitled() {461return this._editorResource.scheme === Schemas.untitled;462}463464private readonly _onDidChangeDirty: Emitter<void> = this._register(new Emitter<void>());465readonly onDidChangeDirty: Event<void> = this._onDidChangeDirty.event;466467private readonly _onDidChangeContent: Emitter<void> = this._register(new Emitter<void>());468readonly onDidChangeContent: Event<void> = this._onDidChangeContent.event;469470private readonly _onDidSave: Emitter<IWorkingCopySaveEvent> = this._register(new Emitter<IWorkingCopySaveEvent>());471readonly onDidSave: Event<IWorkingCopySaveEvent> = this._onDidSave.event;472473readonly onDidChangeReadonly = Event.None;474475//#endregion476477public isReadonly(): boolean {478return !this._editable;479}480481public get viewType() {482return this._viewType;483}484485public get backupId() {486return this._backupId;487}488489public pushEdit(editId: number, label: string | undefined) {490if (!this._editable) {491throw new Error('Document is not editable');492}493494this.change(() => {495this.spliceEdits(editId);496this._currentEditIndex = this._edits.length - 1;497});498499this._undoService.pushElement({500type: UndoRedoElementType.Resource,501resource: this._editorResource,502label: label ?? localize('defaultEditLabel', "Edit"),503code: 'undoredo.customEditorEdit',504undo: () => this.undo(),505redo: () => this.redo(),506});507}508509public changeContent() {510this.change(() => {511this._isDirtyFromContentChange = true;512});513}514515private async undo(): Promise<void> {516if (!this._editable) {517return;518}519520if (this._currentEditIndex < 0) {521// nothing to undo522return;523}524525const undoneEdit = this._edits[this._currentEditIndex];526this.change(() => {527--this._currentEditIndex;528});529await this._proxy.$undo(this._editorResource, this.viewType, undoneEdit, this.isDirty());530}531532private async redo(): Promise<void> {533if (!this._editable) {534return;535}536537if (this._currentEditIndex >= this._edits.length - 1) {538// nothing to redo539return;540}541542const redoneEdit = this._edits[this._currentEditIndex + 1];543this.change(() => {544++this._currentEditIndex;545});546await this._proxy.$redo(this._editorResource, this.viewType, redoneEdit, this.isDirty());547}548549private spliceEdits(editToInsert?: number) {550const start = this._currentEditIndex + 1;551const toRemove = this._edits.length - this._currentEditIndex;552553const removedEdits = typeof editToInsert === 'number'554? this._edits.splice(start, toRemove, editToInsert)555: this._edits.splice(start, toRemove);556557if (removedEdits.length) {558this._proxy.$disposeEdits(this._editorResource, this._viewType, removedEdits);559}560}561562private change(makeEdit: () => void): void {563const wasDirty = this.isDirty();564makeEdit();565this._onDidChangeContent.fire();566567if (this.isDirty() !== wasDirty) {568this._onDidChangeDirty.fire();569}570}571572public async revert(options?: IRevertOptions) {573if (!this._editable) {574return;575}576577if (this._currentEditIndex === this._savePoint && !this._isDirtyFromContentChange && !this._fromBackup) {578return;579}580581if (!options?.soft) {582this._proxy.$revert(this._editorResource, this.viewType, CancellationToken.None);583}584585this.change(() => {586this._isDirtyFromContentChange = false;587this._fromBackup = false;588this._currentEditIndex = this._savePoint;589this.spliceEdits();590});591}592593public async save(options?: ISaveOptions): Promise<boolean> {594const result = !!await this.saveCustomEditor(options);595596// Emit Save Event597if (result) {598this._onDidSave.fire({ reason: options?.reason, source: options?.source });599}600601return result;602}603604public async saveCustomEditor(options?: ISaveOptions): Promise<URI | undefined> {605if (!this._editable) {606return undefined;607}608609if (this.isUntitled()) {610const targetUri = await this.suggestUntitledSavePath(options);611if (!targetUri) {612return undefined;613}614615await this.saveCustomEditorAs(this._editorResource, targetUri, options);616return targetUri;617}618619const savePromise = createCancelablePromise(token => this._proxy.$onSave(this._editorResource, this.viewType, token));620this._ongoingSave?.cancel();621this._ongoingSave = savePromise;622623try {624await savePromise;625626if (this._ongoingSave === savePromise) { // Make sure we are still doing the same save627this.change(() => {628this._isDirtyFromContentChange = false;629this._savePoint = this._currentEditIndex;630this._fromBackup = false;631});632}633} finally {634if (this._ongoingSave === savePromise) { // Make sure we are still doing the same save635this._ongoingSave = undefined;636}637}638639return this._editorResource;640}641642private suggestUntitledSavePath(options: ISaveOptions | undefined): Promise<URI | undefined> {643if (!this.isUntitled()) {644throw new Error('Resource is not untitled');645}646647const remoteAuthority = this._environmentService.remoteAuthority;648const localResource = toLocalResource(this._editorResource, remoteAuthority, this._pathService.defaultUriScheme);649650return this._fileDialogService.pickFileToSave(localResource, options?.availableFileSystems);651}652653public async saveCustomEditorAs(resource: URI, targetResource: URI, _options?: ISaveOptions): Promise<boolean> {654if (this._editable) {655// TODO: handle cancellation656await createCancelablePromise(token => this._proxy.$onSaveAs(this._editorResource, this.viewType, targetResource, token));657this.change(() => {658this._isDirtyFromContentChange = false;659this._savePoint = this._currentEditIndex;660this._fromBackup = false;661});662return true;663} else {664// Since the editor is readonly, just copy the file over665await this.fileService.copy(resource, targetResource, false /* overwrite */);666return true;667}668}669670public get canHotExit() { return typeof this._backupId === 'string' && this._hotExitState.type === HotExitState.Type.Allowed; }671672public async backup(token: CancellationToken): Promise<IWorkingCopyBackup> {673const editors = this._getEditors();674if (!editors.length) {675throw new Error('No editors found for resource, cannot back up');676}677const primaryEditor = editors[0];678679const backupMeta: CustomDocumentBackupData = {680viewType: this.viewType,681editorResource: this._editorResource,682customTitle: primaryEditor.getWebviewTitle(),683iconPath: primaryEditor.iconPath,684backupId: '',685extension: primaryEditor.extension ? {686id: primaryEditor.extension.id.value,687location: primaryEditor.extension.location!,688} : undefined,689webview: {690origin: primaryEditor.webview.origin,691options: primaryEditor.webview.options,692state: primaryEditor.webview.state,693}694};695696const backupData: IWorkingCopyBackup = {697meta: backupMeta698};699700if (!this._editable) {701return backupData;702}703704if (this._hotExitState.type === HotExitState.Type.Pending) {705this._hotExitState.operation.cancel();706}707708const pendingState = new HotExitState.Pending(709createCancelablePromise(token =>710this._proxy.$backup(this._editorResource.toJSON(), this.viewType, token)));711this._hotExitState = pendingState;712713token.onCancellationRequested(() => {714pendingState.operation.cancel();715});716717let errorMessage = '';718try {719const backupId = await pendingState.operation;720// Make sure state has not changed in the meantime721if (this._hotExitState === pendingState) {722this._hotExitState = HotExitState.Allowed;723backupData.meta!.backupId = backupId;724this._backupId = backupId;725}726} catch (e) {727if (isCancellationError(e)) {728// This is expected729throw e;730}731732// Otherwise it could be a real error. Make sure state has not changed in the meantime.733if (this._hotExitState === pendingState) {734this._hotExitState = HotExitState.NotAllowed;735}736if (e.message) {737errorMessage = e.message;738}739}740741if (this._hotExitState === HotExitState.Allowed) {742return backupData;743}744745throw new Error(`Cannot backup in this state: ${errorMessage}`);746}747}748749750