提交 1eac96d1 编写于 作者: B Benjamin Pasero

history - use editor input factory to support to reopen any editor that can be serialised

上级 516bb5fd
......@@ -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;
}
/**
......
......@@ -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);
});
});
......@@ -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;
......
......@@ -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
......
......@@ -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;
......
......@@ -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';
......@@ -17,7 +17,7 @@ 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';
......@@ -25,13 +25,12 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
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';
/**
......@@ -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;
}
......@@ -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,
......@@ -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);
}
......@@ -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();
......@@ -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
......@@ -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
......@@ -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()
......@@ -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) {
......@@ -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) {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册