Skip to content

Commit 884c2d2

Browse files
author
Benjamin Pasero
committed
editors - move code that closes editors on file deletes to editor service to support all editors
1 parent aa23435 commit 884c2d2

8 files changed

Lines changed: 141 additions & 128 deletions

File tree

src/vs/workbench/common/editor.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,11 @@ export interface IFileEditorInput extends IEditorInput, IEncodingSupport, IModeS
780780
* Forces this file input to open as binary instead of text.
781781
*/
782782
setForceOpenAsBinary(): void;
783+
784+
/**
785+
* Figure out if the input has been resolved or not.
786+
*/
787+
isResolved(): boolean;
783788
}
784789

785790
/**

src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts

Lines changed: 8 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,20 @@
66
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
77
import { URI } from 'vs/base/common/uri';
88
import { IEditorViewState } from 'vs/editor/common/editorCommon';
9-
import { toResource, SideBySideEditorInput, IWorkbenchEditorConfiguration, SideBySideEditor as SideBySideEditorChoice } from 'vs/workbench/common/editor';
9+
import { toResource, SideBySideEditor as SideBySideEditorChoice } from 'vs/workbench/common/editor';
1010
import { ITextFileService, TextFileEditorModelState } from 'vs/workbench/services/textfile/common/textfiles';
11-
import { FileOperationEvent, FileOperation, IFileService, FileChangeType, FileChangesEvent, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
11+
import { FileOperationEvent, FileOperation, IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
1212
import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput';
1313
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
1414
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
1515
import { distinct, coalesce } from 'vs/base/common/arrays';
16-
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
17-
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
1816
import { ResourceMap } from 'vs/base/common/map';
1917
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
2018
import { isCodeEditor } from 'vs/editor/browser/editorBrowser';
2119
import { IHostService } from 'vs/workbench/services/host/browser/host';
2220
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
2321
import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
24-
import { timeout, RunOnceWorker } from 'vs/base/common/async';
22+
import { RunOnceWorker } from 'vs/base/common/async';
2523
import { withNullAsUndefined } from 'vs/base/common/types';
2624
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
2725
import { isEqualOrParent, joinPath } from 'vs/base/common/resources';
@@ -37,26 +35,19 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut
3735
@ILifecycleService private readonly lifecycleService: ILifecycleService,
3836
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
3937
@IFileService private readonly fileService: IFileService,
40-
@IEnvironmentService private readonly environmentService: IEnvironmentService,
41-
@IConfigurationService private readonly configurationService: IConfigurationService,
4238
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
4339
@IHostService private readonly hostService: IHostService,
4440
@ICodeEditorService private readonly codeEditorService: ICodeEditorService
4541
) {
4642
super();
4743

48-
this.onConfigurationUpdated(configurationService.getValue<IWorkbenchEditorConfiguration>());
49-
5044
this.registerListeners();
5145
}
5246

5347
private registerListeners(): void {
5448

5549
// Update editors from operation changes
56-
this._register(this.fileService.onDidRunOperation(e => this.onFileOperation(e)));
57-
58-
// Update editors from disk changes
59-
this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e)));
50+
this._register(this.fileService.onDidRunOperation(e => this.onDidRunFileOperation(e)));
6051

6152
// Ensure dirty text file and untitled models are always opened as editors
6253
this._register(this.textFileService.files.onDidChangeDirty(model => this.ensureDirtyFilesAreOpenedWorker.work(model.resource)));
@@ -69,9 +60,6 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut
6960
// Update visible editors when focus is gained
7061
this._register(this.hostService.onDidChangeFocus(e => this.onWindowFocusChange(e)));
7162

72-
// Configuration
73-
this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue<IWorkbenchEditorConfiguration>())));
74-
7563
// Lifecycle
7664
this.lifecycleService.onShutdown(this.dispose, this);
7765
}
@@ -82,17 +70,12 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut
8270
// carrying all necessary data in all environments, we also use the file operation events to make sure operations are handled.
8371
// In any case there is no guarantee if the local event is fired first or the disk one. Thus, code must handle the case
8472
// that the event ordering is random as well as might not carry all information needed.
85-
private onFileOperation(e: FileOperationEvent): void {
73+
private onDidRunFileOperation(e: FileOperationEvent): void {
8674

8775
// Handle moves specially when file is opened
8876
if (e.isOperation(FileOperation.MOVE)) {
8977
this.handleMovedFileInOpenedFileEditors(e.resource, e.target.resource);
9078
}
91-
92-
// Handle deletes
93-
if (e.isOperation(FileOperation.DELETE) || e.isOperation(FileOperation.MOVE)) {
94-
this.handleDeletes(e.resource, false, e.target ? e.target.resource : undefined);
95-
}
9679
}
9780

9881
private handleMovedFileInOpenedFileEditors(oldResource: URI, newResource: URI): void {
@@ -175,110 +158,11 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut
175158

176159
//#endregion
177160

178-
//#region File Changes: Close editors of deleted files unless configured otherwise
179-
180-
private closeOnFileDelete: boolean = false;
181-
182-
private onConfigurationUpdated(configuration: IWorkbenchEditorConfiguration): void {
183-
if (typeof configuration.workbench?.editor?.closeOnFileDelete === 'boolean') {
184-
this.closeOnFileDelete = configuration.workbench.editor.closeOnFileDelete;
185-
} else {
186-
this.closeOnFileDelete = false; // default
187-
}
188-
}
189-
190-
private onDidFilesChange(e: FileChangesEvent): void {
191-
if (e.gotDeleted()) {
192-
this.handleDeletes(e, true);
193-
}
194-
}
195-
196-
private handleDeletes(arg1: URI | FileChangesEvent, isExternal: boolean, movedTo?: URI): void {
197-
const nonDirtyFileEditors = this.getNonDirtyFileEditors();
198-
nonDirtyFileEditors.forEach(async editor => {
199-
const resource = editor.resource;
200-
201-
// Handle deletes in opened editors depending on:
202-
// - the user has not disabled the setting closeOnFileDelete
203-
// - the file change is local or external
204-
// - the input is not resolved (we need to dispose because we cannot restore otherwise since we do not have the contents)
205-
if (this.closeOnFileDelete || !isExternal || !editor.isResolved()) {
206-
207-
// Do NOT close any opened editor that matches the resource path (either equal or being parent) of the
208-
// resource we move to (movedTo). Otherwise we would close a resource that has been renamed to the same
209-
// path but different casing.
210-
if (movedTo && isEqualOrParent(resource, movedTo)) {
211-
return;
212-
}
213-
214-
let matches = false;
215-
if (arg1 instanceof FileChangesEvent) {
216-
matches = arg1.contains(resource, FileChangeType.DELETED);
217-
} else {
218-
matches = isEqualOrParent(resource, arg1);
219-
}
220-
221-
if (!matches) {
222-
return;
223-
}
224-
225-
// We have received reports of users seeing delete events even though the file still
226-
// exists (network shares issue: https://github.com/Microsoft/vscode/issues/13665).
227-
// Since we do not want to close an editor without reason, we have to check if the
228-
// file is really gone and not just a faulty file event.
229-
// This only applies to external file events, so we need to check for the isExternal
230-
// flag.
231-
let exists = false;
232-
if (isExternal) {
233-
await timeout(100);
234-
exists = await this.fileService.exists(resource);
235-
}
236-
237-
if (!exists && !editor.isDisposed()) {
238-
editor.dispose();
239-
} else if (this.environmentService.verbose) {
240-
console.warn(`File exists even though we received a delete event: ${resource.toString()}`);
241-
}
242-
}
243-
});
244-
}
245-
246-
private getNonDirtyFileEditors(): FileEditorInput[] {
247-
const editors: FileEditorInput[] = [];
248-
249-
this.editorService.editors.forEach(editor => {
250-
if (editor instanceof FileEditorInput) {
251-
if (!editor.isDirty()) {
252-
editors.push(editor);
253-
}
254-
} else if (editor instanceof SideBySideEditorInput) {
255-
const master = editor.master;
256-
const details = editor.details;
257-
258-
if (master instanceof FileEditorInput) {
259-
if (!master.isDirty()) {
260-
editors.push(master);
261-
}
262-
}
263-
264-
if (details instanceof FileEditorInput) {
265-
if (!details.isDirty()) {
266-
editors.push(details);
267-
}
268-
}
269-
}
270-
});
271-
272-
return editors;
273-
}
274-
275-
//#endregion
276-
277161
//#region Text File: Ensure every dirty text and untitled file is opened in an editor
278162

279-
private readonly ensureDirtyFilesAreOpenedWorker = this._register(new RunOnceWorker<URI>(units => this.ensureDirtyFilesAreOpened(units), 250));
163+
private readonly ensureDirtyFilesAreOpenedWorker = this._register(new RunOnceWorker<URI>(units => this.ensureDirtyTextFilesAreOpened(units), 250));
280164

281-
private ensureDirtyFilesAreOpened(resources: URI[]): void {
165+
private ensureDirtyTextFilesAreOpened(resources: URI[]): void {
282166
this.doEnsureDirtyFilesAreOpened(distinct(resources.filter(resource => {
283167
if (!this.textFileService.isDirty(resource)) {
284168
return false; // resource must be dirty
@@ -347,7 +231,7 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut
347231

348232
//#endregion
349233

350-
//#region Window Focus Change: Update visible code editors when focus is gained
234+
//#region Window Focus Change: Update visible code editors when focus is gained that have a known text file model
351235

352236
private onWindowFocusChange(focused: boolean): void {
353237
if (focused) {

src/vs/workbench/services/editor/browser/editorService.ts

Lines changed: 123 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55

66
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
77
import { IResourceInput, ITextEditorOptions, IEditorOptions, EditorActivation } from 'vs/platform/editor/common/editor';
8-
import { IEditorInput, IEditor, GroupIdentifier, IFileEditorInput, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IFileInputFactory, EditorInput, SideBySideEditorInput, IEditorInputWithOptions, isEditorInputWithOptions, EditorOptions, TextEditorOptions, IEditorIdentifier, IEditorCloseEvent, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, IRevertOptions, SaveReason, EditorsOrder, isTextEditor } from 'vs/workbench/common/editor';
8+
import { IEditorInput, IEditor, GroupIdentifier, IFileEditorInput, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IFileInputFactory, EditorInput, SideBySideEditorInput, IEditorInputWithOptions, isEditorInputWithOptions, EditorOptions, TextEditorOptions, IEditorIdentifier, IEditorCloseEvent, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, IRevertOptions, SaveReason, EditorsOrder, isTextEditor, IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor';
99
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
1010
import { Registry } from 'vs/platform/registry/common/platform';
1111
import { ResourceMap } from 'vs/base/common/map';
1212
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
13-
import { IFileService } from 'vs/platform/files/common/files';
13+
import { IFileService, FileOperationEvent, FileOperation, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files';
1414
import { Schemas } from 'vs/base/common/network';
1515
import { Event, Emitter } from 'vs/base/common/event';
1616
import { URI } from 'vs/base/common/uri';
17-
import { basename } from 'vs/base/common/resources';
17+
import { basename, isEqualOrParent } from 'vs/base/common/resources';
1818
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
1919
import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService';
2020
import { IResourceEditor, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, IVisibleEditor, IEditorService, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE, ISaveEditorsOptions, ISaveAllEditorsOptions, IRevertAllEditorsOptions, IBaseSaveRevertAllEditorOptions } from 'vs/workbench/services/editor/common/editorService';
@@ -30,6 +30,8 @@ import { EditorsObserver } from 'vs/workbench/browser/parts/editor/editorsObserv
3030
import { IEditorViewState } from 'vs/editor/common/editorCommon';
3131
import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel';
3232
import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput';
33+
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
34+
import { timeout } from 'vs/base/common/async';
3335

3436
type CachedEditorInput = ResourceEditorInput | IFileEditorInput | UntitledTextEditorInput;
3537
type OpenInEditorGroup = IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE;
@@ -72,12 +74,15 @@ export class EditorService extends Disposable implements EditorServiceImpl {
7274
@IInstantiationService private readonly instantiationService: IInstantiationService,
7375
@ILabelService private readonly labelService: ILabelService,
7476
@IFileService private readonly fileService: IFileService,
75-
@IConfigurationService private readonly configurationService: IConfigurationService
77+
@IConfigurationService private readonly configurationService: IConfigurationService,
78+
@IEnvironmentService private readonly environmentService: IEnvironmentService
7679
) {
7780
super();
7881

7982
this.fileInputFactory = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).getFileInputFactory();
8083

84+
this.onConfigurationUpdated(configurationService.getValue<IWorkbenchEditorConfiguration>());
85+
8186
this.registerListeners();
8287
}
8388

@@ -88,6 +93,13 @@ export class EditorService extends Disposable implements EditorServiceImpl {
8893
this.editorGroupService.onDidActiveGroupChange(group => this.handleActiveEditorChange(group));
8994
this.editorGroupService.onDidAddGroup(group => this.registerGroupListeners(group as IEditorGroupView));
9095
this.editorsObserver.onDidChange(() => this._onDidMostRecentlyActiveEditorsChange.fire());
96+
97+
// File changes & operations
98+
this._register(this.fileService.onDidRunOperation(e => this.onDidRunFileOperation(e)));
99+
this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e)));
100+
101+
// Configuration
102+
this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue<IWorkbenchEditorConfiguration>())));
91103
}
92104

93105
private onEditorsRestored(): void {
@@ -166,6 +178,113 @@ export class EditorService extends Disposable implements EditorServiceImpl {
166178
}
167179
}
168180

181+
//#region File Changes: Close editors of deleted files unless configured otherwise
182+
183+
private closeOnFileDelete: boolean = false;
184+
185+
private onConfigurationUpdated(configuration: IWorkbenchEditorConfiguration): void {
186+
if (typeof configuration.workbench?.editor?.closeOnFileDelete === 'boolean') {
187+
this.closeOnFileDelete = configuration.workbench.editor.closeOnFileDelete;
188+
} else {
189+
this.closeOnFileDelete = false; // default
190+
}
191+
}
192+
193+
private onDidFilesChange(e: FileChangesEvent): void {
194+
if (e.gotDeleted()) {
195+
this.handleDeletes(e, true);
196+
}
197+
}
198+
199+
private onDidRunFileOperation(e: FileOperationEvent): void {
200+
if (e.isOperation(FileOperation.DELETE) || e.isOperation(FileOperation.MOVE)) {
201+
this.handleDeletes(e.resource, false, e.target ? e.target.resource : undefined);
202+
}
203+
}
204+
205+
private handleDeletes(arg1: URI | FileChangesEvent, isExternal: boolean, movedTo?: URI): void {
206+
for (const editor of this.getAllNonDirtyEditors({ includeUntitled: false, supportSideBySide: true })) {
207+
(async () => {
208+
const resource = editor.resource;
209+
if (!resource) {
210+
return;
211+
}
212+
213+
// Handle deletes in opened editors depending on:
214+
// - the user has not disabled the setting closeOnFileDelete
215+
// - the file change is local
216+
// - the input is a file that is not resolved (we need to dispose because we cannot restore otherwise since we do not have the contents)
217+
if (this.closeOnFileDelete || !isExternal || (this.fileInputFactory.isFileInput(editor) && !editor.isResolved())) {
218+
219+
// Do NOT close any opened editor that matches the resource path (either equal or being parent) of the
220+
// resource we move to (movedTo). Otherwise we would close a resource that has been renamed to the same
221+
// path but different casing.
222+
if (movedTo && isEqualOrParent(resource, movedTo)) {
223+
return;
224+
}
225+
226+
let matches = false;
227+
if (arg1 instanceof FileChangesEvent) {
228+
matches = arg1.contains(resource, FileChangeType.DELETED);
229+
} else {
230+
matches = isEqualOrParent(resource, arg1);
231+
}
232+
233+
if (!matches) {
234+
return;
235+
}
236+
237+
// We have received reports of users seeing delete events even though the file still
238+
// exists (network shares issue: https://github.com/Microsoft/vscode/issues/13665).
239+
// Since we do not want to close an editor without reason, we have to check if the
240+
// file is really gone and not just a faulty file event.
241+
// This only applies to external file events, so we need to check for the isExternal
242+
// flag.
243+
let exists = false;
244+
if (isExternal && this.fileService.canHandleResource(resource)) {
245+
await timeout(100);
246+
exists = await this.fileService.exists(resource);
247+
}
248+
249+
if (!exists && !editor.isDisposed()) {
250+
editor.dispose();
251+
} else if (this.environmentService.verbose) {
252+
console.warn(`File exists even though we received a delete event: ${resource.toString()}`);
253+
}
254+
}
255+
})();
256+
}
257+
}
258+
259+
private getAllNonDirtyEditors(options: { includeUntitled: boolean, supportSideBySide: boolean }): IEditorInput[] {
260+
const editors: IEditorInput[] = [];
261+
262+
function conditionallyAddEditor(editor: IEditorInput): void {
263+
if (editor.isUntitled() && !options.includeUntitled) {
264+
return;
265+
}
266+
267+
if (editor.isDirty()) {
268+
return;
269+
}
270+
271+
editors.push(editor);
272+
}
273+
274+
for (const editor of this.editors) {
275+
if (options.supportSideBySide && editor instanceof SideBySideEditorInput) {
276+
conditionallyAddEditor(editor.master);
277+
conditionallyAddEditor(editor.details);
278+
} else {
279+
conditionallyAddEditor(editor);
280+
}
281+
}
282+
283+
return editors;
284+
}
285+
286+
//#endregion
287+
169288
get activeControl(): IVisibleEditor | undefined {
170289
return this.editorGroupService.activeGroup?.activeControl;
171290
}

src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class TestEditorInput extends EditorInput implements IFileEditorInput {
5252
setMode(mode: string) { }
5353
setPreferredMode(mode: string) { }
5454
setForceOpenAsBinary(): void { }
55+
isResolved(): boolean { return false; }
5556
}
5657

5758
suite('EditorGroupsService', () => {

0 commit comments

Comments
 (0)