Skip to content

Commit 66c09fb

Browse files
author
Benjamin Pasero
committed
editors - add and adopt IEditorService#isOpen(resource) for fast lookups of opened editors
1 parent f250297 commit 66c09fb

11 files changed

Lines changed: 225 additions & 38 deletions

File tree

src/vs/workbench/browser/dnd.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ export class ResourcesDropHandler {
246246
}
247247

248248
// File: ensure the file is not dirty or opened already
249-
else if (this.textFileService.isDirty(droppedDirtyEditor.resource) || this.editorService.isOpen(this.editorService.createInput({ resource: droppedDirtyEditor.resource, forceFile: true }))) {
249+
else if (this.textFileService.isDirty(droppedDirtyEditor.resource) || this.editorService.isOpen({ resource: droppedDirtyEditor.resource })) {
250250
return false;
251251
}
252252

src/vs/workbench/browser/parts/editor/editorGroupView.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1075,7 +1075,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
10751075
// Update model and make sure to continue to use the editor we get from
10761076
// the model. It is possible that the editor was already opened and we
10771077
// want to ensure that we use the existing instance in that case.
1078-
const editor = this.group.getEditorByIndex(currentIndex)!;
1078+
const editor = this._group.getEditorByIndex(currentIndex);
1079+
if (!editor) {
1080+
return;
1081+
}
10791082

10801083
// Update model
10811084
this._group.moveEditor(editor, moveToIndex);
@@ -1278,7 +1281,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
12781281
return false; // editor must be dirty and not saving
12791282
}
12801283

