move recently closed editors to history service

This commit is contained in:
Benjamin Pasero 2016-06-14 12:00:41 +02:00
parent d5f06aec6b
commit 0d5d1d4b1c
8 changed files with 96 additions and 113 deletions

View file

@ -889,7 +889,7 @@ export class ReopenClosedEditorAction extends Action {
constructor(
id: string,
label: string,
@IPartService private partService: IPartService,
@IHistoryService private historyService: IHistoryService,
@IEditorGroupService private editorGroupService: IEditorGroupService,
@IWorkbenchEditorService private editorService: IWorkbenchEditorService
) {
@ -900,9 +900,9 @@ export class ReopenClosedEditorAction extends Action {
const stacks = this.editorGroupService.getStacksModel();
// Find an editor that was closed and is currently not opened in the group
let lastClosedEditor = stacks.popLastClosedEditor();
let lastClosedEditor = this.historyService.popLastClosedEditor();
while (lastClosedEditor && stacks.activeGroup && stacks.activeGroup.indexOf(lastClosedEditor) >= 0) {
lastClosedEditor = stacks.popLastClosedEditor();
lastClosedEditor = this.historyService.popLastClosedEditor();
}
if (lastClosedEditor) {
@ -1082,9 +1082,6 @@ export class ClearEditorHistoryAction extends Action {
// Editor history
this.historyService.clear();
// Recently closed editors
this.editorGroupService.getStacksModel().clearLastClosedEditors();
return TPromise.as(true);
}
}

View file

@ -451,7 +451,6 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService
// Recover by closing the active editor (if the input is still the active one)
if (group.activeEditor === input) {
this.doCloseActiveEditor(group);
this.stacks.popLastClosedEditor(); // remove from "last closed" history because this input is failing
}
}

View file

@ -610,6 +610,7 @@ export interface IStacksModelChangeEvent {
export interface IEditorStacksModel {
onModelChanged: Event<IStacksModelChangeEvent>;
onEditorClosed: Event<IGroupEvent>;
groups: IEditorGroup[];
activeGroup: IEditorGroup;
@ -623,9 +624,6 @@ export interface IEditorStacksModel {
next(): IEditorIdentifier;
previous(): IEditorIdentifier;
popLastClosedEditor(): IEditorInput;
clearLastClosedEditors(): void;
isOpen(editor: IEditorInput): boolean;
isOpen(resource: URI): boolean;
@ -661,6 +659,11 @@ export interface IEditorContext extends IEditorIdentifier {
event: any;
}
export interface IGroupEvent {
editor: IEditorInput;
pinned: boolean;
}
export type GroupIdentifier = number;
export const EditorOpenPositioning = {

View file

@ -6,7 +6,7 @@
'use strict';
import Event, {Emitter} from 'vs/base/common/event';
import {EditorInput, getUntitledOrFileResource, IEditorStacksModel, IEditorGroup, IEditorIdentifier, GroupIdentifier, IStacksModelChangeEvent, IWorkbenchEditorConfiguration, EditorOpenPositioning} from 'vs/workbench/common/editor';
import {EditorInput, getUntitledOrFileResource, IEditorStacksModel, IEditorGroup, IEditorIdentifier, IGroupEvent, GroupIdentifier, IStacksModelChangeEvent, IWorkbenchEditorConfiguration, EditorOpenPositioning} from 'vs/workbench/common/editor';
import URI from 'vs/base/common/uri';
import {IStorageService, StorageScope} from 'vs/platform/storage/common/storage';
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
@ -22,9 +22,8 @@ import {DiffEditorInput} from 'vs/workbench/common/editor/diffEditorInput';
// TODO@Ben currently only files and untitled editors are tracked with their resources in the stacks model
// Once the resource is a base concept of all editor inputs, every resource should be tracked for any editor
export interface GroupEvent {
export interface GroupEvent extends IGroupEvent {
editor: EditorInput;
pinned: boolean;
}
export interface EditorIdentifier extends IEditorIdentifier {
@ -624,13 +623,11 @@ export class EditorGroup implements IEditorGroup {
interface ISerializedEditorStacksModel {
groups: ISerializedEditorGroup[];
active: number;
lastClosed: ISerializedEditorInput[];
}
export class EditorStacksModel implements IEditorStacksModel {
private static STORAGE_KEY = 'editorStacks.model';
private static MAX_RECENTLY_CLOSED_EDITORS = 20;
private toDispose: IDisposable[];
private loaded: boolean;
@ -639,8 +636,6 @@ export class EditorStacksModel implements IEditorStacksModel {
private _activeGroup: EditorGroup;
private groupToIdentifier: { [id: number]: EditorGroup };
private recentlyClosedEditors: ISerializedEditorInput[];
private _onGroupOpened: Emitter<EditorGroup>;
private _onGroupClosed: Emitter<EditorGroup>;
private _onGroupMoved: Emitter<EditorGroup>;
@ -649,6 +644,7 @@ export class EditorStacksModel implements IEditorStacksModel {
private _onGroupRenamed: Emitter<EditorGroup>;
private _onEditorDisposed: Emitter<EditorIdentifier>;
private _onEditorDirty: Emitter<EditorIdentifier>;
private _onEditorClosed: Emitter<GroupEvent>;
private _onModelChanged: Emitter<IStacksModelChangeEvent>;
constructor(
@ -662,8 +658,6 @@ export class EditorStacksModel implements IEditorStacksModel {
this._groups = [];
this.groupToIdentifier = Object.create(null);
this.recentlyClosedEditors = [];
this._onGroupOpened = new Emitter<EditorGroup>();
this._onGroupClosed = new Emitter<EditorGroup>();
this._onGroupActivated = new Emitter<EditorGroup>();
@ -673,8 +667,9 @@ export class EditorStacksModel implements IEditorStacksModel {
this._onModelChanged = new Emitter<IStacksModelChangeEvent>();
this._onEditorDisposed = new Emitter<EditorIdentifier>();
this._onEditorDirty = new Emitter<EditorIdentifier>();
this._onEditorClosed = new Emitter<GroupEvent>();
this.toDispose.push(this._onGroupOpened, this._onGroupClosed, this._onGroupActivated, this._onGroupDeactivated, this._onGroupMoved, this._onGroupRenamed, this._onModelChanged, this._onEditorDisposed, this._onEditorDirty);
this.toDispose.push(this._onGroupOpened, this._onGroupClosed, this._onGroupActivated, this._onGroupDeactivated, this._onGroupMoved, this._onGroupRenamed, this._onModelChanged, this._onEditorDisposed, this._onEditorDirty, this._onEditorClosed);
this.registerListeners();
}
@ -719,6 +714,10 @@ export class EditorStacksModel implements IEditorStacksModel {
return this._onEditorDirty.event;
}
public get onEditorClosed(): Event<GroupEvent> {
return this._onEditorClosed.event;
}
public get groups(): EditorGroup[] {
this.ensureLoaded();
@ -957,8 +956,7 @@ export class EditorStacksModel implements IEditorStacksModel {
return {
groups: serializableGroups,
active: serializableActiveIndex,
lastClosed: this.recentlyClosedEditors
active: serializableActiveIndex
};
}
@ -994,7 +992,6 @@ export class EditorStacksModel implements IEditorStacksModel {
this._groups = serialized.groups.map(s => this.doCreateGroup(s));
this._activeGroup = this._groups[serialized.active];
this.recentlyClosedEditors = serialized.lastClosed || [];
} else {
this.migrate();
}
@ -1036,7 +1033,6 @@ export class EditorStacksModel implements IEditorStacksModel {
this._groups = [];
this._activeGroup = void 0;
this.groupToIdentifier = Object.create(null);
this.recentlyClosedEditors = [];
}
}
}
@ -1082,7 +1078,10 @@ export class EditorStacksModel implements IEditorStacksModel {
const unbind: IDisposable[] = [];
unbind.push(group.onEditorsStructureChanged(editor => this._onModelChanged.fire({ group, editor, structural: true })));
unbind.push(group.onEditorStateChanged(editor => this._onModelChanged.fire({ group, editor })));
unbind.push(group.onEditorClosed(e => this.onEditorClosed(e)));
unbind.push(group.onEditorClosed(event => {
this.handleOnEditorClosed(event);
this._onEditorClosed.fire(event);
}));
unbind.push(group.onEditorDisposed(editor => this._onEditorDisposed.fire({ editor, group })));
unbind.push(group.onEditorDirty(editor => this._onEditorDirty.fire({ editor, group })));
unbind.push(this.onGroupClosed(g => {
@ -1094,26 +1093,7 @@ export class EditorStacksModel implements IEditorStacksModel {
return group;
}
public popLastClosedEditor(): EditorInput {
this.ensureLoaded();
const registry = Registry.as<IEditorRegistry>(Extensions.Editors);
let serializedEditor = this.recentlyClosedEditors.pop();
if (serializedEditor) {
return registry.getEditorInputFactory(serializedEditor.id).deserialize(this.instantiationService, serializedEditor.value);
}
return null;
}
public clearLastClosedEditors(): void {
this.ensureLoaded();
this.recentlyClosedEditors = [];
}
private onEditorClosed(event: GroupEvent): void {
private handleOnEditorClosed(event: GroupEvent): void {
const editor = event.editor;
// Close the editor when it is no longer open in any group
@ -1129,22 +1109,6 @@ export class EditorStacksModel implements IEditorStacksModel {
});
}
}
// Track closing of pinned editor to support to reopen closed editors
if (event.pinned) {
const registry = Registry.as<IEditorRegistry>(Extensions.Editors);
const factory = registry.getEditorInputFactory(editor.getTypeId());
if (factory) {
let value = factory.serialize(editor);
if (typeof value === 'string') {
this.recentlyClosedEditors.push({ id: editor.getTypeId(), value });
if (this.recentlyClosedEditors.length > EditorStacksModel.MAX_RECENTLY_CLOSED_EDITORS) {
this.recentlyClosedEditors = this.recentlyClosedEditors.slice(this.recentlyClosedEditors.length - EditorStacksModel.MAX_RECENTLY_CLOSED_EDITORS); // upper bound of recently closed
}
}
}
}
}
public isOpen(resource: URI): boolean;

View file

@ -10,7 +10,7 @@ import platform = require('vs/base/common/platform');
import nls = require('vs/nls');
import {EventType} from 'vs/base/common/events';
import {IEditor as IBaseEditor} from 'vs/platform/editor/common/editor';
import {TextEditorOptions, EditorInput} from 'vs/workbench/common/editor';
import {TextEditorOptions, EditorInput, IGroupEvent} from 'vs/workbench/common/editor';
import {BaseTextEditor} from 'vs/workbench/browser/parts/editor/textEditor';
import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService';
import {IHistoryService} from 'vs/workbench/services/history/common/history';
@ -218,6 +218,7 @@ export class HistoryService extends BaseHistoryService implements IHistoryServic
private static STORAGE_KEY = 'history.entries';
private static MAX_HISTORY_ITEMS = 200;
private static MAX_STACK_ITEMS = 20;
private static MAX_RECENTLY_CLOSED_EDITORS = 20;
private stack: IStackEntry[];
private index: number;
@ -225,6 +226,7 @@ export class HistoryService extends BaseHistoryService implements IHistoryServic
private currentFileEditorState: EditorState;
private history: IEditorInput[];
private recentlyClosed: IEditorInput[];
private loaded: boolean;
private registry: IEditorRegistry;
@ -241,6 +243,7 @@ export class HistoryService extends BaseHistoryService implements IHistoryServic
this.index = -1;
this.stack = [];
this.recentlyClosed = [];
this.loaded = false;
this.registry = Registry.as<IEditorRegistry>(Extensions.Editors);
@ -250,6 +253,35 @@ export class HistoryService extends BaseHistoryService implements IHistoryServic
private registerListeners(): void {
this.toUnbind.push(this.lifecycleService.onShutdown(() => this.save()));
this.toUnbind.push(this.editorGroupService.onEditorOpenFail(editor => this.remove(editor)));
this.toUnbind.push(this.editorGroupService.getStacksModel().onEditorClosed(event => this.onEditorClosed(event)));
}
private onEditorClosed(event: IGroupEvent): void {
// Track closing of pinned editor to support to reopen closed editors
if (event.pinned) {
const editor = event.editor;
// Remove all inputs matching and add as last recently closed
this.removeFromRecentlyClosed(editor);
this.recentlyClosed.push(editor);
// Bounding
if (this.recentlyClosed.length > HistoryService.MAX_RECENTLY_CLOSED_EDITORS) {
this.recentlyClosed = this.recentlyClosed.slice(this.recentlyClosed.length - HistoryService.MAX_RECENTLY_CLOSED_EDITORS); // upper bound of recently closed
}
// Restore on dispose
editor.addOneTimeDisposableListener(EventType.DISPOSE, () => {
this.restoreInRecentlyClosed(editor);
});
}
}
public popLastClosedEditor(): IEditorInput {
this.ensureLoaded();
return this.recentlyClosed.pop();
}
public forward(): void {
@ -272,6 +304,7 @@ export class HistoryService extends BaseHistoryService implements IHistoryServic
this.index = -1;
this.stack.splice(0);
this.history = [];
this.recentlyClosed = [];
}
private navigate(): void {
@ -358,6 +391,7 @@ export class HistoryService extends BaseHistoryService implements IHistoryServic
public remove(input: IEditorInput): void {
this.removeFromHistory(input);
this.removeFromStack(input);
setTimeout(() => this.removeFromRecentlyClosed(input)); // race condition with editor close and dispose
}
private removeFromHistory(input: IEditorInput, index?: number): void {
@ -495,6 +529,26 @@ export class HistoryService extends BaseHistoryService implements IHistoryServic
});
}
private restoreInRecentlyClosed(input: IEditorInput): void {
let restoredInput: EditorInput;
let restored = false;
this.recentlyClosed.forEach((e, i) => {
if (e.matches(input)) {
if (!restored) {
restoredInput = this.restoreInput(input);
restored = true;
}
if (restoredInput) {
this.recentlyClosed[i] = restoredInput;
} else {
this.stack.splice(i, 1);
}
}
});
}
private restoreInput(input: IEditorInput): EditorInput {
if (input instanceof EditorInput) {
const factory = this.registry.getEditorInputFactory(input.getTypeId());
@ -520,6 +574,14 @@ export class HistoryService extends BaseHistoryService implements IHistoryServic
});
}
private removeFromRecentlyClosed(input: IEditorInput): void {
this.recentlyClosed.forEach((e, i) => {
if (e.matches(input)) {
this.recentlyClosed.splice(i, 1);
}
});
}
public getHistory(): IEditorInput[] {
this.ensureLoaded();

View file

@ -13,6 +13,11 @@ export interface IHistoryService {
serviceId: ServiceIdentifier<any>;
/**
* Removes and returns the last closed editor if any.
*/
popLastClosedEditor(): IEditorInput;
/**
* Navigate forwards in history.
*/

View file

@ -591,57 +591,6 @@ suite('Editor Stacks Model', () => {
assert.equal(input4, group.getEditors()[2]);
});
test('Stack - Multiple Editors - Recently Closed Tracking', function () {
let services = new ServiceCollection();
services.set(IStorageService, new TestStorageService());
services.set(IWorkspaceContextService, new TestContextService());
const lifecycle = new TestLifecycleService();
services.set(ILifecycleService, lifecycle);
const config = new TestConfigurationService();
config.setUserConfiguration('workbench', { editorOpenPositioning: 'right' });
services.set(IConfigurationService, config);
let inst = new InstantiationService(services);
(<IEditorRegistry>Registry.as(EditorExtensions.Editors)).setInstantiationService(inst);
let model: EditorStacksModel = inst.createInstance(EditorStacksModel);
const group1 = model.openGroup('group1');
const group2 = model.openGroup('group2');
const input1 = input();
const input2 = input();
const input3 = input();
group1.openEditor(input1, { pinned: false, active: true });
group1.openEditor(input2, { pinned: true, active: true });
group1.openEditor(input3, { pinned: true, active: true });
const input4 = input();
const input5 = input();
group2.openEditor(input4, { pinned: true, active: true });
group2.openEditor(input5, { pinned: true, active: true });
assert.ok(!model.popLastClosedEditor());
group1.closeEditor(input1);
assert.ok(!model.popLastClosedEditor()); // preview editors are not recorded
group1.closeEditor(input3);
assert.ok(input3.matches(model.popLastClosedEditor()));
group2.closeAllEditors();
assert.ok(input5.matches(model.popLastClosedEditor()));
assert.ok(input4.matches(model.popLastClosedEditor()));
assert.ok(!model.popLastClosedEditor());
});
test('Stack - Multiple Editors - Pinned and Active (DEFAULT_OPEN_EDITOR_DIRECTION = Direction.LEFT)', function () {
let services = new ServiceCollection();
services.set(IStorageService, new TestStorageService());

View file

@ -66,6 +66,10 @@ export class TestHistoryService implements IHistoryService {
}
public popLastClosedEditor(): IEditorInput {
return null;
}
public getHistory(): IEditorInput[] {
return [];
}