Skip to content

Commit

Permalink
history - use editor input factory to support to reopen any editor th…
Browse files Browse the repository at this point in the history
…at can be serialised
  • Loading branch information
bpasero committed Jun 9, 2020
1 parent 516bb5f commit 1eac96d
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 88 deletions.
21 changes: 15 additions & 6 deletions src/vs/base/common/arrays.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,12 +489,21 @@ export function index<T, R>(array: ReadonlyArray<T>, indexer: (t: T) => string,
export function insert<T>(array: T[], element: T): () => void {
array.push(element);

return () => {
const index = array.indexOf(element);
if (index > -1) {
array.splice(index, 1);
}
};
return () => remove(array, element);
}

/**
* Removes an element from an array if it can be found.
*/
export function remove<T>(array: T[], element: T): T | undefined {
const index = array.indexOf(element);
if (index > -1) {
array.splice(index, 1);

return element;
}

return undefined;
}

/**
Expand Down
9 changes: 9 additions & 0 deletions src/vs/base/test/common/arrays.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,5 +342,14 @@ suite('Arrays', () => {
arrays.coalesceInPlace(sparse);
assert.equal(sparse.length, 5);
});

test('insert, remove', function () {
const array: string[] = [];
const remove = arrays.insert(array, 'foo');
assert.equal(array[0], 'foo');

remove();
assert.equal(array.length, 0);
});
});

6 changes: 3 additions & 3 deletions src/vs/workbench/browser/parts/editor/editor.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,12 @@ class UntitledTextEditorInputFactory implements IEditorInputFactory {
) { }

canSerialize(editorInput: EditorInput): boolean {
return this.filesConfigurationService.isHotExitEnabled;
return this.filesConfigurationService.isHotExitEnabled && !editorInput.isDisposed();
}

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

const untitledTextEditorInput = <UntitledTextEditorInput>editorInput;
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/common/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ export interface IEditorInputFactory {
* Returns a string representation of the provided editor input that contains enough information
* to deserialize back to the original editor input from the deserialize() method.
*/
serialize(editorInput: EditorInput): string | undefined;
serialize(editorInput: IEditorInput): string | undefined;

/**
* Returns an editor input from the provided serialized form of the editor input. This form matches
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ workbenchContributionsRegistry.registerWorkbenchContribution(SearchEditorContrib
type SerializedSearchEditor = { modelUri: string, dirty: boolean, config: SearchConfiguration, name: string, matchRanges: Range[], backingUri: string };
class SearchEditorInputFactory implements IEditorInputFactory {

canSerialize() { return true; }
canSerialize(input: SearchEditorInput) {
return !input.isDisposed();
}

serialize(input: SearchEditorInput) {
let modelUri = undefined;
Expand Down
185 changes: 108 additions & 77 deletions src/vs/workbench/services/history/browser/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { URI, UriComponents } from 'vs/base/common/uri';
import { IEditor } from 'vs/editor/common/editorCommon';
import { ITextEditorOptions, IResourceEditorInput, TextEditorSelectionRevealType, IEditorOptions } from 'vs/platform/editor/common/editor';
import { IEditorInput, IEditorPane, Extensions as EditorExtensions, EditorInput, IEditorCloseEvent, IEditorInputFactoryRegistry, toResource, IEditorIdentifier, GroupIdentifier, EditorsOrder } from 'vs/workbench/common/editor';
import { IEditorInput, IEditorPane, Extensions as EditorExtensions, EditorInput, IEditorCloseEvent, IEditorInputFactoryRegistry, toResource, IEditorIdentifier, GroupIdentifier, EditorsOrder, SideBySideEditor } from 'vs/workbench/common/editor';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { FileChangesEvent, IFileService, FileChangeType } from 'vs/platform/files/common/files';
Expand All @@ -17,21 +17,20 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag
import { Registry } from 'vs/platform/registry/common/platform';
import { Event } from 'vs/base/common/event';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { getCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { createResourceExcludeMatcher } from 'vs/workbench/services/search/common/search';
import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { coalesce } from 'vs/base/common/arrays';
import { coalesce, remove } from 'vs/base/common/arrays';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { withNullAsUndefined } from 'vs/base/common/types';
import { addDisposableListener, EventType, EventHelper } from 'vs/base/browser/dom';
import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
import { Schemas } from 'vs/base/common/network';
import { isEqual } from 'vs/base/common/resources';
import { onUnexpectedError } from 'vs/base/common/errors';

/**
Expand Down Expand Up @@ -85,8 +84,10 @@ interface IStackEntry {
selection?: Selection;
}

interface IRecentlyClosedFile {
resource: URI;
interface IRecentlyClosedEditor {
resource: URI | undefined;
associatedResources: URI[];
serialized: { typeId: string, value: string };
index: number;
sticky: boolean;
}
Expand All @@ -101,6 +102,8 @@ export class HistoryService extends Disposable implements IHistoryService {
private readonly editorHistoryListeners = new Map();
private readonly editorStackListeners = new Map();

private readonly editorInputFactory = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories);

constructor(
@IEditorService private readonly editorService: EditorServiceImpl,
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
Expand Down Expand Up @@ -260,7 +263,7 @@ export class HistoryService extends Disposable implements IHistoryService {
remove(arg1: IEditorInput | IResourceEditorInput | FileChangesEvent): void {
this.removeFromHistory(arg1);
this.removeFromNavigationStack(arg1);
this.removeFromRecentlyClosedFiles(arg1);
this.removeFromRecentlyClosedEditors(arg1);
this.removeFromRecentlyOpened(arg1);
}

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

// Closed files
this.recentlyClosedFiles = [];
// Recently closed editors
this.recentlyClosedEditors = [];

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

//#endregion

//#region Recently Closed Files
//#region Recently Closed Editors

private static readonly MAX_RECENTLY_CLOSED_EDITORS = 20;

private recentlyClosedFiles: IRecentlyClosedFile[] = [];
private recentlyClosedEditors: IRecentlyClosedEditor[] = [];

private onEditorClosed(event: IEditorCloseEvent): void {
const { editor, replaced } = event;
if (replaced) {
return; // ignore if editor was replaced
}

const factory = this.editorInputFactory.getEditorInputFactory(editor.getTypeId());
if (!factory || !factory.canSerialize(editor)) {
return; // we need a factory from this point that can serialize this editor
}

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

// Remove all inputs matching and add as last recently closed
this.removeFromRecentlyClosedFiles(event.editor);
this.recentlyClosedFiles.push({ resource, index: event.index, sticky: event.sticky });
const associatedResources: URI[] = [];
const editorResource = toResource(editor, { supportSideBySide: SideBySideEditor.BOTH });
if (URI.isUri(editorResource)) {
associatedResources.push(editorResource);
} else if (editorResource) {
associatedResources.push(...coalesce([editorResource.master, editorResource.detail]));
}

// Bounding
if (this.recentlyClosedFiles.length > HistoryService.MAX_RECENTLY_CLOSED_EDITORS) {
this.recentlyClosedFiles.shift();
}
// Remove from list of recently closed before...
this.removeFromRecentlyClosedEditors(editor);

// Context
this.canReopenClosedEditorContextKey.set(true);
}
// ...adding it as last recently closed
this.recentlyClosedEditors.push({
resource: editor.resource,
associatedResources,
serialized: { typeId: editor.getTypeId(), value: serialized },
index: event.index,
sticky: event.sticky
});

// Bounding
if (this.recentlyClosedEditors.length > HistoryService.MAX_RECENTLY_CLOSED_EDITORS) {
this.recentlyClosedEditors.shift();
}

// Context
this.canReopenClosedEditorContextKey.set(true);
}

reopenLastClosedEditor(): void {
let lastClosedFile = this.recentlyClosedFiles.pop();
while (lastClosedFile && this.containsRecentlyClosedFile(this.editorGroupService.activeGroup, lastClosedFile)) {
lastClosedFile = this.recentlyClosedFiles.pop(); // pop until we find a file that is not opened
}

if (lastClosedFile) {
(async () => {
let options: IEditorOptions;
if (lastClosedFile.sticky) {
// Sticky: in case the target index is outside of the range of
// sticky editors, we make sure to not provide the index as
// option. Otherwise the index will cause the sticky flag to
// be ignored.
if (!this.editorGroupService.activeGroup.isSticky(lastClosedFile.index)) {
options = { pinned: true, sticky: true };
} else {
options = { pinned: true, sticky: true, index: lastClosedFile.index };
}
} else {
options = { pinned: true, index: lastClosedFile.index };
}

const editor = await this.editorService.openEditor({ resource: lastClosedFile.resource, options });

// Fix for https://github.com/Microsoft/vscode/issues/67882
// If opening of the editor fails, make sure to try the next one
// but make sure to remove this one from the list to prevent
// endless loops.
if (!editor) {
this.recentlyClosedFiles.pop();
this.reopenLastClosedEditor();
}
})();
// Open editor if we have one
const lastClosedEditor = this.recentlyClosedEditors.pop();
if (lastClosedEditor) {
this.doReopenLastClosedEditor(lastClosedEditor);
}

// Context
this.canReopenClosedEditorContextKey.set(this.recentlyClosedFiles.length > 0);
// Update context
this.canReopenClosedEditorContextKey.set(this.recentlyClosedEditors.length > 0);
}

private containsRecentlyClosedFile(group: IEditorGroup, recentlyClosedEditor: IRecentlyClosedFile): boolean {
for (const editor of group.editors) {
if (isEqual(editor.resource, recentlyClosedEditor.resource)) {
return true;
private async doReopenLastClosedEditor(lastClosedEditor: IRecentlyClosedEditor): Promise<void> {

// Determine editor options
let options: IEditorOptions;
if (lastClosedEditor.sticky) {
// Sticky: in case the target index is outside of the range of
// sticky editors, we make sure to not provide the index as
// option. Otherwise the index will cause the sticky flag to
// be ignored.
if (!this.editorGroupService.activeGroup.isSticky(lastClosedEditor.index)) {
options = { pinned: true, sticky: true, ignoreError: true };
} else {
options = { pinned: true, sticky: true, index: lastClosedEditor.index, ignoreError: true };
}
} else {
options = { pinned: true, index: lastClosedEditor.index, ignoreError: true };
}

return false;
// Deserialize and open editor unless already opened
const restoredEditor = this.editorInputFactory.getEditorInputFactory(lastClosedEditor.serialized.typeId)?.deserialize(this.instantiationService, lastClosedEditor.serialized.value);
let editorPane: IEditorPane | undefined = undefined;
if (restoredEditor && !this.editorGroupService.activeGroup.isOpened(restoredEditor)) {
editorPane = await this.editorService.openEditor(restoredEditor, options);
}

// If no editor was opened, try with the next one
if (!editorPane) {
// Fix for https://github.com/Microsoft/vscode/issues/67882
// If opening of the editor fails, make sure to try the next one
// but make sure to remove this one from the list to prevent
// endless loops.
remove(this.recentlyClosedEditors, lastClosedEditor);
this.reopenLastClosedEditor();
}
}

private removeFromRecentlyClosedFiles(arg1: IEditorInput | IResourceEditorInput | FileChangesEvent): void {
this.recentlyClosedFiles = this.recentlyClosedFiles.filter(e => !this.matchesFile(e.resource, arg1));
this.canReopenClosedEditorContextKey.set(this.recentlyClosedFiles.length > 0);
private removeFromRecentlyClosedEditors(arg1: IEditorInput | IResourceEditorInput | FileChangesEvent): void {
this.recentlyClosedEditors = this.recentlyClosedEditors.filter(recentlyClosedEditor => {
if (recentlyClosedEditor.resource && this.matchesFile(recentlyClosedEditor.resource, arg1)) {
return false; // editor matches directly
}

if (recentlyClosedEditor.associatedResources.some(associatedResource => this.matchesFile(associatedResource, arg1))) {
return false; // an associated resource matches
}

return true;
});

// Update context
this.canReopenClosedEditorContextKey.set(this.recentlyClosedEditors.length > 0);
}

//#endregion
Expand Down Expand Up @@ -721,7 +756,7 @@ export class HistoryService extends Disposable implements IHistoryService {
this.canNavigateBackContextKey.set(this.navigationStack.length > 0 && this.navigationStackIndex > 0);
this.canNavigateForwardContextKey.set(this.navigationStack.length > 0 && this.navigationStackIndex < this.navigationStack.length - 1);
this.canNavigateToLastEditLocationContextKey.set(!!this.lastEditLocation);
this.canReopenClosedEditorContextKey.set(this.recentlyClosedFiles.length > 0);
this.canReopenClosedEditorContextKey.set(this.recentlyClosedEditors.length > 0);
}

//#endregion
Expand Down Expand Up @@ -833,18 +868,16 @@ export class HistoryService extends Disposable implements IHistoryService {
}
}

const registry = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories);

return coalesce(entries.map(entry => {
try {
return this.safeLoadHistoryEntry(registry, entry);
return this.safeLoadHistoryEntry(entry);
} catch (error) {
return undefined; // https://github.com/Microsoft/vscode/issues/60960
}
}));
}

private safeLoadHistoryEntry(registry: IEditorInputFactoryRegistry, entry: ISerializedEditorHistoryEntry): IEditorInput | IResourceEditorInput | undefined {
private safeLoadHistoryEntry(entry: ISerializedEditorHistoryEntry): IEditorInput | IResourceEditorInput | undefined {
const serializedEditorHistoryEntry = entry;

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

const registry = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories);

const entries: ISerializedEditorHistoryEntry[] = coalesce(this.history.map((input): ISerializedEditorHistoryEntry | undefined => {

// Editor input: try via factory
if (input instanceof EditorInput) {
const factory = registry.getEditorInputFactory(input.getTypeId());
const factory = this.editorInputFactory.getEditorInputFactory(input.getTypeId());
if (factory) {
const deserialized = factory.serialize(input);
if (deserialized) {
Expand Down

0 comments on commit 1eac96d

Please sign in to comment.