提交 d92afa06 编写于 作者: B Benjamin Pasero

editors - introduce and use EditorsObserver and use for MRU list

上级 c7da14d0
......@@ -151,4 +151,14 @@ export interface EditorServiceImpl extends IEditorService {
* Emitted when an editor failed to open.
*/
readonly onDidOpenEditorFail: Event<IEditorIdentifier>;
/**
* Emitted when the list of most recently active editors change.
*/
readonly onDidMostRecentlyActiveEditorsChange: Event<void>;
/**
* Access to the list of most recently active editors.
*/
readonly mostRecentlyActiveEditors: ReadonlyArray<IEditorIdentifier>;
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IEditorInput, IEditorInputFactoryRegistry, IEditorIdentifier, GroupIdentifier, Extensions } from 'vs/workbench/common/editor';
import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { Registry } from 'vs/platform/registry/common/platform';
import { Event, Emitter } from 'vs/base/common/event';
import { IEditorGroupsService, IEditorGroup, EditorsOrder, GroupChangeKind, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService';
import { coalesce } from 'vs/base/common/arrays';
import { LinkedMap, Touch } from 'vs/base/common/map';
interface ISerializedEditorsList {
entries: ISerializedEditorIdentifier[];
}
interface ISerializedEditorIdentifier {
groupId: GroupIdentifier;
index: number;
}
/**
* A observer of opened editors across all editor groups by most recently used.
* Rules:
* - the last editor in the list is the one most recently activated
* - the first editor in the list is the one that was activated the longest time ago
* - an editor that opens inactive will be placed behind the currently active editor
*/
export class EditorsObserver extends Disposable {
private static readonly STORAGE_KEY = 'editorsObserver.state';
private readonly keyMap = new Map<GroupIdentifier, Map<IEditorInput, IEditorIdentifier>>();
private readonly mostRecentEditorsMap = new LinkedMap<IEditorIdentifier, IEditorIdentifier>();
private readonly _onDidChange = this._register(new Emitter<void>());
readonly onDidChange = this._onDidChange.event;
get editors(): IEditorIdentifier[] {
return this.mostRecentEditorsMap.values();
}
constructor(
@IEditorGroupsService private editorGroupsService: IEditorGroupsService,
@IStorageService private readonly storageService: IStorageService
) {
super();
this.registerListeners();
}
private registerListeners(): void {
this._register(this.storageService.onWillSaveState(() => this.saveState()));
this._register(this.editorGroupsService.onDidAddGroup(group => this.onGroupAdded(group)));
this.editorGroupsService.whenRestored.then(() => this.loadState());
}
private onGroupAdded(group: IEditorGroup): void {
// Make sure to add any already existing editor
// of the new group into our list in LRU order
const groupEditorsMru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE);
for (let i = groupEditorsMru.length - 1; i >= 0; i--) {
this.addMostRecentEditor(group, groupEditorsMru[i], false /* is not active */);
}
// Make sure that active editor is put as first if group is active
if (this.editorGroupsService.activeGroup === group && group.activeEditor) {
this.addMostRecentEditor(group, group.activeEditor, true /* is active */);
}
// Group Listeners
this.registerGroupListeners(group);
}
private registerGroupListeners(group: IEditorGroup): void {
const groupDisposables = new DisposableStore();
groupDisposables.add(group.onDidGroupChange(e => {
switch (e.kind) {
// Group gets active: put active editor as most recent
case GroupChangeKind.GROUP_ACTIVE: {
if (this.editorGroupsService.activeGroup === group && group.activeEditor) {
this.addMostRecentEditor(group, group.activeEditor, true /* is active */);
}
break;
}
// Editor gets active: put active editor as most recent
// if group is active, otherwise second most recent
case GroupChangeKind.EDITOR_ACTIVE: {
if (e.editor) {
this.addMostRecentEditor(group, e.editor, this.editorGroupsService.activeGroup === group);
}
break;
}
// Editor opens: put it as second most recent
case GroupChangeKind.EDITOR_OPEN: {
if (e.editor) {
this.addMostRecentEditor(group, e.editor, false /* is not active */);
}
break;
}
// Editor closes: remove from recently opened
case GroupChangeKind.EDITOR_CLOSE: {
if (e.editor) {
this.removeMostRecentEditor(group, e.editor);
}
break;
}
}
}));
// Make sure to cleanup on dispose
Event.once(group.onWillDispose)(() => dispose(groupDisposables));
}
private addMostRecentEditor(group: IEditorGroup, editor: IEditorInput, isActive: boolean): void {
const key = this.ensureKey(group, editor);
const mostRecentEditor = this.mostRecentEditorsMap.first;
// Active or first entry: add to end of map
if (isActive || !mostRecentEditor) {
this.mostRecentEditorsMap.set(key, key, mostRecentEditor ? Touch.AsOld /* make first */ : undefined);
}
// Otherwise: insert before most recent
else {
// we have most recent editors. as such we
// put this newly opened editor right before
// the current most recent one because it cannot
// be the most recently active one unless
// it becomes active. but it is still more
// active then any other editor in the list.
this.mostRecentEditorsMap.set(key, key, Touch.AsOld /* make first */);
this.mostRecentEditorsMap.set(mostRecentEditor, mostRecentEditor, Touch.AsOld /* make first */);
}
// Event
this._onDidChange.fire();
}
private removeMostRecentEditor(group: IEditorGroup, editor: IEditorInput): void {
const key = this.findKey(group, editor);
if (key) {
// Remove from most recent editors
this.mostRecentEditorsMap.delete(key);
// Remove from key map
const map = this.keyMap.get(group.id);
if (map && map.delete(key.editor) && map.size === 0) {
this.keyMap.delete(group.id);
}
// Event
this._onDidChange.fire();
}
}
private findKey(group: IEditorGroup, editor: IEditorInput): IEditorIdentifier | undefined {
const groupMap = this.keyMap.get(group.id);
if (!groupMap) {
return undefined;
}
return groupMap.get(editor);
}
private ensureKey(group: IEditorGroup, editor: IEditorInput): IEditorIdentifier {
let groupMap = this.keyMap.get(group.id);
if (!groupMap) {
groupMap = new Map();
this.keyMap.set(group.id, groupMap);
}
let key = groupMap.get(editor);
if (!key) {
key = { groupId: group.id, editor };
groupMap.set(editor, key);
}
return key;
}
private saveState(): void {
if (this.mostRecentEditorsMap.isEmpty()) {
this.storageService.remove(EditorsObserver.STORAGE_KEY, StorageScope.WORKSPACE);
} else {
this.storageService.store(EditorsObserver.STORAGE_KEY, JSON.stringify(this.serialize()), StorageScope.WORKSPACE);
}
}
private serialize(): ISerializedEditorsList {
const registry = Registry.as<IEditorInputFactoryRegistry>(Extensions.EditorInputFactories);
const entries = this.mostRecentEditorsMap.values();
const mapGroupToSerializableEditorsOfGroup = new Map<IEditorGroup, IEditorInput[]>();
return {
entries: coalesce(entries.map(({ editor, groupId }) => {
// Find group for entry
const group = this.editorGroupsService.getGroup(groupId);
if (!group) {
return undefined;
}
// Find serializable editors of group
let serializableEditorsOfGroup = mapGroupToSerializableEditorsOfGroup.get(group);
if (!serializableEditorsOfGroup) {
serializableEditorsOfGroup = group.getEditors(EditorsOrder.SEQUENTIAL).filter(editor => {
const factory = registry.getEditorInputFactory(editor.getTypeId());
return factory?.canSerialize(editor);
});
mapGroupToSerializableEditorsOfGroup.set(group, serializableEditorsOfGroup);
}
// Only store the index of the editor of that group
// which can be undefined if the editor is not serializable
const index = serializableEditorsOfGroup.indexOf(editor);
if (index === -1) {
return undefined;
}
return { groupId, index };
}))
};
}
private loadState(): void {
const serialized = this.storageService.get(EditorsObserver.STORAGE_KEY, StorageScope.WORKSPACE);
// Previous state:
if (serialized) {
// Load editors map from persisted state
this.deserialize(JSON.parse(serialized));
}
// No previous state: best we can do is add each editor
// from oldest to most recently used editor group
else {
const groups = this.editorGroupsService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE);
for (let i = groups.length - 1; i >= 0; i--) {
const group = groups[i];
const groupEditorsMru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE);
for (let i = groupEditorsMru.length - 1; i >= 0; i--) {
this.addMostRecentEditor(group, groupEditorsMru[i], true /* enforce as active to preserve order */);
}
}
}
// Ensure we listen on group changes for those that exist on startup
for (const group of this.editorGroupsService.groups) {
this.registerGroupListeners(group);
}
}
private deserialize(serialized: ISerializedEditorsList): void {
const mapValues: [IEditorIdentifier, IEditorIdentifier][] = [];
for (const { groupId, index } of serialized.entries) {
// Find group for entry
const group = this.editorGroupsService.getGroup(groupId);
if (!group) {
continue;
}
// Find editor for entry
const editor = group.getEditorByIndex(index);
if (!editor) {
continue;
}
// Make sure key is registered as well
const editorIdentifier = this.ensureKey(group, editor);
mapValues.push([editorIdentifier, editorIdentifier]);
}
// Fill map with deserialized values
this.mostRecentEditorsMap.fromJSON(mapValues);
}
}
......@@ -27,6 +27,7 @@ import { IEditorGroupView, IEditorOpeningEvent, EditorServiceImpl } from 'vs/wor
import { ILabelService } from 'vs/platform/label/common/label';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { withNullAsUndefined } from 'vs/base/common/types';
import { EditorsObserver } from 'vs/workbench/browser/parts/editor/editorsObserver';
type CachedEditorInput = ResourceEditorInput | IFileEditorInput;
type OpenInEditorGroup = IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE;
......@@ -51,14 +52,19 @@ export class EditorService extends Disposable implements EditorServiceImpl {
private readonly _onDidOpenEditorFail = this._register(new Emitter<IEditorIdentifier>());
readonly onDidOpenEditorFail = this._onDidOpenEditorFail.event;
private readonly _onDidMostRecentlyActiveEditorsChange = this._register(new Emitter<void>());
readonly onDidMostRecentlyActiveEditorsChange = this._onDidMostRecentlyActiveEditorsChange.event;
//#endregion
private fileInputFactory: IFileInputFactory;
private openEditorHandlers: IOpenEditorOverrideHandler[] = [];
private readonly openEditorHandlers: IOpenEditorOverrideHandler[] = [];
private lastActiveEditor: IEditorInput | undefined = undefined;
private lastActiveGroupId: GroupIdentifier | undefined = undefined;
private readonly editorsObserver = this._register(this.instantiationService.createInstance(EditorsObserver));
constructor(
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
@IUntitledTextEditorService private readonly untitledTextEditorService: IUntitledTextEditorService,
......@@ -80,6 +86,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
this.editorGroupService.whenRestored.then(() => this.onEditorsRestored());
this.editorGroupService.onDidActiveGroupChange(group => this.handleActiveEditorChange(group));
this.editorGroupService.onDidAddGroup(group => this.registerGroupListeners(group as IEditorGroupView));
this.editorsObserver.onDidChange(() => this._onDidMostRecentlyActiveEditorsChange.fire());
}
private onEditorsRestored(): void {
......@@ -188,6 +195,10 @@ export class EditorService extends Disposable implements EditorServiceImpl {
return editors;
}
get mostRecentlyActiveEditors(): IEditorIdentifier[] {
return this.editorsObserver.editors;
}
get activeEditor(): IEditorInput | undefined {
const activeGroup = this.editorGroupService.activeGroup;
......
......@@ -7,7 +7,7 @@ import { onUnexpectedError } from 'vs/base/common/errors';
import { URI, UriComponents } from 'vs/base/common/uri';
import { IEditor } from 'vs/editor/common/editorCommon';
import { ITextEditorOptions, IResourceInput, ITextEditorSelection } from 'vs/platform/editor/common/editor';
import { IEditorInput, IEditor as IBaseEditor, Extensions as EditorExtensions, EditorInput, IEditorCloseEvent, IEditorInputFactoryRegistry, toResource, IEditorIdentifier, GroupIdentifier, Extensions } from 'vs/workbench/common/editor';
import { IEditorInput, IEditor as IBaseEditor, Extensions as EditorExtensions, EditorInput, IEditorCloseEvent, IEditorInputFactoryRegistry, toResource, IEditorIdentifier, GroupIdentifier } 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, FILES_EXCLUDE_CONFIG } from 'vs/platform/files/common/files';
......@@ -16,9 +16,9 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace
import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { Registry } from 'vs/platform/registry/common/platform';
import { Event, Emitter } from 'vs/base/common/event';
import { Event } from 'vs/base/common/event';
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { IEditorGroupsService, IEditorGroup, EditorsOrder, GroupChangeKind, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorGroupsService, EditorsOrder } from 'vs/workbench/services/editor/common/editorGroupsService';
import { getCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { getExcludes, ISearchConfiguration } from 'vs/workbench/services/search/common/search';
import { IExpression } from 'vs/base/common/glob';
......@@ -34,9 +34,6 @@ 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 { LinkedMap, Touch } from 'vs/base/common/map';
//#region Text Editor State helper
/**
* Stores the selection & view state of an editor and allows to compare it to other selection states.
......@@ -86,295 +83,6 @@ export class TextEditorState {
}
}
//#endregion
//#region Editors History
interface ISerializedEditorHistory {
history: ISerializedEditorIdentifier[];
}
interface ISerializedEditorIdentifier {
groupId: GroupIdentifier;
index: number;
}
/**
* A history of opened editors across all editor groups by most recently used.
* Rules:
* - the last editor in the history is the one most recently activated
* - the first editor in the history is the one that was activated the longest time ago
* - an editor that opens inactive will be placed behind the currently active editor
*/
export class EditorsHistory extends Disposable {
private static readonly STORAGE_KEY = 'history.editors';
private readonly keyMap = new Map<GroupIdentifier, Map<IEditorInput, IEditorIdentifier>>();
private readonly mostRecentEditorsMap = new LinkedMap<IEditorIdentifier, IEditorIdentifier>();
private readonly _onDidChange = this._register(new Emitter<void>());
readonly onDidChange = this._onDidChange.event;
get editors(): IEditorIdentifier[] {
return this.mostRecentEditorsMap.values();
}
constructor(
@IEditorGroupsService private editorGroupsService: IEditorGroupsService,
@IStorageService private readonly storageService: IStorageService
) {
super();
this.registerListeners();
}
private registerListeners(): void {
this._register(this.storageService.onWillSaveState(() => this.saveState()));
this._register(this.editorGroupsService.onDidAddGroup(group => this.onGroupAdded(group)));
this.editorGroupsService.whenRestored.then(() => this.loadState());
}
private onGroupAdded(group: IEditorGroup): void {
// Make sure to add any already existing editor
// of the new group into our history in LRU order
const groupEditorsMru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE);
for (let i = groupEditorsMru.length - 1; i >= 0; i--) {
this.addMostRecentEditor(group, groupEditorsMru[i], false /* is not active */);
}
// Make sure that active editor is put as first if group is active
if (this.editorGroupsService.activeGroup === group && group.activeEditor) {
this.addMostRecentEditor(group, group.activeEditor, true /* is active */);
}
// Group Listeners
this.registerGroupListeners(group);
}
private registerGroupListeners(group: IEditorGroup): void {
const groupDisposables = new DisposableStore();
groupDisposables.add(group.onDidGroupChange(e => {
switch (e.kind) {
// Group gets active: put active editor as most recent
case GroupChangeKind.GROUP_ACTIVE: {
if (this.editorGroupsService.activeGroup === group && group.activeEditor) {
this.addMostRecentEditor(group, group.activeEditor, true /* is active */);
}
break;
}
// Editor gets active: put active editor as most recent
// if group is active, otherwise second most recent
case GroupChangeKind.EDITOR_ACTIVE: {
if (e.editor) {
this.addMostRecentEditor(group, e.editor, this.editorGroupsService.activeGroup === group);
}
break;
}
// Editor opens: put it as second most recent
case GroupChangeKind.EDITOR_OPEN: {
if (e.editor) {
this.addMostRecentEditor(group, e.editor, false /* is not active */);
}
break;
}
// Editor closes: remove from recently opened
case GroupChangeKind.EDITOR_CLOSE: {
if (e.editor) {
this.removeMostRecentEditor(group, e.editor);
}
break;
}
}
}));
// Make sure to cleanup on dispose
Event.once(group.onWillDispose)(() => dispose(groupDisposables));
}
private addMostRecentEditor(group: IEditorGroup, editor: IEditorInput, isActive: boolean): void {
const key = this.ensureKey(group, editor);
const mostRecentEditor = this.mostRecentEditorsMap.first;
// Active or first entry: add to end of map
if (isActive || !mostRecentEditor) {
this.mostRecentEditorsMap.set(key, key, mostRecentEditor ? Touch.AsOld /* make first */ : undefined);
}
// Otherwise: insert before most recent
else {
// we have most recent editors. as such we
// put this newly opened editor right before
// the current most recent one because it cannot
// be the most recently active one unless
// it becomes active. but it is still more
// active then any other editor in the list.
this.mostRecentEditorsMap.set(key, key, Touch.AsOld /* make first */);
this.mostRecentEditorsMap.set(mostRecentEditor, mostRecentEditor, Touch.AsOld /* make first */);
}
// Event
this._onDidChange.fire();
}
private removeMostRecentEditor(group: IEditorGroup, editor: IEditorInput): void {
const key = this.findKey(group, editor);
if (key) {
// Remove from most recent editors
this.mostRecentEditorsMap.delete(key);
// Remove from key map
const map = this.keyMap.get(group.id);
if (map && map.delete(key.editor) && map.size === 0) {
this.keyMap.delete(group.id);
}
// Event
this._onDidChange.fire();
}
}
private findKey(group: IEditorGroup, editor: IEditorInput): IEditorIdentifier | undefined {
const groupMap = this.keyMap.get(group.id);
if (!groupMap) {
return undefined;
}
return groupMap.get(editor);
}
private ensureKey(group: IEditorGroup, editor: IEditorInput): IEditorIdentifier {
let groupMap = this.keyMap.get(group.id);
if (!groupMap) {
groupMap = new Map();
this.keyMap.set(group.id, groupMap);
}
let key = groupMap.get(editor);
if (!key) {
key = { groupId: group.id, editor };
groupMap.set(editor, key);
}
return key;
}
private saveState(): void {
if (this.mostRecentEditorsMap.isEmpty()) {
this.storageService.remove(EditorsHistory.STORAGE_KEY, StorageScope.WORKSPACE);
} else {
this.storageService.store(EditorsHistory.STORAGE_KEY, JSON.stringify(this.serialize()), StorageScope.WORKSPACE);
}
}
private serialize(): ISerializedEditorHistory {
const registry = Registry.as<IEditorInputFactoryRegistry>(Extensions.EditorInputFactories);
const history = this.mostRecentEditorsMap.values();
const mapGroupToSerializableEditorsOfGroup = new Map<IEditorGroup, IEditorInput[]>();
return {
history: coalesce(history.map(({ editor, groupId }) => {
// Find group for entry
const group = this.editorGroupsService.getGroup(groupId);
if (!group) {
return undefined;
}
// Find serializable editors of group
let serializableEditorsOfGroup = mapGroupToSerializableEditorsOfGroup.get(group);
if (!serializableEditorsOfGroup) {
serializableEditorsOfGroup = group.getEditors(EditorsOrder.SEQUENTIAL).filter(editor => {
const factory = registry.getEditorInputFactory(editor.getTypeId());
return factory?.canSerialize(editor);
});
mapGroupToSerializableEditorsOfGroup.set(group, serializableEditorsOfGroup);
}
// Only store the index of the editor of that group
// which can be undefined if the editor is not serializable
const index = serializableEditorsOfGroup.indexOf(editor);
if (index === -1) {
return undefined;
}
return { groupId, index };
}))
};
}
private loadState(): void {
const serialized = this.storageService.get(EditorsHistory.STORAGE_KEY, StorageScope.WORKSPACE);
// Previous state:
if (serialized) {
// Load history map from persisted state
this.deserialize(JSON.parse(serialized));
}
// No previous state: best we can do is add each editor
// from oldest to most recently used editor group
else {
const groups = this.editorGroupsService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE);
for (let i = groups.length - 1; i >= 0; i--) {
const group = groups[i];
const groupEditorsMru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE);
for (let i = groupEditorsMru.length - 1; i >= 0; i--) {
this.addMostRecentEditor(group, groupEditorsMru[i], true /* enforce as active to preserve order */);
}
}
}
// Ensure we listen on group changes for those that exist on startup
for (const group of this.editorGroupsService.groups) {
this.registerGroupListeners(group);
}
}
deserialize(serialized: ISerializedEditorHistory): void {
const mapValues: [IEditorIdentifier, IEditorIdentifier][] = [];
for (const { groupId, index } of serialized.history) {
// Find group for entry
const group = this.editorGroupsService.getGroup(groupId);
if (!group) {
continue;
}
// Find editor for entry
const editor = group.getEditorByIndex(index);
if (!editor) {
continue;
}
// Make sure key is registered as well
const editorIdentifier = this.ensureKey(group, editor);
mapValues.push([editorIdentifier, editorIdentifier]);
}
// Fill map with deserialized values
this.mostRecentEditorsMap.fromJSON(mapValues);
}
}
//#endregion
interface ISerializedEditorHistoryEntry {
resourceJSON?: object;
editorInputJSON?: { typeId: string; deserialized: string; };
......@@ -424,7 +132,7 @@ export class HistoryService extends Disposable implements IHistoryService {
this._register(this.storageService.onWillSaveState(() => this.saveState()));
this._register(this.fileService.onFileChanges(event => this.onFileChanges(event)));
this._register(this.resourceFilter.onExpressionChange(() => this.removeExcludedFromHistory()));
this._register(this.mostRecentlyUsedOpenEditors.onDidChange(() => this.handleEditorEventInRecentEditorsStack()));
this._register(this.editorService.onDidMostRecentlyActiveEditorsChange(() => this.handleEditorEventInRecentEditorsStack()));
// if the service is created late enough that an editor is already opened
// make sure to trigger the onActiveEditorChanged() to track the editor
......@@ -1102,7 +810,7 @@ export class HistoryService extends Disposable implements IHistoryService {
this.editorHistoryListeners.clear();
}
getHistory(): Array<IEditorInput | IResourceInput> {
getHistory(): ReadonlyArray<IEditorInput | IResourceInput> {
this.ensureHistoryLoaded();
return this.history.slice(0);
......@@ -1266,12 +974,10 @@ export class HistoryService extends Disposable implements IHistoryService {
//#region Editor Most Recently Used History
private readonly mostRecentlyUsedOpenEditors = this._register(this.instantiationService.createInstance(EditorsHistory));
private recentlyUsedEditorsStack: IEditorIdentifier[] | undefined = undefined;
private recentlyUsedEditorsStack: ReadonlyArray<IEditorIdentifier> | undefined = undefined;
private recentlyUsedEditorsStackIndex = 0;
private recentlyUsedEditorsInGroupStack: IEditorIdentifier[] | undefined = undefined;
private recentlyUsedEditorsInGroupStack: ReadonlyArray<IEditorIdentifier> | undefined = undefined;
private recentlyUsedEditorsInGroupStackIndex = 0;
private navigatingInRecentlyUsedEditorsStack = false;
......@@ -1309,15 +1015,15 @@ export class HistoryService extends Disposable implements IHistoryService {
}
}
private ensureRecentlyUsedStack(indexModifier: (index: number) => number, groupId?: GroupIdentifier): [IEditorIdentifier[], number] {
let editors: IEditorIdentifier[];
private ensureRecentlyUsedStack(indexModifier: (index: number) => number, groupId?: GroupIdentifier): [ReadonlyArray<IEditorIdentifier>, number] {
let editors: ReadonlyArray<IEditorIdentifier>;
let index: number;
const group = typeof groupId === 'number' ? this.editorGroupService.getGroup(groupId) : undefined;
// Across groups
if (!group) {
editors = this.recentlyUsedEditorsStack || this.mostRecentlyUsedOpenEditors.editors;
editors = this.recentlyUsedEditorsStack || this.editorService.mostRecentlyActiveEditors;
index = this.recentlyUsedEditorsStackIndex;
}
......@@ -1362,8 +1068,8 @@ export class HistoryService extends Disposable implements IHistoryService {
}
}
getMostRecentlyUsedOpenEditors(): Array<IEditorIdentifier> {
return this.mostRecentlyUsedOpenEditors.editors;
getMostRecentlyUsedOpenEditors(): ReadonlyArray<IEditorIdentifier> {
return this.editorService.mostRecentlyActiveEditors;
}
//#endregion
......
......@@ -57,7 +57,7 @@ export interface IHistoryService {
/**
* Get the entire history of editors that were opened.
*/
getHistory(): Array<IEditorInput | IResourceInput>;
getHistory(): ReadonlyArray<IEditorInput | IResourceInput>;
/**
* Looking at the editor history, returns the workspace root of the last file that was
......@@ -91,5 +91,5 @@ export interface IHistoryService {
/**
* Get a list of most recently used editors that are open.
*/
getMostRecentlyUsedOpenEditors(): Array<IEditorIdentifier>;
getMostRecentlyUsedOpenEditors(): ReadonlyArray<IEditorIdentifier>;
}
......@@ -11,18 +11,16 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart';
import { IEditorRegistry, EditorDescriptor, Extensions } from 'vs/workbench/browser/editor';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { EditorActivation, IEditorModel } from 'vs/platform/editor/common/editor';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorModel } from 'vs/platform/editor/common/editor';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
import { CancellationToken } from 'vs/base/common/cancellation';
import { EditorsHistory, HistoryService } from 'vs/workbench/services/history/browser/history';
import { WillSaveStateReason } from 'vs/platform/storage/common/storage';
import { HistoryService } from 'vs/workbench/services/history/browser/history';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { EditorService } from 'vs/workbench/services/editor/browser/editorService';
import { timeout } from 'vs/base/common/async';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
......@@ -180,418 +178,6 @@ suite('HistoryService', function () {
part.dispose();
});
suite('EditorHistory', function () {
test('basics (single group)', async () => {
const instantiationService = workbenchInstantiationService();
const part = instantiationService.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
await part.whenRestored;
const history = new EditorsHistory(part, new TestStorageService());
let historyChangeListenerCalled = false;
const listener = history.onDidChange(() => {
historyChangeListenerCalled = true;
});
let currentHistory = history.editors;
assert.equal(currentHistory.length, 0);
assert.equal(historyChangeListenerCalled, false);
const input1 = new HistoryTestEditorInput(URI.parse('foo://bar1'));
await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
currentHistory = history.editors;
assert.equal(currentHistory.length, 1);
assert.equal(currentHistory[0].groupId, part.activeGroup.id);
assert.equal(currentHistory[0].editor, input1);
assert.equal(historyChangeListenerCalled, true);
const input2 = new HistoryTestEditorInput(URI.parse('foo://bar2'));
const input3 = new HistoryTestEditorInput(URI.parse('foo://bar3'));
await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
await part.activeGroup.openEditor(input3, EditorOptions.create({ pinned: true }));
currentHistory = history.editors;
assert.equal(currentHistory.length, 3);
assert.equal(currentHistory[0].groupId, part.activeGroup.id);
assert.equal(currentHistory[0].editor, input3);
assert.equal(currentHistory[1].groupId, part.activeGroup.id);
assert.equal(currentHistory[1].editor, input2);
assert.equal(currentHistory[2].groupId, part.activeGroup.id);
assert.equal(currentHistory[2].editor, input1);
await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
currentHistory = history.editors;
assert.equal(currentHistory.length, 3);
assert.equal(currentHistory[0].groupId, part.activeGroup.id);
assert.equal(currentHistory[0].editor, input2);
assert.equal(currentHistory[1].groupId, part.activeGroup.id);
assert.equal(currentHistory[1].editor, input3);
assert.equal(currentHistory[2].groupId, part.activeGroup.id);
assert.equal(currentHistory[2].editor, input1);
historyChangeListenerCalled = false;
await part.activeGroup.closeEditor(input1);
currentHistory = history.editors;
assert.equal(currentHistory.length, 2);
assert.equal(currentHistory[0].groupId, part.activeGroup.id);
assert.equal(currentHistory[0].editor, input2);
assert.equal(currentHistory[1].groupId, part.activeGroup.id);
assert.equal(currentHistory[1].editor, input3);
assert.equal(historyChangeListenerCalled, true);
await part.activeGroup.closeAllEditors();
currentHistory = history.editors;
assert.equal(currentHistory.length, 0);
part.dispose();
listener.dispose();
});
test('basics (multi group)', async () => {
const instantiationService = workbenchInstantiationService();
const part = instantiationService.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
await part.whenRestored;
const rootGroup = part.activeGroup;
const history = new EditorsHistory(part, new TestStorageService());
let currentHistory = history.editors;
assert.equal(currentHistory.length, 0);
const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT);
const input1 = new HistoryTestEditorInput(URI.parse('foo://bar1'));
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE }));
await sideGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE }));
currentHistory = history.editors;
assert.equal(currentHistory.length, 2);
assert.equal(currentHistory[0].groupId, sideGroup.id);
assert.equal(currentHistory[0].editor, input1);
assert.equal(currentHistory[1].groupId, rootGroup.id);
assert.equal(currentHistory[1].editor, input1);
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE }));
currentHistory = history.editors;
assert.equal(currentHistory.length, 2);
assert.equal(currentHistory[0].groupId, rootGroup.id);
assert.equal(currentHistory[0].editor, input1);
assert.equal(currentHistory[1].groupId, sideGroup.id);
assert.equal(currentHistory[1].editor, input1);
// Opening an editor inactive should not change
// the most recent editor, but rather put it behind
const input2 = new HistoryTestEditorInput(URI.parse('foo://bar2'));
await rootGroup.openEditor(input2, EditorOptions.create({ inactive: true }));
currentHistory = history.editors;
assert.equal(currentHistory.length, 3);
assert.equal(currentHistory[0].groupId, rootGroup.id);
assert.equal(currentHistory[0].editor, input1);
assert.equal(currentHistory[1].groupId, rootGroup.id);
assert.equal(currentHistory[1].editor, input2);
assert.equal(currentHistory[2].groupId, sideGroup.id);
assert.equal(currentHistory[2].editor, input1);
await rootGroup.closeAllEditors();
currentHistory = history.editors;
assert.equal(currentHistory.length, 1);
assert.equal(currentHistory[0].groupId, sideGroup.id);
assert.equal(currentHistory[0].editor, input1);
await sideGroup.closeAllEditors();
currentHistory = history.editors;
assert.equal(currentHistory.length, 0);
part.dispose();
});
test('copy group', async () => {
const instantiationService = workbenchInstantiationService();
const part = instantiationService.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
await part.whenRestored;
const history = new EditorsHistory(part, new TestStorageService());
const input1 = new HistoryTestEditorInput(URI.parse('foo://bar1'));
const input2 = new HistoryTestEditorInput(URI.parse('foo://bar2'));
const input3 = new HistoryTestEditorInput(URI.parse('foo://bar3'));
const rootGroup = part.activeGroup;
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true }));
let currentHistory = history.editors;
assert.equal(currentHistory.length, 3);
assert.equal(currentHistory[0].groupId, rootGroup.id);
assert.equal(currentHistory[0].editor, input3);
assert.equal(currentHistory[1].groupId, rootGroup.id);
assert.equal(currentHistory[1].editor, input2);
assert.equal(currentHistory[2].groupId, rootGroup.id);
assert.equal(currentHistory[2].editor, input1);
const copiedGroup = part.copyGroup(rootGroup, rootGroup, GroupDirection.RIGHT);
copiedGroup.setActive(true);
currentHistory = history.editors;
assert.equal(currentHistory.length, 6);
assert.equal(currentHistory[0].groupId, copiedGroup.id);
assert.equal(currentHistory[0].editor, input3);
assert.equal(currentHistory[1].groupId, rootGroup.id);
assert.equal(currentHistory[1].editor, input3);
assert.equal(currentHistory[2].groupId, copiedGroup.id);
assert.equal(currentHistory[2].editor, input2);
assert.equal(currentHistory[3].groupId, copiedGroup.id);
assert.equal(currentHistory[3].editor, input1);
assert.equal(currentHistory[4].groupId, rootGroup.id);
assert.equal(currentHistory[4].editor, input2);
assert.equal(currentHistory[5].groupId, rootGroup.id);
assert.equal(currentHistory[5].editor, input1);
part.dispose();
});
test('initial editors are part of history and state is persisted & restored (single group)', async () => {
const instantiationService = workbenchInstantiationService();
instantiationService.invokeFunction(accessor => Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).start(accessor));
const part = instantiationService.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
await part.whenRestored;
const rootGroup = part.activeGroup;
const input1 = new HistoryTestEditorInput(URI.parse('foo://bar1'));
const input2 = new HistoryTestEditorInput(URI.parse('foo://bar2'));
const input3 = new HistoryTestEditorInput(URI.parse('foo://bar3'));
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true }));
const storage = new TestStorageService();
const history = new EditorsHistory(part, storage);
await part.whenRestored;
let currentHistory = history.editors;
assert.equal(currentHistory.length, 3);
assert.equal(currentHistory[0].groupId, rootGroup.id);
assert.equal(currentHistory[0].editor, input3);
assert.equal(currentHistory[1].groupId, rootGroup.id);
assert.equal(currentHistory[1].editor, input2);
assert.equal(currentHistory[2].groupId, rootGroup.id);
assert.equal(currentHistory[2].editor, input1);
storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
const restoredHistory = new EditorsHistory(part, storage);
await part.whenRestored;
currentHistory = restoredHistory.editors;
assert.equal(currentHistory.length, 3);
assert.equal(currentHistory[0].groupId, rootGroup.id);
assert.equal(currentHistory[0].editor, input3);
assert.equal(currentHistory[1].groupId, rootGroup.id);
assert.equal(currentHistory[1].editor, input2);
assert.equal(currentHistory[2].groupId, rootGroup.id);
assert.equal(currentHistory[2].editor, input1);
part.dispose();
});
test('initial editors are part of history (multi group)', async () => {
const instantiationService = workbenchInstantiationService();
const part = instantiationService.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
await part.whenRestored;
const rootGroup = part.activeGroup;
const input1 = new HistoryTestEditorInput(URI.parse('foo://bar1'));
const input2 = new HistoryTestEditorInput(URI.parse('foo://bar2'));
const input3 = new HistoryTestEditorInput(URI.parse('foo://bar3'));
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT);
await sideGroup.openEditor(input3, EditorOptions.create({ pinned: true }));
const storage = new TestStorageService();
const history = new EditorsHistory(part, storage);
await part.whenRestored;
let currentHistory = history.editors;
assert.equal(currentHistory.length, 3);
assert.equal(currentHistory[0].groupId, sideGroup.id);
assert.equal(currentHistory[0].editor, input3);
assert.equal(currentHistory[1].groupId, rootGroup.id);
assert.equal(currentHistory[1].editor, input2);
assert.equal(currentHistory[2].groupId, rootGroup.id);
assert.equal(currentHistory[2].editor, input1);
storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
const restoredHistory = new EditorsHistory(part, storage);
await part.whenRestored;
currentHistory = restoredHistory.editors;
assert.equal(currentHistory.length, 3);
assert.equal(currentHistory[0].groupId, sideGroup.id);
assert.equal(currentHistory[0].editor, input3);
assert.equal(currentHistory[1].groupId, rootGroup.id);
assert.equal(currentHistory[1].editor, input2);
assert.equal(currentHistory[2].groupId, rootGroup.id);
assert.equal(currentHistory[2].editor, input1);
part.dispose();
});
test('history does not restore editors that cannot be serialized', async () => {
const instantiationService = workbenchInstantiationService();
instantiationService.invokeFunction(accessor => Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).start(accessor));
const part = instantiationService.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
await part.whenRestored;
const rootGroup = part.activeGroup;
const input1 = new TestEditorInput(URI.parse('foo://bar1'));
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
const storage = new TestStorageService();
const history = new EditorsHistory(part, storage);
await part.whenRestored;
let currentHistory = history.editors;
assert.equal(currentHistory.length, 1);
assert.equal(currentHistory[0].groupId, rootGroup.id);
assert.equal(currentHistory[0].editor, input1);
storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
const restoredHistory = new EditorsHistory(part, storage);
await part.whenRestored;
currentHistory = restoredHistory.editors;
assert.equal(currentHistory.length, 0);
part.dispose();
});
test('open next/previous recently used editor (single group)', async () => {
const [part, historyService] = await createServices();
const input1 = new TestEditorInput(URI.parse('foo://bar1'));
const input2 = new TestEditorInput(URI.parse('foo://bar2'));
await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
assert.equal(part.activeGroup.activeEditor, input1);
await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
assert.equal(part.activeGroup.activeEditor, input2);
historyService.openPreviouslyUsedEditor();
assert.equal(part.activeGroup.activeEditor, input1);
historyService.openNextRecentlyUsedEditor();
assert.equal(part.activeGroup.activeEditor, input2);
historyService.openPreviouslyUsedEditor(part.activeGroup.id);
assert.equal(part.activeGroup.activeEditor, input1);
historyService.openNextRecentlyUsedEditor(part.activeGroup.id);
assert.equal(part.activeGroup.activeEditor, input2);
part.dispose();
});
test('open next/previous recently used editor (multi group)', async () => {
const [part, historyService] = await createServices();
const rootGroup = part.activeGroup;
const input1 = new TestEditorInput(URI.parse('foo://bar1'));
const input2 = new TestEditorInput(URI.parse('foo://bar2'));
const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT);
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
await sideGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
historyService.openPreviouslyUsedEditor();
assert.equal(part.activeGroup, rootGroup);
assert.equal(rootGroup.activeEditor, input1);
historyService.openNextRecentlyUsedEditor();
assert.equal(part.activeGroup, sideGroup);
assert.equal(sideGroup.activeEditor, input2);
part.dispose();
});
test('open next/previous recently is reset when other input opens', async () => {
const [part, historyService] = await createServices();
const input1 = new TestEditorInput(URI.parse('foo://bar1'));
const input2 = new TestEditorInput(URI.parse('foo://bar2'));
const input3 = new TestEditorInput(URI.parse('foo://bar3'));
const input4 = new TestEditorInput(URI.parse('foo://bar4'));
await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
await part.activeGroup.openEditor(input3, EditorOptions.create({ pinned: true }));
historyService.openPreviouslyUsedEditor();
assert.equal(part.activeGroup.activeEditor, input2);
await timeout(0);
await part.activeGroup.openEditor(input4, EditorOptions.create({ pinned: true }));
historyService.openPreviouslyUsedEditor();
assert.equal(part.activeGroup.activeEditor, input2);
historyService.openNextRecentlyUsedEditor();
assert.equal(part.activeGroup.activeEditor, input4);
part.dispose();
});
});
});
......@@ -374,7 +374,7 @@ export class TestHistoryService implements IHistoryService {
remove(_input: IEditorInput | IResourceInput): void { }
clear(): void { }
clearRecentlyOpened(): void { }
getHistory(): Array<IEditorInput | IResourceInput> { return []; }
getHistory(): ReadonlyArray<IEditorInput | IResourceInput> { return []; }
openNextRecentlyUsedEditor(group?: GroupIdentifier): void { }
openPreviouslyUsedEditor(group?: GroupIdentifier): void { }
getMostRecentlyUsedOpenEditors(): Array<IEditorIdentifier> { return []; }
......@@ -902,11 +902,13 @@ export class TestEditorService implements EditorServiceImpl {
onDidVisibleEditorsChange: Event<void> = Event.None;
onDidCloseEditor: Event<IEditorCloseEvent> = Event.None;
onDidOpenEditorFail: Event<IEditorIdentifier> = Event.None;
onDidMostRecentlyActiveEditorsChange: Event<void> = Event.None;
activeControl!: IVisibleEditor;
activeTextEditorWidget: any;
activeEditor!: IEditorInput;
editors: ReadonlyArray<IEditorInput> = [];
mostRecentlyActiveEditors: ReadonlyArray<IEditorIdentifier> = [];
visibleControls: ReadonlyArray<IVisibleEditor> = [];
visibleTextEditorWidgets = [];
visibleEditors: ReadonlyArray<IEditorInput> = [];
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册