Skip to content

Commit 1eac96d

Browse files
author
Benjamin Pasero
committed
history - use editor input factory to support to reopen any editor that can be serialised
1 parent 516bb5f commit 1eac96d

6 files changed

Lines changed: 139 additions & 88 deletions

File tree

src/vs/base/common/arrays.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -489,12 +489,21 @@ export function index<T, R>(array: ReadonlyArray<T>, indexer: (t: T) => string,
489489
export function insert<T>(array: T[], element: T): () => void {
490490
array.push(element);
491491

492-
return () => {
493-
const index = array.indexOf(element);
494-
if (index > -1) {
495-
array.splice(index, 1);
496-
}
497-
};
492+
return () => remove(array, element);
493+
}
494+
495+
/**
496+
* Removes an element from an array if it can be found.
497+
*/
498+
export function remove<T>(array: T[], element: T): T | undefined {
499+
const index = array.indexOf(element);
500+
if (index > -1) {
501+
array.splice(index, 1);
502+
503+
return element;
504+
}
505+
506+
return undefined;
498507
}
499508

500509
/**

src/vs/base/test/common/arrays.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,5 +342,14 @@ suite('Arrays', () => {
342342
arrays.coalesceInPlace(sparse);
343343
assert.equal(sparse.length, 5);
344344
});
345+
346+
test('insert, remove', function () {
347+
const array: string[] = [];
348+
const remove = arrays.insert(array, 'foo');
349+
assert.equal(array[0], 'foo');
350+
351+
remove();
352+
assert.equal(array.length, 0);
353+
});
345354
});
346355

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,12 @@ class UntitledTextEditorInputFactory implements IEditorInputFactory {
118118
) { }
119119

120120
canSerialize(editorInput: EditorInput): boolean {
121-
return this.filesConfigurationService.isHotExitEnabled;
121+
return this.filesConfigurationService.isHotExitEnabled && !editorInput.isDisposed();
122122
}
123123

124124
serialize(editorInput: EditorInput): string | undefined {
125-
if (!this.filesConfigurationService.isHotExitEnabled) {
126-
return undefined; // never restore untitled unless hot exit is enabled
125+
if (!this.filesConfigurationService.isHotExitEnabled || editorInput.isDisposed()) {
126+
return undefined;
127127
}
128128

129129
const untitledTextEditorInput = <UntitledTextEditorInput>editorInput;

src/vs/workbench/common/editor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ export interface IEditorInputFactory {
235235
* Returns a string representation of the provided editor input that contains enough information
236236
* to deserialize back to the original editor input from the deserialize() method.
237237
*/
238-
serialize(editorInput: EditorInput): string | undefined;
238+
serialize(editorInput: IEditorInput): string | undefined;
239239

240240
/**
241241
* Returns an editor input from the provided serialized form of the editor input. This form matches

src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,9 @@ workbenchContributionsRegistry.registerWorkbenchContribution(SearchEditorContrib
109109
type SerializedSearchEditor = { modelUri: string, dirty: boolean, config: SearchConfiguration, name: string, matchRanges: Range[], backingUri: string };
110110
class SearchEditorInputFactory implements IEditorInputFactory {
111111

112-
canSerialize() { return true; }
112+
canSerialize(input: SearchEditorInput) {
113+
return !input.isDisposed();
114+
}
113115

114116
serialize(input: SearchEditorInput) {
115117
let modelUri = undefined;

src/vs/workbench/services/history/browser/history.ts

Lines changed: 108 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { URI, UriComponents } from 'vs/base/common/uri';
77
import { IEditor } from 'vs/editor/common/editorCommon';
88
import { ITextEditorOptions, IResourceEditorInput, TextEditorSelectionRevealType, IEditorOptions } from 'vs/platform/editor/common/editor';
9-
import { IEditorInput, IEditorPane, Extensions as EditorExtensions, EditorInput, IEditorCloseEvent, IEditorInputFactoryRegistry, toResource, IEditorIdentifier, GroupIdentifier, EditorsOrder } from 'vs/workbench/common/editor';
9+
import { IEditorInput, IEditorPane, Extensions as EditorExtensions, EditorInput, IEditorCloseEvent, IEditorInputFactoryRegistry, toResource, IEditorIdentifier, GroupIdentifier, EditorsOrder, SideBySideEditor } from 'vs/workbench/common/editor';
1010
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
1111
import { IHistoryService } from 'vs/workbench/services/history/common/history';
1212
import { FileChangesEvent, IFileService, FileChangeType } from 'vs/platform/files/common/files';
@@ -17,21 +17,20 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag
1717
import { Registry } from 'vs/platform/registry/common/platform';
1818
import { Event } from 'vs/base/common/event';
1919
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
20-
import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
20+
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
2121
import { getCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser';
2222
import { createResourceExcludeMatcher } from 'vs/workbench/services/search/common/search';
2323
import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
2424
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
2525
import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
2626
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
2727
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
28-
import { coalesce } from 'vs/base/common/arrays';
28+
import { coalesce, remove } from 'vs/base/common/arrays';
2929
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
3030
import { withNullAsUndefined } from 'vs/base/common/types';
3131
import { addDisposableListener, EventType, EventHelper } from 'vs/base/browser/dom';
3232
import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
3333
import { Schemas } from 'vs/base/common/network';
34-
import { isEqual } from 'vs/base/common/resources';
3534
import { onUnexpectedError } from 'vs/base/common/errors';
3635

3736
/**
@@ -85,8 +84,10 @@ interface IStackEntry {
8584
selection?: Selection;
8685
}
8786

88-
interface IRecentlyClosedFile {
89-
resource: URI;
87+
interface IRecentlyClosedEditor {
88+
resource: URI | undefined;
89+
associatedResources: URI[];
90+
serialized: { typeId: string, value: string };
9091
index: number;
9192
sticky: boolean;
9293
}
@@ -101,6 +102,8 @@ export class HistoryService extends Disposable implements IHistoryService {
101102
private readonly editorHistoryListeners = new Map();
102103
private readonly editorStackListeners = new Map();
103104

105+
private readonly editorInputFactory = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories);
106+
104107
constructor(
105108
@IEditorService private readonly editorService: EditorServiceImpl,
106109
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
@@ -260,7 +263,7 @@ export class HistoryService extends Disposable implements IHistoryService {
260263
remove(arg1: IEditorInput | IResourceEditorInput | FileChangesEvent): void {
261264
this.removeFromHistory(arg1);
262265
this.removeFromNavigationStack(arg1);
263-
this.removeFromRecentlyClosedFiles(arg1);
266+
this.removeFromRecentlyClosedEditors(arg1);
264267
this.removeFromRecentlyOpened(arg1);
265268
}
266269

@@ -286,8 +289,8 @@ export class HistoryService extends Disposable implements IHistoryService {
286289
this.editorStackListeners.forEach(listeners => dispose(listeners));
287290
this.editorStackListeners.clear();
288291

289-
// Closed files
290-
this.recentlyClosedFiles = [];
292+
// Recently closed editors
293+
this.recentlyClosedEditors = [];
291294

292295
// Context Keys
293296
this.updateContextKeys();
@@ -602,88 +605,120 @@ export class HistoryService extends Disposable implements IHistoryService {
602605

603606
//#endregion
604607

605-
//#region Recently Closed Files
608+
//#region Recently Closed Editors
606609

607610
private static readonly MAX_RECENTLY_CLOSED_EDITORS = 20;
608611

609-
private recentlyClosedFiles: IRecentlyClosedFile[] = [];
612+
private recentlyClosedEditors: IRecentlyClosedEditor[] = [];
610613

611614
private onEditorClosed(event: IEditorCloseEvent): void {
615+
const { editor, replaced } = event;
616+
if (replaced) {
617+
return; // ignore if editor was replaced
618+
}
619+
620+
const factory = this.editorInputFactory.getEditorInputFactory(editor.getTypeId());
621+
if (!factory || !factory.canSerialize(editor)) {
622+
return; // we need a factory from this point that can serialize this editor
623+
}
612624

613-
// Track closing of editor to support to reopen closed editors (unless editor was replaced)
614-
if (!event.replaced) {
615-
const resource = event.editor ? event.editor.resource : undefined;
616-
const supportsReopen = resource && this.fileService.canHandleResource(resource); // we only support file'ish things to reopen
617-
if (resource && supportsReopen) {
625+
const serialized = factory.serialize(editor);
626+
if (typeof serialized !== 'string') {
627+
return; // we need something to deserialize from
628+
}
618629

619-
// Remove all inputs matching and add as last recently closed
620-
this.removeFromRecentlyClosedFiles(event.editor);
621-
this.recentlyClosedFiles.push({ resource, index: event.index, sticky: event.sticky });
630+
const associatedResources: URI[] = [];
631+
const editorResource = toResource(editor, { supportSideBySide: SideBySideEditor.BOTH });
632+
if (URI.isUri(editorResource)) {
633+
associatedResources.push(editorResource);
634+
} else if (editorResource) {
635+
associatedResources.push(...coalesce([editorResource.master, editorResource.detail]));
636+
}
622637

623-
// Bounding
624-
if (this.recentlyClosedFiles.length > HistoryService.MAX_RECENTLY_CLOSED_EDITORS) {
625-
this.recentlyClosedFiles.shift();
626-
}
638+
// Remove from list of recently closed before...
639+
this.removeFromRecentlyClosedEditors(editor);
627640

628-
// Context
629-
this.canReopenClosedEditorContextKey.set(true);
630-
}
641+
// ...adding it as last recently closed
642+
this.recentlyClosedEditors.push({
643+
resource: editor.resource,
644+
associatedResources,
645+
serialized: { typeId: editor.getTypeId(), value: serialized },
646+
index: event.index,
647+
sticky: event.sticky
648+
});
649+
650+
// Bounding
651+
if (this.recentlyClosedEditors.length > HistoryService.MAX_RECENTLY_CLOSED_EDITORS) {
652+
this.recentlyClosedEditors.shift();
631653
}
654+
655+
// Context
656+
this.canReopenClosedEditorContextKey.set(true);
632657
}
633658

634659
reopenLastClosedEditor(): void {
635-
let lastClosedFile = this.recentlyClosedFiles.pop();
636-
while (lastClosedFile && this.containsRecentlyClosedFile(this.editorGroupService.activeGroup, lastClosedFile)) {
637-
lastClosedFile = this.recentlyClosedFiles.pop(); // pop until we find a file that is not opened
638-
}
639-
640-
if (lastClosedFile) {
641-
(async () => {
642-
let options: IEditorOptions;
643-
if (lastClosedFile.sticky) {
644-
// Sticky: in case the target index is outside of the range of
645-
// sticky editors, we make sure to not provide the index as
646-
// option. Otherwise the index will cause the sticky flag to
647-
// be ignored.
648-
if (!this.editorGroupService.activeGroup.isSticky(lastClosedFile.index)) {
649-
options = { pinned: true, sticky: true };
650-
} else {
651-
options = { pinned: true, sticky: true, index: lastClosedFile.index };
652-
}
653-
} else {
654-
options = { pinned: true, index: lastClosedFile.index };
655-
}
656-
657-
const editor = await this.editorService.openEditor({ resource: lastClosedFile.resource, options });
658660

659-
// Fix for https://github.com/Microsoft/vscode/issues/67882
660-
// If opening of the editor fails, make sure to try the next one
661-
// but make sure to remove this one from the list to prevent
662-
// endless loops.
663-
if (!editor) {
664-
this.recentlyClosedFiles.pop();
665-
this.reopenLastClosedEditor();
666-
}
667-
})();
661+
// Open editor if we have one
662+
const lastClosedEditor = this.recentlyClosedEditors.pop();
663+
if (lastClosedEditor) {
664+
this.doReopenLastClosedEditor(lastClosedEditor);
668665
}
669666

670-
// Context
671-
this.canReopenClosedEditorContextKey.set(this.recentlyClosedFiles.length > 0);
667+
// Update context
668+
this.canReopenClosedEditorContextKey.set(this.recentlyClosedEditors.length > 0);
672669
}
673670

674-
private containsRecentlyClosedFile(group: IEditorGroup, recentlyClosedEditor: IRecentlyClosedFile): boolean {
675-
for (const editor of group.editors) {
676-
if (isEqual(editor.resource, recentlyClosedEditor.resource)) {
677-
return true;
671+
private async doReopenLastClosedEditor(lastClosedEditor: IRecentlyClosedEditor): Promise<void> {
672+
673+
// Determine editor options
674+
let options: IEditorOptions;
675+
if (lastClosedEditor.sticky) {
676+
// Sticky: in case the target index is outside of the range of
677+
// sticky editors, we make sure to not provide the index as
678+
// option. Otherwise the index will cause the sticky flag to
679+
// be ignored.
680+
if (!this.editorGroupService.activeGroup.isSticky(lastClosedEditor.index)) {
681+
options = { pinned: true, sticky: true, ignoreError: true };
682+
} else {
683+
options = { pinned: true, sticky: true, index: lastClosedEditor.index, ignoreError: true };
678684
}
685+
} else {
686+
options = { pinned: true, index: lastClosedEditor.index, ignoreError: true };
679687
}
680688

681-
return false;
689+
// Deserialize and open editor unless already opened
690+
const restoredEditor = this.editorInputFactory.getEditorInputFactory(lastClosedEditor.serialized.typeId)?.deserialize(this.instantiationService, lastClosedEditor.serialized.value);
691+
let editorPane: IEditorPane | undefined = undefined;
692+
if (restoredEditor && !this.editorGroupService.activeGroup.isOpened(restoredEditor)) {
693+
editorPane = await this.editorService.openEditor(restoredEditor, options);
694+
}
695+
696+
// If no editor was opened, try with the next one
697+
if (!editorPane) {
698+
// Fix for https://github.com/Microsoft/vscode/issues/67882
699+
// If opening of the editor fails, make sure to try the next one
700+
// but make sure to remove this one from the list to prevent
701+
// endless loops.
702+
remove(this.recentlyClosedEditors, lastClosedEditor);
703+
this.reopenLastClosedEditor();
704+
}
682705
}
683706

684-
private removeFromRecentlyClosedFiles(arg1: IEditorInput | IResourceEditorInput | FileChangesEvent): void {
685-
this.recentlyClosedFiles = this.recentlyClosedFiles.filter(e => !this.matchesFile(e.resource, arg1));
686-
this.canReopenClosedEditorContextKey.set(this.recentlyClosedFiles.length > 0);
707+
private removeFromRecentlyClosedEditors(arg1: IEditorInput | IResourceEditorInput | FileChangesEvent): void {
708+
this.recentlyClosedEditors = this.recentlyClosedEditors.filter(recentlyClosedEditor => {
709+
if (recentlyClosedEditor.resource && this.matchesFile(recentlyClosedEditor.resource, arg1)) {
710+
return false; // editor matches directly
711+
}
712+
713+
if (recentlyClosedEditor.associatedResources.some(associatedResource => this.matchesFile(associatedResource, arg1))) {
714+
return false; // an associated resource matches
715+
}
716+
717+
return true;
718+
});
719+
720+
// Update context
721+
this.canReopenClosedEditorContextKey.set(this.recentlyClosedEditors.length > 0);
687722
}
688723

689724
//#endregion
@@ -721,7 +756,7 @@ export class HistoryService extends Disposable implements IHistoryService {
721756
this.canNavigateBackContextKey.set(this.navigationStack.length > 0 && this.navigationStackIndex > 0);
722757
this.canNavigateForwardContextKey.set(this.navigationStack.length > 0 && this.navigationStackIndex < this.navigationStack.length - 1);
723758
this.canNavigateToLastEditLocationContextKey.set(!!this.lastEditLocation);
724-
this.canReopenClosedEditorContextKey.set(this.recentlyClosedFiles.length > 0);
759+
this.canReopenClosedEditorContextKey.set(this.recentlyClosedEditors.length > 0);
725760
}
726761

727762
//#endregion
@@ -833,18 +868,16 @@ export class HistoryService extends Disposable implements IHistoryService {
833868
}
834869
}
835870

836-
const registry = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories);
837-
838871
return coalesce(entries.map(entry => {
839872
try {
840-
return this.safeLoadHistoryEntry(registry, entry);
873+
return this.safeLoadHistoryEntry(entry);
841874
} catch (error) {
842875
return undefined; // https://github.com/Microsoft/vscode/issues/60960
843876
}
844877
}));
845878
}
846879

847-
private safeLoadHistoryEntry(registry: IEditorInputFactoryRegistry, entry: ISerializedEditorHistoryEntry): IEditorInput | IResourceEditorInput | undefined {
880+
private safeLoadHistoryEntry(entry: ISerializedEditorHistoryEntry): IEditorInput | IResourceEditorInput | undefined {
848881
const serializedEditorHistoryEntry = entry;
849882

850883
// File resource: via URI.revive()
@@ -855,7 +888,7 @@ export class HistoryService extends Disposable implements IHistoryService {
855888
// Editor input: via factory
856889
const { editorInputJSON } = serializedEditorHistoryEntry;
857890
if (editorInputJSON?.deserialized) {
858-
const factory = registry.getEditorInputFactory(editorInputJSON.typeId);
891+
const factory = this.editorInputFactory.getEditorInputFactory(editorInputJSON.typeId);
859892
if (factory) {
860893
const input = factory.deserialize(this.instantiationService, editorInputJSON.deserialized);
861894
if (input) {
@@ -874,13 +907,11 @@ export class HistoryService extends Disposable implements IHistoryService {
874907
return; // nothing to save because history was not used
875908
}
876909

877-
const registry = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories);
878-
879910
const entries: ISerializedEditorHistoryEntry[] = coalesce(this.history.map((input): ISerializedEditorHistoryEntry | undefined => {
880911

881912
// Editor input: try via factory
882913
if (input instanceof EditorInput) {
883-
const factory = registry.getEditorInputFactory(input.getTypeId());
914+
const factory = this.editorInputFactory.getEditorInputFactory(input.getTypeId());
884915
if (factory) {
885916
const deserialized = factory.serialize(input);
886917
if (deserialized) {

0 commit comments

Comments
 (0)