mirror of
https://github.com/Microsoft/vscode
synced 2024-10-02 09:18:59 +00:00
Label custom editor as deleted if the backing file is deleted
Fixes #99270 This should make it so a custom editor tab is tagged `(deleted)` if the backing file is deleted
This commit is contained in:
parent
d89e2e17a5
commit
f3f4eebecb
|
@ -4,7 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { multibyteAwareBtoa } from 'vs/base/browser/dom';
|
||||
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
|
@ -16,7 +16,7 @@ import { URI, UriComponents } from 'vs/base/common/uri';
|
|||
import * as modes from 'vs/editor/common/modes';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { FileChangesEvent, FileChangeType, 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';
|
||||
|
@ -274,6 +274,8 @@ namespace HotExitState {
|
|||
|
||||
class MainThreadCustomEditorModel extends Disposable implements ICustomEditorModel, IWorkingCopy {
|
||||
|
||||
#isDisposed = false;
|
||||
|
||||
private _fromBackup: boolean = false;
|
||||
private _hotExitState: HotExitState.State = HotExitState.Allowed;
|
||||
private _backupId: string | undefined;
|
||||
|
@ -282,9 +284,13 @@ 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;
|
||||
|
||||
public static async create(
|
||||
instantiationService: IInstantiationService,
|
||||
proxy: extHostProtocol.ExtHostCustomEditorsShape,
|
||||
|
@ -321,6 +327,8 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
|
|||
if (_editable) {
|
||||
this._register(workingCopyService.registerWorkingCopy(this));
|
||||
}
|
||||
|
||||
this._register(_fileService.onDidFilesChange(e => this.onDidFilesChange(e)));
|
||||
}
|
||||
|
||||
get editorResource() {
|
||||
|
@ -328,10 +336,14 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
|
|||
}
|
||||
|
||||
dispose() {
|
||||
this.#isDisposed = true;
|
||||
|
||||
if (this._editable) {
|
||||
this._undoService.removeElements(this._editorResource);
|
||||
}
|
||||
|
||||
this._proxy.$disposeCustomDocument(this._editorResource, this._viewType);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -371,6 +383,10 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
|
|||
return this._fromBackup;
|
||||
}
|
||||
|
||||
public isOrphaned(): boolean {
|
||||
return this._inOrphaned;
|
||||
}
|
||||
|
||||
private isUntitled() {
|
||||
return this._editorResource.scheme === Schemas.untitled;
|
||||
}
|
||||
|
@ -383,6 +399,58 @@ 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 isReadonly() {
|
||||
return !this._editable;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { basename } from 'vs/base/common/path';
|
|||
import { isEqual } from 'vs/base/common/resources';
|
||||
import { assertIsDefined } from 'vs/base/common/types';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
|
@ -63,9 +64,9 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput {
|
|||
return true;
|
||||
}
|
||||
|
||||
@memoize
|
||||
getName(): string {
|
||||
return basename(this.labelService.getUriLabel(this.resource));
|
||||
const name = basename(this.labelService.getUriLabel(this.resource));
|
||||
return this.decorateLabel(name);
|
||||
}
|
||||
|
||||
matches(other: IEditorInput): boolean {
|
||||
|
@ -92,15 +93,34 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput {
|
|||
public getTitle(verbosity?: Verbosity): string {
|
||||
switch (verbosity) {
|
||||
case Verbosity.SHORT:
|
||||
return this.shortTitle;
|
||||
return this.decorateLabel(this.shortTitle);
|
||||
default:
|
||||
case Verbosity.MEDIUM:
|
||||
return this.mediumTitle;
|
||||
return this.decorateLabel(this.mediumTitle);
|
||||
case Verbosity.LONG:
|
||||
return this.longTitle;
|
||||
return this.decorateLabel(this.longTitle);
|
||||
}
|
||||
}
|
||||
|
||||
private decorateLabel(label: string): string {
|
||||
const orphaned = this._modelRef?.object.isOrphaned();
|
||||
const readonly = this.isReadonly();
|
||||
|
||||
if (orphaned && readonly) {
|
||||
return localize('orphanedReadonlyFile', "{0} (deleted, read-only)", label);
|
||||
}
|
||||
|
||||
if (orphaned) {
|
||||
return localize('orphanedFile', "{0} (deleted)", label);
|
||||
}
|
||||
|
||||
if (readonly) {
|
||||
return localize('readonlyFile', "{0} (read-only)", label);
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
public isReadonly(): boolean {
|
||||
return this._modelRef ? this._modelRef.object.isReadonly() : false;
|
||||
}
|
||||
|
@ -169,6 +189,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput {
|
|||
if (!this._modelRef) {
|
||||
this._modelRef = this._register(assertIsDefined(await this.customEditorService.models.tryRetain(this.resource, this.viewType)));
|
||||
this._register(this._modelRef.object.onDidChangeDirty(() => this._onDidChangeDirty.fire()));
|
||||
this._register(this._modelRef.object.onDidChangeOrphaned(() => this._onDidChangeLabel.fire()));
|
||||
|
||||
if (this.isDirty()) {
|
||||
this._onDidChangeDirty.fire();
|
||||
|
|
|
@ -61,6 +61,9 @@ export interface ICustomEditorModel extends IDisposable {
|
|||
|
||||
isReadonly(): boolean;
|
||||
|
||||
isOrphaned(): boolean;
|
||||
readonly onDidChangeOrphaned: Event<void>;
|
||||
|
||||
isDirty(): boolean;
|
||||
readonly onDidChangeDirty: Event<void>;
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import { URI } from 'vs/base/common/uri';
|
|||
import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
import { IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor';
|
||||
import { ICustomEditorModel } from 'vs/workbench/contrib/customEditor/common/customEditor';
|
||||
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { ITextFileEditorModel, ITextFileService, TextFileEditorModelState } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export class CustomTextEditorModel extends Disposable implements ICustomEditorModel {
|
||||
|
@ -28,6 +28,11 @@ export class CustomTextEditorModel extends Disposable implements ICustomEditorMo
|
|||
});
|
||||
}
|
||||
|
||||
private readonly _textFileModel: ITextFileEditorModel | undefined;
|
||||
|
||||
private readonly _onDidChangeOrphaned = this._register(new Emitter<void>());
|
||||
public readonly onDidChangeOrphaned = this._onDidChangeOrphaned.event;
|
||||
|
||||
private constructor(
|
||||
public readonly viewType: string,
|
||||
private readonly _resource: URI,
|
||||
|
@ -38,6 +43,11 @@ export class CustomTextEditorModel extends Disposable implements ICustomEditorMo
|
|||
|
||||
this._register(_model);
|
||||
|
||||
this._textFileModel = this.textFileService.files.get(_resource);
|
||||
if (this._textFileModel) {
|
||||
this._register(this._textFileModel.onDidChangeOrphaned(() => this._onDidChangeOrphaned.fire()));
|
||||
}
|
||||
|
||||
this._register(this.textFileService.files.onDidChangeDirty(e => {
|
||||
if (isEqual(this.resource, e.resource)) {
|
||||
this._onDidChangeDirty.fire();
|
||||
|
@ -62,6 +72,10 @@ export class CustomTextEditorModel extends Disposable implements ICustomEditorMo
|
|||
return this.textFileService.isDirty(this.resource);
|
||||
}
|
||||
|
||||
public isOrphaned(): boolean {
|
||||
return !!this._textFileModel?.hasState(TextFileEditorModelState.ORPHAN);
|
||||
}
|
||||
|
||||
private readonly _onDidChangeDirty: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onDidChangeDirty: Event<void> = this._onDidChangeDirty.event;
|
||||
|
||||
|
|
Loading…
Reference in a new issue