Path: blob/main/src/vs/workbench/api/browser/mainThreadCustomEditors.ts
3296 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();178179// If the model is still dirty, make sure we have time to save it180if (modelRef.object.isDirty()) {181const sub = modelRef.object.onDidChangeDirty(() => {182if (!modelRef.object.isDirty()) {183sub.dispose();184modelRef.dispose();185}186});187return;188}189190modelRef.dispose();191});192193if (capabilities.supportsMove) {194webviewInput.onMove(async (newResource: URI) => {195const oldModel = modelRef;196modelRef = await this.getOrCreateCustomEditorModel(modelType, newResource, viewType, {}, CancellationToken.None);197this._proxyCustomEditors.$onMoveCustomEditor(handle, newResource, viewType);198oldModel.dispose();199});200}201202try {203const actualResource = modelType === CustomEditorModelType.Text ? this._uriIdentityService.asCanonicalUri(resource) : resource;204await this._proxyCustomEditors.$resolveCustomEditor(actualResource, handle, viewType, {205title: webviewInput.getTitle(),206contentOptions: webviewInput.webview.contentOptions,207options: webviewInput.webview.options,208active: webviewInput === this._editorService.activeEditor,209}, editorGroupToColumn(this._editorGroupService, webviewInput.group || 0), cancellation);210} catch (error) {211onUnexpectedError(error);212webviewInput.webview.setHtml(this.mainThreadWebview.getWebviewResolvedFailedContent(viewType));213modelRef.dispose();214return;215}216}217}));218219this._editorProviders.set(viewType, disposables);220}221222public $unregisterEditorProvider(viewType: string): void {223if (!this._editorProviders.has(viewType)) {224throw new Error(`No provider for ${viewType} registered`);225}226227this._editorProviders.deleteAndDispose(viewType);228229this._customEditorService.models.disposeAllModelsForView(viewType);230}231232private async getOrCreateCustomEditorModel(233modelType: CustomEditorModelType,234resource: URI,235viewType: string,236options: { backupId?: string },237cancellation: CancellationToken,238): Promise<IReference<ICustomEditorModel>> {239const existingModel = this._customEditorService.models.tryRetain(resource, viewType);240if (existingModel) {241return existingModel;242}243244switch (modelType) {245case CustomEditorModelType.Text:246{247const model = CustomTextEditorModel.create(this._instantiationService, viewType, resource);248return this._customEditorService.models.add(resource, viewType, model);249}250case CustomEditorModelType.Custom:251{252const model = MainThreadCustomEditorModel.create(this._instantiationService, this._proxyCustomEditors, viewType, resource, options, () => {253return Array.from(this.mainThreadWebviewPanels.webviewInputs)254.filter(editor => editor instanceof CustomEditorInput && isEqual(editor.resource, resource)) as CustomEditorInput[];255}, cancellation);256return this._customEditorService.models.add(resource, viewType, model);257}258}259}260261public async $onDidEdit(resourceComponents: UriComponents, viewType: string, editId: number, label: string | undefined): Promise<void> {262const model = await this.getCustomEditorModel(resourceComponents, viewType);263model.pushEdit(editId, label);264}265266public async $onContentChange(resourceComponents: UriComponents, viewType: string): Promise<void> {267const model = await this.getCustomEditorModel(resourceComponents, viewType);268model.changeContent();269}270271private async getCustomEditorModel(resourceComponents: UriComponents, viewType: string) {272const resource = URI.revive(resourceComponents);273const model = await this._customEditorService.models.get(resource, viewType);274if (!model || !(model instanceof MainThreadCustomEditorModel)) {275throw new Error('Could not find model for webview editor');276}277return model;278}279280//#region Working Copy281private async onWillRunWorkingCopyFileOperation(e: WorkingCopyFileEvent) {282if (e.operation !== FileOperation.MOVE) {283return;284}285e.waitUntil((async () => {286const models = [];287for (const file of e.files) {288if (file.source) {289models.push(...(await this._customEditorService.models.getAllModels(file.source)));290}291}292for (const model of models) {293if (model instanceof MainThreadCustomEditorModel && model.isDirty()) {294const workingCopy = await model.backup(CancellationToken.None);295if (workingCopy.meta) {296// This cast is safe because we do an instanceof check above and a custom document backup data is always returned297this._editorRenameBackups.set(model.editorResource.toString(), workingCopy.meta as CustomDocumentBackupData);298}299}300}301})());302}303//#endregion304}305306namespace HotExitState {307export const enum Type {308Allowed,309NotAllowed,310Pending,311}312313export const Allowed = Object.freeze({ type: Type.Allowed } as const);314export const NotAllowed = Object.freeze({ type: Type.NotAllowed } as const);315316export class Pending {317readonly type = Type.Pending;318319constructor(320public readonly operation: CancelablePromise<string>,321) { }322}323324export type State = typeof Allowed | typeof NotAllowed | Pending;325}326327328class MainThreadCustomEditorModel extends ResourceWorkingCopy implements ICustomEditorModel {329330private _fromBackup: boolean = false;331private _hotExitState: HotExitState.State = HotExitState.Allowed;332private _backupId: string | undefined;333334private _currentEditIndex: number = -1;335private _savePoint: number = -1;336private readonly _edits: Array<number> = [];337private _isDirtyFromContentChange = false;338339private _ongoingSave?: CancelablePromise<void>;340341// TODO@mjbvz consider to enable a `typeId` that is specific for custom342// editors. Using a distinct `typeId` allows the working copy to have343// any resource (including file based resources) even if other working344// copies exist with the same resource.345//346// IMPORTANT: changing the `typeId` has an impact on backups for this347// working copy. Any value that is not the empty string will be used348// as seed to the backup. Only change the `typeId` if you have implemented349// a fallback solution to resolve any existing backups that do not have350// this seed.351readonly typeId = NO_TYPE_ID;352353public static async create(354instantiationService: IInstantiationService,355proxy: extHostProtocol.ExtHostCustomEditorsShape,356viewType: string,357resource: URI,358options: { backupId?: string },359getEditors: () => CustomEditorInput[],360cancellation: CancellationToken,361): Promise<MainThreadCustomEditorModel> {362const editors = getEditors();363let untitledDocumentData: VSBuffer | undefined;364if (editors.length !== 0) {365untitledDocumentData = editors[0].untitledDocumentData;366}367const { editable } = await proxy.$createCustomDocument(resource, viewType, options.backupId, untitledDocumentData, cancellation);368return instantiationService.createInstance(MainThreadCustomEditorModel, proxy, viewType, resource, !!options.backupId, editable, !!untitledDocumentData, getEditors);369}370371constructor(372private readonly _proxy: extHostProtocol.ExtHostCustomEditorsShape,373private readonly _viewType: string,374private readonly _editorResource: URI,375fromBackup: boolean,376private readonly _editable: boolean,377startDirty: boolean,378private readonly _getEditors: () => CustomEditorInput[],379@IFileDialogService private readonly _fileDialogService: IFileDialogService,380@IFileService fileService: IFileService,381@ILabelService private readonly _labelService: ILabelService,382@IUndoRedoService private readonly _undoService: IUndoRedoService,383@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,384@IWorkingCopyService workingCopyService: IWorkingCopyService,385@IPathService private readonly _pathService: IPathService,386@IExtensionService extensionService: IExtensionService,387) {388super(MainThreadCustomEditorModel.toWorkingCopyResource(_viewType, _editorResource), fileService);389390this._fromBackup = fromBackup;391392if (_editable) {393this._register(workingCopyService.registerWorkingCopy(this));394395this._register(extensionService.onWillStop(e => {396e.veto(true, localize('vetoExtHostRestart', "An extension provided editor for '{0}' is still open that would close otherwise.", this.name));397}));398}399400// Normally means we're re-opening an untitled file401if (startDirty) {402this._isDirtyFromContentChange = true;403}404}405406get editorResource() {407return this._editorResource;408}409410override dispose() {411if (this._editable) {412this._undoService.removeElements(this._editorResource);413}414415this._proxy.$disposeCustomDocument(this._editorResource, this._viewType);416417super.dispose();418}419420//#region IWorkingCopy421422// Make sure each custom editor has a unique resource for backup and edits423private static toWorkingCopyResource(viewType: string, resource: URI) {424const authority = viewType.replace(/[^a-z0-9\-_]/gi, '-');425const path = `/${multibyteAwareBtoa(resource.with({ query: null, fragment: null }).toString(true))}`;426return URI.from({427scheme: Schemas.vscodeCustomEditor,428authority: authority,429path: path,430query: JSON.stringify(resource.toJSON()),431});432}433434public get name() {435return basename(this._labelService.getUriLabel(this._editorResource));436}437438public get capabilities(): WorkingCopyCapabilities {439return this.isUntitled() ? WorkingCopyCapabilities.Untitled : WorkingCopyCapabilities.None;440}441442public isDirty(): boolean {443if (this._isDirtyFromContentChange) {444return true;445}446if (this._edits.length > 0) {447return this._savePoint !== this._currentEditIndex;448}449return this._fromBackup;450}451452private isUntitled() {453return this._editorResource.scheme === Schemas.untitled;454}455456private readonly _onDidChangeDirty: Emitter<void> = this._register(new Emitter<void>());457readonly onDidChangeDirty: Event<void> = this._onDidChangeDirty.event;458459private readonly _onDidChangeContent: Emitter<void> = this._register(new Emitter<void>());460readonly onDidChangeContent: Event<void> = this._onDidChangeContent.event;461462private readonly _onDidSave: Emitter<IWorkingCopySaveEvent> = this._register(new Emitter<IWorkingCopySaveEvent>());463readonly onDidSave: Event<IWorkingCopySaveEvent> = this._onDidSave.event;464465readonly onDidChangeReadonly = Event.None;466467//#endregion468469public isReadonly(): boolean {470return !this._editable;471}472473public get viewType() {474return this._viewType;475}476477public get backupId() {478return this._backupId;479}480481public pushEdit(editId: number, label: string | undefined) {482if (!this._editable) {483throw new Error('Document is not editable');484}485486this.change(() => {487this.spliceEdits(editId);488this._currentEditIndex = this._edits.length - 1;489});490491this._undoService.pushElement({492type: UndoRedoElementType.Resource,493resource: this._editorResource,494label: label ?? localize('defaultEditLabel', "Edit"),495code: 'undoredo.customEditorEdit',496undo: () => this.undo(),497redo: () => this.redo(),498});499}500501public changeContent() {502this.change(() => {503this._isDirtyFromContentChange = true;504});505}506507private async undo(): Promise<void> {508if (!this._editable) {509return;510}511512if (this._currentEditIndex < 0) {513// nothing to undo514return;515}516517const undoneEdit = this._edits[this._currentEditIndex];518this.change(() => {519--this._currentEditIndex;520});521await this._proxy.$undo(this._editorResource, this.viewType, undoneEdit, this.isDirty());522}523524private async redo(): Promise<void> {525if (!this._editable) {526return;527}528529if (this._currentEditIndex >= this._edits.length - 1) {530// nothing to redo531return;532}533534const redoneEdit = this._edits[this._currentEditIndex + 1];535this.change(() => {536++this._currentEditIndex;537});538await this._proxy.$redo(this._editorResource, this.viewType, redoneEdit, this.isDirty());539}540541private spliceEdits(editToInsert?: number) {542const start = this._currentEditIndex + 1;543const toRemove = this._edits.length - this._currentEditIndex;544545const removedEdits = typeof editToInsert === 'number'546? this._edits.splice(start, toRemove, editToInsert)547: this._edits.splice(start, toRemove);548549if (removedEdits.length) {550this._proxy.$disposeEdits(this._editorResource, this._viewType, removedEdits);551}552}553554private change(makeEdit: () => void): void {555const wasDirty = this.isDirty();556makeEdit();557this._onDidChangeContent.fire();558559if (this.isDirty() !== wasDirty) {560this._onDidChangeDirty.fire();561}562}563564public async revert(options?: IRevertOptions) {565if (!this._editable) {566return;567}568569if (this._currentEditIndex === this._savePoint && !this._isDirtyFromContentChange && !this._fromBackup) {570return;571}572573if (!options?.soft) {574this._proxy.$revert(this._editorResource, this.viewType, CancellationToken.None);575}576577this.change(() => {578this._isDirtyFromContentChange = false;579this._fromBackup = false;580this._currentEditIndex = this._savePoint;581this.spliceEdits();582});583}584585public async save(options?: ISaveOptions): Promise<boolean> {586const result = !!await this.saveCustomEditor(options);587588// Emit Save Event589if (result) {590this._onDidSave.fire({ reason: options?.reason, source: options?.source });591}592593return result;594}595596public async saveCustomEditor(options?: ISaveOptions): Promise<URI | undefined> {597if (!this._editable) {598return undefined;599}600601if (this.isUntitled()) {602const targetUri = await this.suggestUntitledSavePath(options);603if (!targetUri) {604return undefined;605}606607await this.saveCustomEditorAs(this._editorResource, targetUri, options);608return targetUri;609}610611const savePromise = createCancelablePromise(token => this._proxy.$onSave(this._editorResource, this.viewType, token));612this._ongoingSave?.cancel();613this._ongoingSave = savePromise;614615try {616await savePromise;617618if (this._ongoingSave === savePromise) { // Make sure we are still doing the same save619this.change(() => {620this._isDirtyFromContentChange = false;621this._savePoint = this._currentEditIndex;622this._fromBackup = false;623});624}625} finally {626if (this._ongoingSave === savePromise) { // Make sure we are still doing the same save627this._ongoingSave = undefined;628}629}630631return this._editorResource;632}633634private suggestUntitledSavePath(options: ISaveOptions | undefined): Promise<URI | undefined> {635if (!this.isUntitled()) {636throw new Error('Resource is not untitled');637}638639const remoteAuthority = this._environmentService.remoteAuthority;640const localResource = toLocalResource(this._editorResource, remoteAuthority, this._pathService.defaultUriScheme);641642return this._fileDialogService.pickFileToSave(localResource, options?.availableFileSystems);643}644645public async saveCustomEditorAs(resource: URI, targetResource: URI, _options?: ISaveOptions): Promise<boolean> {646if (this._editable) {647// TODO: handle cancellation648await createCancelablePromise(token => this._proxy.$onSaveAs(this._editorResource, this.viewType, targetResource, token));649this.change(() => {650this._savePoint = this._currentEditIndex;651});652return true;653} else {654// Since the editor is readonly, just copy the file over655await this.fileService.copy(resource, targetResource, false /* overwrite */);656return true;657}658}659660public get canHotExit() { return typeof this._backupId === 'string' && this._hotExitState.type === HotExitState.Type.Allowed; }661662public async backup(token: CancellationToken): Promise<IWorkingCopyBackup> {663const editors = this._getEditors();664if (!editors.length) {665throw new Error('No editors found for resource, cannot back up');666}667const primaryEditor = editors[0];668669const backupMeta: CustomDocumentBackupData = {670viewType: this.viewType,671editorResource: this._editorResource,672backupId: '',673extension: primaryEditor.extension ? {674id: primaryEditor.extension.id.value,675location: primaryEditor.extension.location!,676} : undefined,677webview: {678origin: primaryEditor.webview.origin,679options: primaryEditor.webview.options,680state: primaryEditor.webview.state,681}682};683684const backupData: IWorkingCopyBackup = {685meta: backupMeta686};687688if (!this._editable) {689return backupData;690}691692if (this._hotExitState.type === HotExitState.Type.Pending) {693this._hotExitState.operation.cancel();694}695696const pendingState = new HotExitState.Pending(697createCancelablePromise(token =>698this._proxy.$backup(this._editorResource.toJSON(), this.viewType, token)));699this._hotExitState = pendingState;700701token.onCancellationRequested(() => {702pendingState.operation.cancel();703});704705let errorMessage = '';706try {707const backupId = await pendingState.operation;708// Make sure state has not changed in the meantime709if (this._hotExitState === pendingState) {710this._hotExitState = HotExitState.Allowed;711backupData.meta!.backupId = backupId;712this._backupId = backupId;713}714} catch (e) {715if (isCancellationError(e)) {716// This is expected717throw e;718}719720// Otherwise it could be a real error. Make sure state has not changed in the meantime.721if (this._hotExitState === pendingState) {722this._hotExitState = HotExitState.NotAllowed;723}724if (e.message) {725errorMessage = e.message;726}727}728729if (this._hotExitState === HotExitState.Allowed) {730return backupData;731}732733throw new Error(`Cannot backup in this state: ${errorMessage}`);734}735}736737738