Fix data loss when renaming custom editors (#124057)

* Start structuring the custom editor rename work

* Restore renamed / moved editors from backup

* Add back missing readonly

* Discard backup after using it

* Address feedback

* Remove console log

* Switch to using a map

* Delete used backups from the map
This commit is contained in:
Logan Ramos
2021-05-18 21:16:00 -04:00
committed by GitHub
parent bbb89b86f5
commit 721cdd6472
4 changed files with 61 additions and 8 deletions

View File

@@ -16,7 +16,7 @@ import { isEqual, isEqualOrParent, toLocalResource } from 'vs/base/common/resour
import { URI, UriComponents } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
import { FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files';
import { FileOperation, FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILabelService } from 'vs/platform/label/common/label';
import { IUndoRedoService, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo';
@@ -35,7 +35,7 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IPathService } from 'vs/workbench/services/path/common/pathService';
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
import { IWorkingCopyFileService, WorkingCopyFileEvent } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { IWorkingCopy, IWorkingCopyBackup, NO_TYPE_ID, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy';
import { ResourceWorkingCopy } from 'vs/workbench/services/workingCopy/common/resourceWorkingCopy';
@@ -51,6 +51,8 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc
private readonly _editorProviders = new Map<string, IDisposable>();
private readonly _editorRenameBackups = new Map<string, CustomDocumentBackupData>();
constructor(
context: extHostProtocol.IExtHostContext,
private readonly mainThreadWebview: MainThreadWebviews,
@@ -61,7 +63,7 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc
@ICustomEditorService private readonly _customEditorService: ICustomEditorService,
@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
@IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService,
@IInstantiationService private readonly _instantiationService: IInstantiationService
@IInstantiationService private readonly _instantiationService: IInstantiationService,
) {
super();
@@ -90,6 +92,9 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc
},
resolveWebview: () => { throw new Error('not implemented'); }
}));
// Working copy operations
this._register(workingCopyFileService.onWillRunWorkingCopyFileOperation(async e => this.onWillRunWorkingCopyFileOperation(e)));
}
override dispose() {
@@ -138,9 +143,18 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc
webviewInput.webview.options = options;
webviewInput.webview.extension = extension;
// If there's an old resource this was a move and we must resolve the backup at the same time as the webview
// This is because the backup must be ready upon model creation, and the input resolve method comes after
let backupId = webviewInput.backupId;
if (webviewInput.oldResource && !webviewInput.backupId) {
const backup = this._editorRenameBackups.get(webviewInput.oldResource.toString());
backupId = backup?.backupId;
this._editorRenameBackups.delete(webviewInput.oldResource.toString());
}
let modelRef: IReference<ICustomEditorModel>;
try {
modelRef = await this.getOrCreateCustomEditorModel(modelType, resource, viewType, { backupId: webviewInput.backupId }, cancellation);
modelRef = await this.getOrCreateCustomEditorModel(modelType, resource, viewType, { backupId }, cancellation);
} catch (error) {
onUnexpectedError(error);
webviewInput.webview.html = this.mainThreadWebview.getWebviewResolvedFailedContent(viewType);
@@ -253,6 +267,31 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc
}
return model;
}
//#region Working Copy
private async onWillRunWorkingCopyFileOperation(e: WorkingCopyFileEvent) {
if (e.operation !== FileOperation.MOVE) {
return;
}
e.waitUntil((async () => {
const models = [];
for (const file of e.files) {
if (file.source) {
models.push(...(await this._customEditorService.models.getAllModels(file.source)));
}
}
for (const model of models) {
if (model instanceof MainThreadCustomEditorModel && model.isDirty()) {
const workingCopy = await model.backup(CancellationToken.None);
if (workingCopy.meta) {
// This cast is safe because we do an instanceof check above and a custom document backup data is always returned
this._editorRenameBackups.set(model.editorResource.toString(), workingCopy.meta as CustomDocumentBackupData);
}
}
}
})());
}
//#endregion
}
namespace HotExitState {