1281-
if (editor instanceof SideBySideEditorInput && this.isOpened(editor.master)) {
1284+
if (editor instanceof SideBySideEditorInput && this._group.contains(editor.master)) {
12821285
return false; // master-side of editor is still opened somewhere else
12831286
}
12841287

src/vs/workbench/browser/parts/editor/editorsObserver.ts

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { IEditorInput, IEditorInputFactoryRegistry, IEditorIdentifier, GroupIdentifier, Extensions, IEditorPartOptionsChangeEvent, EditorsOrder } from 'vs/workbench/common/editor';
6+
import { IEditorInput, IEditorInputFactoryRegistry, IEditorIdentifier, GroupIdentifier, Extensions, IEditorPartOptionsChangeEvent, EditorsOrder, toResource, SideBySideEditor } from 'vs/workbench/common/editor';
77
import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
88
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
99
import { Registry } from 'vs/platform/registry/common/platform';
1010
import { Event, Emitter } from 'vs/base/common/event';
1111
import { IEditorGroupsService, IEditorGroup, GroupChangeKind, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService';
1212
import { coalesce } from 'vs/base/common/arrays';
13-
import { LinkedMap, Touch } from 'vs/base/common/map';
13+
import { LinkedMap, Touch, ResourceMap } from 'vs/base/common/map';
1414
import { equals } from 'vs/base/common/objects';
15+
import { URI } from 'vs/base/common/uri';
1516

1617
interface ISerializedEditorsList {
1718
entries: ISerializedEditorIdentifier[];
@@ -37,9 +38,10 @@ export class EditorsObserver extends Disposable {
3738

3839
private readonly keyMap = new Map<GroupIdentifier, Map<IEditorInput, IEditorIdentifier>>();
3940
private readonly mostRecentEditorsMap = new LinkedMap<IEditorIdentifier, IEditorIdentifier>();
41+
private readonly editorResourcesMap = new ResourceMap<number>();
4042

41-
private readonly _onDidChange = this._register(new Emitter<void>());
42-
readonly onDidChange = this._onDidChange.event;
43+
private readonly _onDidMostRecentlyActiveEditorsChange = this._register(new Emitter<void>());
44+
readonly onDidMostRecentlyActiveEditorsChange = this._onDidMostRecentlyActiveEditorsChange.event;
4345

4446
get count(): number {
4547
return this.mostRecentEditorsMap.size;
@@ -49,6 +51,10 @@ export class EditorsObserver extends Disposable {
4951
return this.mostRecentEditorsMap.values();
5052
}
5153

54+
hasEditor(resource: URI): boolean {
55+
return this.editorResourcesMap.has(resource);
56+
}
57+
5258
constructor(
5359
@IEditorGroupsService private editorGroupsService: IEditorGroupsService,
5460
@IStorageService private readonly storageService: IStorageService
@@ -72,12 +78,12 @@ export class EditorsObserver extends Disposable {
7278
// of the new group into our list in LRU order
7379
const groupEditorsMru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE);
7480
for (let i = groupEditorsMru.length - 1; i >= 0; i--) {
75-
this.addMostRecentEditor(group, groupEditorsMru[i], false /* is not active */);
81+
this.addMostRecentEditor(group, groupEditorsMru[i], false /* is not active */, true /* is new */);
7682
}
7783

7884
// Make sure that active editor is put as first if group is active
7985
if (this.editorGroupsService.activeGroup === group && group.activeEditor) {
80-
this.addMostRecentEditor(group, group.activeEditor, true /* is active */);
86+
this.addMostRecentEditor(group, group.activeEditor, true /* is active */, false /* already added before */);
8187
}
8288

8389
// Group Listeners
@@ -92,7 +98,7 @@ export class EditorsObserver extends Disposable {
9298
// Group gets active: put active editor as most recent
9399
case GroupChangeKind.GROUP_ACTIVE: {
94100
if (this.editorGroupsService.activeGroup === group && group.activeEditor) {
95-
this.addMostRecentEditor(group, group.activeEditor, true /* is active */);
101+
this.addMostRecentEditor(group, group.activeEditor, true /* is active */, false /* editor already opened */);
96102
}
97103

98104
break;
@@ -102,7 +108,7 @@ export class EditorsObserver extends Disposable {
102108
// if group is active, otherwise second most recent
103109
case GroupChangeKind.EDITOR_ACTIVE: {
104110
if (e.editor) {
105-
this.addMostRecentEditor(group, e.editor, this.editorGroupsService.activeGroup === group);
111+
this.addMostRecentEditor(group, e.editor, this.editorGroupsService.activeGroup === group, false /* editor already opened */);
106112
}
107113

108114
break;
@@ -114,7 +120,7 @@ export class EditorsObserver extends Disposable {
114120
// start to close oldest ones if needed.
115121
case GroupChangeKind.EDITOR_OPEN: {
116122
if (e.editor) {
117-
this.addMostRecentEditor(group, e.editor, false /* is not active */);
123+
this.addMostRecentEditor(group, e.editor, false /* is not active */, true /* is new */);
118124
this.ensureOpenedEditorsLimit({ groupId: group.id, editor: e.editor }, group.id);
119125
}
120126

@@ -148,7 +154,7 @@ export class EditorsObserver extends Disposable {
148154
}
149155
}
150156

151-
private addMostRecentEditor(group: IEditorGroup, editor: IEditorInput, isActive: boolean): void {
157+
private addMostRecentEditor(group: IEditorGroup, editor: IEditorInput, isActive: boolean, isNew: boolean): void {
152158
const key = this.ensureKey(group, editor);
153159
const mostRecentEditor = this.mostRecentEditorsMap.first;
154160

@@ -169,11 +175,39 @@ export class EditorsObserver extends Disposable {
169175
this.mostRecentEditorsMap.set(mostRecentEditor, mostRecentEditor, Touch.AsOld /* make first */);
170176
}
171177

178+
// Update in resource map if this is a new editor
179+
if (isNew) {
180+
this.updateEditorResourcesMap(editor, true);
181+
}
182+
172183
// Event
173-
this._onDidChange.fire();
184+
this._onDidMostRecentlyActiveEditorsChange.fire();
185+
}
186+
187+
private updateEditorResourcesMap(editor: IEditorInput, add: boolean): void {
188+
const resource = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER });
189+
if (!resource) {
190+
return; // require a resource
191+
}
192+
193+
if (add) {
194+
this.editorResourcesMap.set(resource, (this.editorResourcesMap.get(resource) ?? 0) + 1);
195+
} else {
196+
const counter = this.editorResourcesMap.get(resource) ?? 0;
197+
if (counter > 1) {
198+
this.editorResourcesMap.set(resource, counter - 1);
199+
} else {
200+
this.editorResourcesMap.delete(resource);
201+
}
202+
}
174203
}
175204

176205
private removeMostRecentEditor(group: IEditorGroup, editor: IEditorInput): void {
206+
207+
// Update in resource map
208+
this.updateEditorResourcesMap(editor, false);
209+
210+
// Update in MRU list
177211
const key = this.findKey(group, editor);
178212
if (key) {
179213

@@ -187,7 +221,7 @@ export class EditorsObserver extends Disposable {
187221
}
188222

189223
// Event
190-
this._onDidChange.fire();
224+
this._onDidMostRecentlyActiveEditorsChange.fire();
191225
}
192226
}
193227

@@ -361,7 +395,7 @@ export class EditorsObserver extends Disposable {
361395
const group = groups[i];
362396
const groupEditorsMru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE);
363397
for (let i = groupEditorsMru.length - 1; i >= 0; i--) {
364-
this.addMostRecentEditor(group, groupEditorsMru[i], true /* enforce as active to preserve order */);
398+
this.addMostRecentEditor(group, groupEditorsMru[i], true /* enforce as active to preserve order */, true /* is new */);
365399
}
366400
}
367401
}
@@ -392,6 +426,9 @@ export class EditorsObserver extends Disposable {
392426
// Make sure key is registered as well
393427
const editorIdentifier = this.ensureKey(group, editor);
394428
mapValues.push([editorIdentifier, editorIdentifier]);
429+
430+
// Update in resource map
431+
this.updateEditorResourcesMap(editor, true);
395432
}
396433

397434
// Fill map with deserialized values

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { IHostService } from 'vs/workbench/services/host/browser/host';
1313
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
1414
import { RunOnceWorker } from 'vs/base/common/async';
1515
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
16-
import { Schemas } from 'vs/base/common/network';
1716

1817
export class TextFileEditorTracker extends Disposable implements IWorkbenchContribution {
1918

@@ -45,7 +44,7 @@ export class TextFileEditorTracker extends Disposable implements IWorkbenchContr
4544

4645
//#region Text File: Ensure every dirty text and untitled file is opened in an editor
4746

48-
private readonly ensureDirtyFilesAreOpenedWorker = this._register(new RunOnceWorker<URI>(units => this.ensureDirtyTextFilesAreOpened(units), 250));
47+
private readonly ensureDirtyFilesAreOpenedWorker = this._register(new RunOnceWorker<URI>(units => this.ensureDirtyTextFilesAreOpened(units), 50));
4948

5049
private ensureDirtyTextFilesAreOpened(resources: URI[]): void {
5150
this.doEnsureDirtyTextFilesAreOpened(distinct(resources.filter(resource => {
@@ -58,7 +57,7 @@ export class TextFileEditorTracker extends Disposable implements IWorkbenchContr
5857
return false; // resource must not be pending to save
5958
}
6059

61-
if (this.editorService.isOpen(this.editorService.createInput({ resource, forceFile: resource.scheme !== Schemas.untitled, forceUntitled: resource.scheme === Schemas.untitled }))) {
60+
if (this.editorService.isOpen({ resource })) {
6261
return false; // model must not be opened already as file
6362
}
6463

src/vs/workbench/contrib/files/browser/views/openEditorsView.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,7 @@ class OpenEditorsDragAndDrop implements IListDragAndDrop<OpenEditor | IEditorGro
677677

678678
drop(data: IDragAndDropData, targetElement: OpenEditor | IEditorGroup, _targetIndex: number, originalEvent: DragEvent): void {
679679
const group = targetElement instanceof OpenEditor ? targetElement.group : targetElement;
680-
const index = targetElement instanceof OpenEditor ? targetElement.group.getIndexOfEditor(targetElement.editor) : 0;
680+
const index = targetElement instanceof OpenEditor ? targetElement.editorIndex : 0;
681681

682682
if (data instanceof ElementsDragAndDropData) {
683683
const elementsData = data.elements;

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
8484
this.editorGroupService.whenRestored.then(() => this.onEditorsRestored());
8585
this.editorGroupService.onDidActiveGroupChange(group => this.handleActiveEditorChange(group));
8686
this.editorGroupService.onDidAddGroup(group => this.registerGroupListeners(group as IEditorGroupView));
87-
this.editorsObserver.onDidChange(() => this._onDidMostRecentlyActiveEditorsChange.fire());
87+
this.editorsObserver.onDidMostRecentlyActiveEditorsChange(() => this._onDidMostRecentlyActiveEditorsChange.fire());
8888

8989
// Out of workspace file watchers
9090
this._register(this.onDidVisibleEditorsChange(() => this.handleVisibleEditorsChange()));
@@ -705,8 +705,18 @@ export class EditorService extends Disposable implements EditorServiceImpl {
705705

706706
//#region isOpen()
707707

708-
isOpen(editor: IEditorInput): boolean {
709-
return this.editorGroupService.groups.some(group => group.isOpened(editor));
708+
isOpen(editor: IEditorInput): boolean;
709+
isOpen(editor: IResourceInput): boolean;
710+
isOpen(editor: IEditorInput | IResourceInput): boolean {
711+
if (editor instanceof EditorInput) {
712+
return this.editorGroupService.groups.some(group => group.isOpened(editor));
713+
}
714+
715+
if (editor.resource) {
716+
return this.editorsObserver.hasEditor(editor.resource);
717+
}
718+
719+
return false;
710720
}
711721

712722
//#endregion
@@ -1169,7 +1179,9 @@ export class DelegatingEditorService implements IEditorService {
11691179
return this.editorService.replaceEditors(editors as IResourceEditorReplacement[] /* TS fail */, group);
11701180
}
11711181

1172-
isOpen(editor: IEditorInput): boolean { return this.editorService.isOpen(editor); }
1182+
isOpen(editor: IEditorInput): boolean;
1183+
isOpen(editor: IResourceInput): boolean;
1184+
isOpen(editor: IEditorInput | IResourceInput): boolean { return this.editorService.isOpen(editor as IResourceInput /* TS fail */); }
11731185

11741186
overrideOpenEditor(handler: IOpenEditorOverrideHandler): IDisposable { return this.editorService.overrideOpenEditor(handler); }
11751187

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,13 @@ export interface IEditorService {
196196
* Find out if the provided editor is opened in any editor group.
197197
*
198198
* Note: An editor can be opened but not actively visible.
199+
*
200+
* @param editor the editor to check for being opened. If a
201+
* `IResourceInput` is passed in, the resource is checked on
202+
* all opened editors. In case of a side by side editor, the
203+
* right hand side resource is considered only.
199204
*/
205+
isOpen(editor: IResourceInput): boolean;
200206
isOpen(editor: IEditorInput): boolean;
201207

202208
/**

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ suite('EditorService', () => {
105105
assert.ok(!service.activeTextEditorMode);
106106
assert.equal(service.visibleTextEditorWidgets.length, 0);
107107
assert.equal(service.isOpen(input), true);
108+
assert.equal(service.isOpen({ resource: input.resource }), true);
108109
assert.equal(activeEditorChangeEventCounter, 1);
109110
assert.equal(visibleEditorChangeEventCounter, 1);
110111

@@ -137,7 +138,9 @@ suite('EditorService', () => {
137138
assert.equal(otherInput, service.getEditors(EditorsOrder.SEQUENTIAL)[1].editor);
138139
assert.equal(service.visibleControls.length, 1);
139140
assert.equal(service.isOpen(input), true);
141+
assert.equal(service.isOpen({ resource: input.resource }), true);
140142
assert.equal(service.isOpen(otherInput), true);
143+
assert.equal(service.isOpen({ resource: otherInput.resource }), true);
141144

142145
assert.equal(activeEditorChangeEventCounter, 4);
143146
assert.equal(visibleEditorChangeEventCounter, 4);
@@ -149,6 +152,53 @@ suite('EditorService', () => {
149152
part.dispose();
150153
});
151154

155+
test('isOpen() with side by side editor', async () => {
156+
const [part, service] = createEditorService();
157+
158+
const input = new TestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID);
159+
const otherInput = new TestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID);
160+
const sideBySideInput = new SideBySideEditorInput('sideBySide', '', input, otherInput);
161+
162+
await part.whenRestored;
163+
164+
const editor1 = await service.openEditor(sideBySideInput, { pinned: true });
165+
assert.equal(part.activeGroup.count, 1);
166+
167+
assert.equal(service.isOpen(input), false);
168+
assert.equal(service.isOpen(otherInput), false);
169+
assert.equal(service.isOpen(sideBySideInput), true);
170+
assert.equal(service.isOpen({ resource: input.resource }), false);
171+
assert.equal(service.isOpen({ resource: otherInput.resource }), true);
172+
173+
const editor2 = await service.openEditor(input, { pinned: true });
174+
assert.equal(part.activeGroup.count, 2);
175+
176+
assert.equal(service.isOpen(input), true);
177+
assert.equal(service.isOpen(otherInput), false);
178+
assert.equal(service.isOpen(sideBySideInput), true);
179+
assert.equal(service.isOpen({ resource: input.resource }), true);
180+
assert.equal(service.isOpen({ resource: otherInput.resource }), true);
181+
182+
await editor2?.group?.closeEditor(input);
183+
assert.equal(part.activeGroup.count, 1);
184+
185+
assert.equal(service.isOpen(input), false);
186+
assert.equal(service.isOpen(otherInput), false);
187+
assert.equal(service.isOpen(sideBySideInput), true);
188+
assert.equal(service.isOpen({ resource: input.resource }), false);
189+
assert.equal(service.isOpen({ resource: otherInput.resource }), true);
190+
191+
await editor1?.group?.closeEditor(sideBySideInput);
192+
193+
assert.equal(service.isOpen(input), false);
194+
assert.equal(service.isOpen(otherInput), false);
195+
assert.equal(service.isOpen(sideBySideInput), false);
196+
assert.equal(service.isOpen({ resource: input.resource }), false);
197+
assert.equal(service.isOpen({ resource: otherInput.resource }), false);
198+
199+
part.dispose();
200+
});
201+
152202
test('openEditors() / replaceEditors()', async () => {
153203
const [part, service] = createEditorService();
154204

0 commit comments

Comments
 (0)