working copy - introduce generic resource working copy that can track orphaned state

This commit is contained in:
Benjamin Pasero
2021-05-07 10:06:38 +02:00
parent 6bee0f2f3f
commit 7d5b672eb7
4 changed files with 264 additions and 186 deletions

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { multibyteAwareBtoa } from 'vs/base/browser/dom';
import { CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async';
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { VSBuffer } from 'vs/base/common/buffer';
import { CancellationToken } from 'vs/base/common/cancellation';
import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors';
@@ -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 { FileChangesEvent, FileChangeType, FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files';
import { 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';
@@ -38,6 +38,7 @@ import { IPathService } from 'vs/workbench/services/path/common/pathService';
import { IWorkingCopyFileService } 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';
const enum CustomEditorModelType {
Custom,
@@ -276,9 +277,7 @@ namespace HotExitState {
}
class MainThreadCustomEditorModel extends Disposable implements ICustomEditorModel, IWorkingCopy {
#isDisposed = false;
class MainThreadCustomEditorModel extends ResourceWorkingCopy implements ICustomEditorModel {
private _fromBackup: boolean = false;
private _hotExitState: HotExitState.State = HotExitState.Allowed;
@@ -288,13 +287,9 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
private _savePoint: number = -1;
private readonly _edits: Array<number> = [];
private _isDirtyFromContentChange = false;
private _inOrphaned = false;
private _ongoingSave?: CancelablePromise<void>;
private readonly _onDidChangeOrphaned = this._register(new Emitter<void>());
public readonly onDidChangeOrphaned = this._onDidChangeOrphaned.event;
// TODO@mjbvz consider to enable a `typeId` that is specific for custom
// editors. Using a distinct `typeId` allows the working copy to have
// any resource (including file based resources) even if other working
@@ -334,14 +329,14 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
startDirty: boolean,
private readonly _getEditors: () => CustomEditorInput[],
@IFileDialogService private readonly _fileDialogService: IFileDialogService,
@IFileService private readonly _fileService: IFileService,
@IFileService fileService: IFileService,
@ILabelService private readonly _labelService: ILabelService,
@IUndoRedoService private readonly _undoService: IUndoRedoService,
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
@IWorkingCopyService workingCopyService: IWorkingCopyService,
@IPathService private readonly _pathService: IPathService,
) {
super();
super(MainThreadCustomEditorModel.toWorkingCopyResource(_viewType, _editorResource), fileService);
this._fromBackup = fromBackup;
@@ -353,8 +348,6 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
if (startDirty) {
this._isDirtyFromContentChange = true;
}
this._register(_fileService.onDidFilesChange(e => this.onDidFilesChange(e)));
}
get editorResource() {
@@ -362,8 +355,6 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
}
override dispose() {
this.#isDisposed = true;
if (this._editable) {
this._undoService.removeElements(this._editorResource);
}
@@ -375,11 +366,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
//#region IWorkingCopy
public get resource() {
// Make sure each custom editor has a unique resource for backup and edits
return MainThreadCustomEditorModel.toWorkingCopyResource(this._viewType, this._editorResource);
}
// Make sure each custom editor has a unique resource for backup and edits
private static toWorkingCopyResource(viewType: string, resource: URI) {
const authority = viewType.replace(/[^a-z0-9\-_]/gi, '-');
const path = `/${multibyteAwareBtoa(resource.with({ query: null, fragment: null }).toString(true))}`;
@@ -409,10 +396,6 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
return this._fromBackup;
}
public isOrphaned(): boolean {
return this._inOrphaned;
}
private isUntitled() {
return this._editorResource.scheme === Schemas.untitled;
}
@@ -425,64 +408,12 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
//#endregion
private async onDidFilesChange(e: FileChangesEvent): Promise<void> {
let fileEventImpactsModel = false;
let newInOrphanModeGuess: boolean | undefined;
// If we are currently orphaned, we check if the model file was added back
if (this._inOrphaned) {
const modelFileAdded = e.contains(this.editorResource, FileChangeType.ADDED);
if (modelFileAdded) {
newInOrphanModeGuess = false;
fileEventImpactsModel = true;
}
}
// Otherwise we check if the model file was deleted
else {
const modelFileDeleted = e.contains(this.editorResource, FileChangeType.DELETED);
if (modelFileDeleted) {
newInOrphanModeGuess = true;
fileEventImpactsModel = true;
}
}
if (fileEventImpactsModel && this._inOrphaned !== newInOrphanModeGuess) {
let newInOrphanModeValidated: boolean = false;
if (newInOrphanModeGuess) {
// We have received reports of users seeing delete events even though the file still
// exists (network shares issue: https://github.com/microsoft/vscode/issues/13665).
// Since we do not want to mark the model as orphaned, we have to check if the
// file is really gone and not just a faulty file event.
await timeout(100);
if (this.#isDisposed) {
newInOrphanModeValidated = true;
} else {
const exists = await this._fileService.exists(this.editorResource);
newInOrphanModeValidated = !exists;
}
}
if (this._inOrphaned !== newInOrphanModeValidated && !this.#isDisposed) {
this.setOrphaned(newInOrphanModeValidated);
}
}
}
private setOrphaned(orphaned: boolean): void {
if (this._inOrphaned !== orphaned) {
this._inOrphaned = orphaned;
this._onDidChangeOrphaned.fire();
}
}
public isEditable(): boolean {
return this._editable;
}
public isOnReadonlyFileSystem(): boolean {
return this._fileService.hasCapability(this.editorResource, FileSystemProviderCapabilities.Readonly);
return this.fileService.hasCapability(this.editorResource, FileSystemProviderCapabilities.Readonly);
}
public get viewType() {
@@ -656,7 +587,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
return true;
} else {
// Since the editor is readonly, just copy the file over
await this._fileService.copy(resource, targetResource, false /* overwrite */);
await this.fileService.copy(resource, targetResource, false /* overwrite */);
return true;
}
}