mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-22 04:40:30 -04:00
SQL Operations Studio Public Preview 1 (0.23) release source code
This commit is contained in:
708
src/vs/workbench/services/history/browser/history.ts
Normal file
708
src/vs/workbench/services/history/browser/history.ts
Normal file
@@ -0,0 +1,708 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import errors = require('vs/base/common/errors');
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { IEditor } from 'vs/editor/common/editorCommon';
|
||||
import { IEditor as IBaseEditor, IEditorInput, ITextEditorOptions, IResourceInput } from 'vs/platform/editor/common/editor';
|
||||
import { EditorInput, IEditorCloseEvent, IEditorRegistry, Extensions, toResource, IEditorGroup } from 'vs/workbench/common/editor';
|
||||
import { IWorkbenchEditorService } 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';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { once } from 'vs/base/common/event';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
|
||||
import { IWindowsService } from 'vs/platform/windows/common/windows';
|
||||
import { getCodeEditor } from 'vs/editor/common/services/codeEditorService';
|
||||
import { getExcludes, ISearchConfiguration } from 'vs/platform/search/common/search';
|
||||
import { parse, IExpression } from 'vs/base/common/glob';
|
||||
import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ResourceGlobMatcher } from 'vs/workbench/common/resources';
|
||||
|
||||
/**
|
||||
* Stores the selection & view state of an editor and allows to compare it to other selection states.
|
||||
*/
|
||||
export class EditorState {
|
||||
|
||||
private static EDITOR_SELECTION_THRESHOLD = 5; // number of lines to move in editor to justify for new state
|
||||
|
||||
constructor(private _editorInput: IEditorInput, private _selection: Selection) {
|
||||
}
|
||||
|
||||
public get editorInput(): IEditorInput {
|
||||
return this._editorInput;
|
||||
}
|
||||
|
||||
public get selection(): Selection {
|
||||
return this._selection;
|
||||
}
|
||||
|
||||
public justifiesNewPushState(other: EditorState, event?: ICursorPositionChangedEvent): boolean {
|
||||
if (!this._editorInput.matches(other._editorInput)) {
|
||||
return true; // push different editor inputs
|
||||
}
|
||||
|
||||
if (!Selection.isISelection(this._selection) || !Selection.isISelection(other._selection)) {
|
||||
return true; // unknown selections
|
||||
}
|
||||
|
||||
if (event && event.source === 'api') {
|
||||
return true; // always let API source win (e.g. "Go to definition" should add a history entry)
|
||||
}
|
||||
|
||||
const myLineNumber = Math.min(this._selection.selectionStartLineNumber, this._selection.positionLineNumber);
|
||||
const otherLineNumber = Math.min(other._selection.selectionStartLineNumber, other._selection.positionLineNumber);
|
||||
|
||||
if (Math.abs(myLineNumber - otherLineNumber) < EditorState.EDITOR_SELECTION_THRESHOLD) {
|
||||
return false; // ignore selection changes in the range of EditorState.EDITOR_SELECTION_THRESHOLD lines
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
interface ISerializedFileHistoryEntry {
|
||||
resource?: string;
|
||||
resourceJSON: object;
|
||||
}
|
||||
|
||||
export abstract class BaseHistoryService {
|
||||
|
||||
protected toUnbind: IDisposable[];
|
||||
|
||||
private activeEditorListeners: IDisposable[];
|
||||
|
||||
constructor(
|
||||
protected editorGroupService: IEditorGroupService,
|
||||
protected editorService: IWorkbenchEditorService
|
||||
) {
|
||||
this.toUnbind = [];
|
||||
this.activeEditorListeners = [];
|
||||
|
||||
// Listeners
|
||||
this.toUnbind.push(this.editorGroupService.onEditorsChanged(() => this.onEditorsChanged()));
|
||||
}
|
||||
|
||||
private onEditorsChanged(): void {
|
||||
|
||||
// Dispose old listeners
|
||||
dispose(this.activeEditorListeners);
|
||||
this.activeEditorListeners = [];
|
||||
|
||||
const activeEditor = this.editorService.getActiveEditor();
|
||||
|
||||
// Propagate to history
|
||||
this.handleActiveEditorChange(activeEditor);
|
||||
|
||||
// Apply listener for selection changes if this is a text editor
|
||||
const control = getCodeEditor(activeEditor);
|
||||
if (control) {
|
||||
this.activeEditorListeners.push(control.onDidChangeCursorPosition(event => {
|
||||
this.handleEditorSelectionChangeEvent(activeEditor, event);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract handleExcludesChange(): void;
|
||||
|
||||
protected abstract handleEditorSelectionChangeEvent(editor?: IBaseEditor, event?: ICursorPositionChangedEvent): void;
|
||||
|
||||
protected abstract handleActiveEditorChange(editor?: IBaseEditor): void;
|
||||
|
||||
public dispose(): void {
|
||||
this.toUnbind = dispose(this.toUnbind);
|
||||
}
|
||||
}
|
||||
|
||||
interface IStackEntry {
|
||||
input: IEditorInput | IResourceInput;
|
||||
options?: ITextEditorOptions;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface IRecentlyClosedFile {
|
||||
resource: URI;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export class HistoryService extends BaseHistoryService implements IHistoryService {
|
||||
|
||||
public _serviceBrand: any;
|
||||
|
||||
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 static MERGE_EVENT_CHANGES_THRESHOLD = 100;
|
||||
|
||||
private stack: IStackEntry[];
|
||||
private index: number;
|
||||
private navigatingInStack: boolean;
|
||||
private currentFileEditorState: EditorState;
|
||||
|
||||
private history: (IEditorInput | IResourceInput)[];
|
||||
private recentlyClosedFiles: IRecentlyClosedFile[];
|
||||
private loaded: boolean;
|
||||
private registry: IEditorRegistry;
|
||||
private resourceFilter: ResourceGlobMatcher;
|
||||
|
||||
constructor(
|
||||
@IWorkbenchEditorService editorService: IWorkbenchEditorService,
|
||||
@IEditorGroupService editorGroupService: IEditorGroupService,
|
||||
@IWorkspaceContextService private contextService: IWorkspaceContextService,
|
||||
@IStorageService private storageService: IStorageService,
|
||||
@IConfigurationService private configurationService: IConfigurationService,
|
||||
@ILifecycleService private lifecycleService: ILifecycleService,
|
||||
@IFileService private fileService: IFileService,
|
||||
@IWindowsService private windowService: IWindowsService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService,
|
||||
) {
|
||||
super(editorGroupService, editorService);
|
||||
|
||||
this.index = -1;
|
||||
this.stack = [];
|
||||
this.recentlyClosedFiles = [];
|
||||
this.loaded = false;
|
||||
this.registry = Registry.as<IEditorRegistry>(Extensions.Editors);
|
||||
this.resourceFilter = instantiationService.createInstance(ResourceGlobMatcher, root => this.getExcludes(root), (expression: IExpression) => parse(expression));
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private getExcludes(root?: URI): IExpression {
|
||||
const scope = root ? { resource: root } : void 0;
|
||||
|
||||
return getExcludes(this.configurationService.getConfiguration<ISearchConfiguration>(void 0, scope));
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this.toUnbind.push(this.lifecycleService.onShutdown(reason => this.save()));
|
||||
this.toUnbind.push(this.editorGroupService.onEditorOpenFail(editor => this.remove(editor)));
|
||||
this.toUnbind.push(this.editorGroupService.getStacksModel().onEditorClosed(event => this.onEditorClosed(event)));
|
||||
this.toUnbind.push(this.fileService.onFileChanges(e => this.onFileChanges(e)));
|
||||
this.toUnbind.push(this.resourceFilter.onExpressionChange(() => this.handleExcludesChange()));
|
||||
}
|
||||
|
||||
private onFileChanges(e: FileChangesEvent): void {
|
||||
if (e.gotDeleted()) {
|
||||
this.remove(e); // remove from history files that got deleted or moved
|
||||
}
|
||||
}
|
||||
|
||||
private onEditorClosed(event: IEditorCloseEvent): void {
|
||||
|
||||
// Track closing of pinned editor to support to reopen closed editors
|
||||
if (event.pinned) {
|
||||
const file = toResource(event.editor, { filter: 'file' }); // we only support files to reopen
|
||||
if (file) {
|
||||
|
||||
// Remove all inputs matching and add as last recently closed
|
||||
this.removeFromRecentlyClosedFiles(event.editor);
|
||||
this.recentlyClosedFiles.push({ resource: file, index: event.index });
|
||||
|
||||
// Bounding
|
||||
if (this.recentlyClosedFiles.length > HistoryService.MAX_RECENTLY_CLOSED_EDITORS) {
|
||||
this.recentlyClosedFiles.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public reopenLastClosedEditor(): void {
|
||||
this.ensureHistoryLoaded();
|
||||
|
||||
const stacks = this.editorGroupService.getStacksModel();
|
||||
|
||||
let lastClosedFile = this.recentlyClosedFiles.pop();
|
||||
while (lastClosedFile && this.isFileOpened(lastClosedFile.resource, stacks.activeGroup)) {
|
||||
lastClosedFile = this.recentlyClosedFiles.pop(); // pop until we find a file that is not opened
|
||||
}
|
||||
|
||||
if (lastClosedFile) {
|
||||
this.editorService.openEditor({ resource: lastClosedFile.resource, options: { pinned: true, index: lastClosedFile.index } });
|
||||
}
|
||||
}
|
||||
|
||||
public forward(acrossEditors?: boolean): void {
|
||||
if (this.stack.length > this.index + 1) {
|
||||
if (acrossEditors) {
|
||||
this.doForwardAcrossEditors();
|
||||
} else {
|
||||
this.doForwardInEditors();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private doForwardInEditors(): void {
|
||||
this.index++;
|
||||
this.navigate();
|
||||
}
|
||||
|
||||
private doForwardAcrossEditors(): void {
|
||||
let currentIndex = this.index;
|
||||
const currentEntry = this.stack[this.index];
|
||||
|
||||
// Find the next entry that does not match our current entry
|
||||
while (this.stack.length > currentIndex + 1) {
|
||||
currentIndex++;
|
||||
|
||||
const previousEntry = this.stack[currentIndex];
|
||||
if (!this.matches(currentEntry.input, previousEntry.input)) {
|
||||
this.index = currentIndex;
|
||||
this.navigate(true /* across editors */);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public back(acrossEditors?: boolean): void {
|
||||
if (this.index > 0) {
|
||||
if (acrossEditors) {
|
||||
this.doBackAcrossEditors();
|
||||
} else {
|
||||
this.doBackInEditors();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private doBackInEditors(): void {
|
||||
this.index--;
|
||||
this.navigate();
|
||||
}
|
||||
|
||||
private doBackAcrossEditors(): void {
|
||||
let currentIndex = this.index;
|
||||
const currentEntry = this.stack[this.index];
|
||||
|
||||
// Find the next previous entry that does not match our current entry
|
||||
while (currentIndex > 0) {
|
||||
currentIndex--;
|
||||
|
||||
const previousEntry = this.stack[currentIndex];
|
||||
if (!this.matches(currentEntry.input, previousEntry.input)) {
|
||||
this.index = currentIndex;
|
||||
this.navigate(true /* across editors */);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.ensureHistoryLoaded();
|
||||
|
||||
this.index = -1;
|
||||
this.stack.splice(0);
|
||||
this.history = [];
|
||||
this.recentlyClosedFiles = [];
|
||||
}
|
||||
|
||||
private navigate(acrossEditors?: boolean): void {
|
||||
const entry = this.stack[this.index];
|
||||
|
||||
let options = entry.options;
|
||||
if (options && !acrossEditors /* ignore line/col options when going across editors */) {
|
||||
options.revealIfOpened = true;
|
||||
} else {
|
||||
options = { revealIfOpened: true };
|
||||
}
|
||||
|
||||
this.navigatingInStack = true;
|
||||
|
||||
let openEditorPromise: TPromise<IBaseEditor>;
|
||||
if (entry.input instanceof EditorInput) {
|
||||
openEditorPromise = this.editorService.openEditor(entry.input, options);
|
||||
} else {
|
||||
openEditorPromise = this.editorService.openEditor({ resource: (entry.input as IResourceInput).resource, options });
|
||||
}
|
||||
|
||||
openEditorPromise.done(() => {
|
||||
this.navigatingInStack = false;
|
||||
}, error => {
|
||||
this.navigatingInStack = false;
|
||||
errors.onUnexpectedError(error);
|
||||
});
|
||||
}
|
||||
|
||||
protected handleEditorSelectionChangeEvent(editor?: IBaseEditor, event?: ICursorPositionChangedEvent): void {
|
||||
this.handleEditorEventInStack(editor, event);
|
||||
}
|
||||
|
||||
protected handleActiveEditorChange(editor?: IBaseEditor): void {
|
||||
this.handleEditorEventInHistory(editor);
|
||||
this.handleEditorEventInStack(editor);
|
||||
}
|
||||
|
||||
private handleEditorEventInHistory(editor?: IBaseEditor): void {
|
||||
const input = editor ? editor.input : void 0;
|
||||
|
||||
// Ensure we have at least a name to show and not configured to exclude input
|
||||
if (!input || !input.getName() || !this.include(input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ensureHistoryLoaded();
|
||||
|
||||
const historyInput = this.preferResourceInput(input);
|
||||
|
||||
// Remove any existing entry and add to the beginning
|
||||
this.removeFromHistory(input);
|
||||
this.history.unshift(historyInput);
|
||||
|
||||
// Respect max entries setting
|
||||
if (this.history.length > HistoryService.MAX_HISTORY_ITEMS) {
|
||||
this.history.pop();
|
||||
}
|
||||
|
||||
// Remove this from the history unless the history input is a resource
|
||||
// that can easily be restored even when the input gets disposed
|
||||
if (historyInput instanceof EditorInput) {
|
||||
const onceDispose = once(historyInput.onDispose);
|
||||
onceDispose(() => {
|
||||
this.removeFromHistory(input);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private include(input: IEditorInput | IResourceInput): boolean {
|
||||
if (input instanceof EditorInput) {
|
||||
return true; // include any non files
|
||||
}
|
||||
|
||||
const resourceInput = input as IResourceInput;
|
||||
|
||||
return !this.resourceFilter.matches(resourceInput.resource);
|
||||
}
|
||||
|
||||
protected handleExcludesChange(): void {
|
||||
this.removeExcludedFromHistory();
|
||||
}
|
||||
|
||||
public remove(input: IEditorInput | IResourceInput): void;
|
||||
public remove(input: FileChangesEvent): void;
|
||||
public remove(arg1: IEditorInput | IResourceInput | FileChangesEvent): void {
|
||||
this.removeFromHistory(arg1);
|
||||
this.removeFromStack(arg1);
|
||||
this.removeFromRecentlyClosedFiles(arg1);
|
||||
this.removeFromRecentlyOpened(arg1);
|
||||
}
|
||||
|
||||
private removeExcludedFromHistory(): void {
|
||||
this.ensureHistoryLoaded();
|
||||
|
||||
this.history = this.history.filter(e => this.include(e));
|
||||
}
|
||||
|
||||
private removeFromHistory(arg1: IEditorInput | IResourceInput | FileChangesEvent): void {
|
||||
this.ensureHistoryLoaded();
|
||||
|
||||
this.history = this.history.filter(e => !this.matches(arg1, e));
|
||||
}
|
||||
|
||||
private handleEditorEventInStack(editor: IBaseEditor, event?: ICursorPositionChangedEvent): void {
|
||||
const control = getCodeEditor(editor);
|
||||
|
||||
// treat editor changes that happen as part of stack navigation specially
|
||||
// we do not want to add a new stack entry as a matter of navigating the
|
||||
// stack but we need to keep our currentFileEditorState up to date with
|
||||
// the navigtion that occurs.
|
||||
if (this.navigatingInStack) {
|
||||
if (control && editor.input) {
|
||||
this.currentFileEditorState = new EditorState(editor.input, control.getSelection());
|
||||
} else {
|
||||
this.currentFileEditorState = null; // we navigated to a non file editor
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (control && editor.input) {
|
||||
this.handleTextEditorEvent(editor, control, event);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentFileEditorState = null; // at this time we have no active file editor view state
|
||||
|
||||
if (editor && editor.input) {
|
||||
this.handleNonTextEditorEvent(editor);
|
||||
}
|
||||
}
|
||||
|
||||
private handleTextEditorEvent(editor: IBaseEditor, editorControl: IEditor, event?: ICursorPositionChangedEvent): void {
|
||||
const stateCandidate = new EditorState(editor.input, editorControl.getSelection());
|
||||
if (!this.currentFileEditorState || this.currentFileEditorState.justifiesNewPushState(stateCandidate, event)) {
|
||||
this.currentFileEditorState = stateCandidate;
|
||||
|
||||
let options: ITextEditorOptions;
|
||||
|
||||
const selection = editorControl.getSelection();
|
||||
if (selection) {
|
||||
options = {
|
||||
selection: { startLineNumber: selection.startLineNumber, startColumn: selection.startColumn }
|
||||
};
|
||||
}
|
||||
|
||||
this.add(editor.input, options, true /* from event */);
|
||||
}
|
||||
}
|
||||
|
||||
private handleNonTextEditorEvent(editor: IBaseEditor): void {
|
||||
const currentStack = this.stack[this.index];
|
||||
if (currentStack && this.matches(editor.input, currentStack.input)) {
|
||||
return; // do not push same editor input again
|
||||
}
|
||||
|
||||
this.add(editor.input, void 0, true /* from event */);
|
||||
}
|
||||
|
||||
public add(input: IEditorInput, options?: ITextEditorOptions, fromEvent?: boolean): void {
|
||||
if (!this.navigatingInStack) {
|
||||
this.addToStack(input, options, fromEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private addToStack(input: IEditorInput, options?: ITextEditorOptions, fromEvent?: boolean): void {
|
||||
|
||||
// Overwrite an entry in the stack if we have a matching input that comes
|
||||
// with editor options to indicate that this entry is more specific. Also
|
||||
// prevent entries that have the exact same options. Finally, Overwrite
|
||||
// entries if it came from an event and we detect that the change came in
|
||||
// very fast which indicates that it was not coming in from a user change
|
||||
// but rather rapid programmatic changes. We just take the last of the changes
|
||||
// to not cause too many entries on the stack.
|
||||
let replace = false;
|
||||
if (this.stack[this.index]) {
|
||||
const currentEntry = this.stack[this.index];
|
||||
if (this.matches(input, currentEntry.input) && (this.sameOptions(currentEntry.options, options) || (fromEvent && Date.now() - currentEntry.timestamp < HistoryService.MERGE_EVENT_CHANGES_THRESHOLD))) {
|
||||
replace = true;
|
||||
}
|
||||
}
|
||||
|
||||
const stackInput = this.preferResourceInput(input);
|
||||
const entry = { input: stackInput, options, timestamp: fromEvent ? Date.now() : void 0 };
|
||||
|
||||
// If we are not at the end of history, we remove anything after
|
||||
if (this.stack.length > this.index + 1) {
|
||||
this.stack = this.stack.slice(0, this.index + 1);
|
||||
}
|
||||
|
||||
// Replace at current position
|
||||
if (replace) {
|
||||
this.stack[this.index] = entry;
|
||||
}
|
||||
|
||||
// Add to stack at current position
|
||||
else {
|
||||
this.index++;
|
||||
this.stack.splice(this.index, 0, entry);
|
||||
|
||||
// Check for limit
|
||||
if (this.stack.length > HistoryService.MAX_STACK_ITEMS) {
|
||||
this.stack.shift(); // remove first and dispose
|
||||
if (this.index > 0) {
|
||||
this.index--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove this from the stack unless the stack input is a resource
|
||||
// that can easily be restored even when the input gets disposed
|
||||
if (stackInput instanceof EditorInput) {
|
||||
const onceDispose = once(stackInput.onDispose);
|
||||
onceDispose(() => {
|
||||
this.removeFromStack(input);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private preferResourceInput(input: IEditorInput): IEditorInput | IResourceInput {
|
||||
const file = toResource(input, { filter: 'file' });
|
||||
if (file) {
|
||||
return { resource: file };
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
private sameOptions(optionsA?: ITextEditorOptions, optionsB?: ITextEditorOptions): boolean {
|
||||
if (!optionsA && !optionsB) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((!optionsA && optionsB) || (optionsA && !optionsB)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const s1 = optionsA.selection;
|
||||
const s2 = optionsB.selection;
|
||||
|
||||
if (!s1 && !s2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((!s1 && s2) || (s1 && !s2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return s1.startLineNumber === s2.startLineNumber; // we consider the history entry same if we are on the same line
|
||||
}
|
||||
|
||||
private removeFromStack(arg1: IEditorInput | IResourceInput | FileChangesEvent): void {
|
||||
this.stack = this.stack.filter(e => !this.matches(arg1, e.input));
|
||||
this.index = this.stack.length - 1; // reset index
|
||||
}
|
||||
|
||||
private removeFromRecentlyClosedFiles(arg1: IEditorInput | IResourceInput | FileChangesEvent): void {
|
||||
this.recentlyClosedFiles = this.recentlyClosedFiles.filter(e => !this.matchesFile(e.resource, arg1));
|
||||
}
|
||||
|
||||
private removeFromRecentlyOpened(arg1: IEditorInput | IResourceInput | FileChangesEvent): void {
|
||||
if (arg1 instanceof EditorInput || arg1 instanceof FileChangesEvent) {
|
||||
return; // for now do not delete from file events since recently open are likely out of workspace files for which there are no delete events
|
||||
}
|
||||
|
||||
const input = arg1 as IResourceInput;
|
||||
|
||||
this.windowService.removeFromRecentlyOpened([input.resource.fsPath]);
|
||||
}
|
||||
|
||||
private isFileOpened(resource: URI, group: IEditorGroup): boolean {
|
||||
if (!group) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!group.contains(resource)) {
|
||||
return false; // fast check
|
||||
}
|
||||
|
||||
return group.getEditors().some(e => this.matchesFile(resource, e));
|
||||
}
|
||||
|
||||
private matches(arg1: IEditorInput | IResourceInput | FileChangesEvent, inputB: IEditorInput | IResourceInput): boolean {
|
||||
if (arg1 instanceof FileChangesEvent) {
|
||||
if (inputB instanceof EditorInput) {
|
||||
return false; // we only support this for IResourceInput
|
||||
}
|
||||
|
||||
const resourceInputB = inputB as IResourceInput;
|
||||
|
||||
return arg1.contains(resourceInputB.resource, FileChangeType.DELETED);
|
||||
}
|
||||
|
||||
if (arg1 instanceof EditorInput && inputB instanceof EditorInput) {
|
||||
return arg1.matches(inputB);
|
||||
}
|
||||
|
||||
if (arg1 instanceof EditorInput) {
|
||||
return this.matchesFile((inputB as IResourceInput).resource, arg1);
|
||||
}
|
||||
|
||||
if (inputB instanceof EditorInput) {
|
||||
return this.matchesFile((arg1 as IResourceInput).resource, inputB);
|
||||
}
|
||||
|
||||
const resourceInputA = arg1 as IResourceInput;
|
||||
const resourceInputB = inputB as IResourceInput;
|
||||
|
||||
return resourceInputA && resourceInputB && resourceInputA.resource.toString() === resourceInputB.resource.toString();
|
||||
}
|
||||
|
||||
private matchesFile(resource: URI, arg2: IEditorInput | IResourceInput | FileChangesEvent): boolean {
|
||||
if (arg2 instanceof FileChangesEvent) {
|
||||
return arg2.contains(resource, FileChangeType.DELETED);
|
||||
}
|
||||
|
||||
if (arg2 instanceof EditorInput) {
|
||||
const file = toResource(arg2, { filter: 'file' });
|
||||
|
||||
return file && file.toString() === resource.toString();
|
||||
}
|
||||
|
||||
const resourceInput = arg2 as IResourceInput;
|
||||
|
||||
return resourceInput && resourceInput.resource.toString() === resource.toString();
|
||||
}
|
||||
|
||||
public getHistory(): (IEditorInput | IResourceInput)[] {
|
||||
this.ensureHistoryLoaded();
|
||||
|
||||
return this.history.slice(0);
|
||||
}
|
||||
|
||||
private ensureHistoryLoaded(): void {
|
||||
if (!this.loaded) {
|
||||
this.loadHistory();
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
private save(): void {
|
||||
if (!this.history) {
|
||||
return; // nothing to save because history was not used
|
||||
}
|
||||
|
||||
const entries: ISerializedFileHistoryEntry[] = this.history.map(input => {
|
||||
if (input instanceof EditorInput) {
|
||||
return void 0; // only file resource inputs are serializable currently
|
||||
}
|
||||
|
||||
return { resourceJSON: (input as IResourceInput).resource.toJSON() };
|
||||
}).filter(serialized => !!serialized);
|
||||
|
||||
this.storageService.store(HistoryService.STORAGE_KEY, JSON.stringify(entries), StorageScope.WORKSPACE);
|
||||
}
|
||||
|
||||
private loadHistory(): void {
|
||||
let entries: ISerializedFileHistoryEntry[] = [];
|
||||
|
||||
const entriesRaw = this.storageService.get(HistoryService.STORAGE_KEY, StorageScope.WORKSPACE);
|
||||
if (entriesRaw) {
|
||||
entries = JSON.parse(entriesRaw);
|
||||
}
|
||||
|
||||
this.history = entries.map(entry => {
|
||||
const serializedFileInput = entry as ISerializedFileHistoryEntry;
|
||||
if (serializedFileInput.resource || serializedFileInput.resourceJSON) {
|
||||
return { resource: !!serializedFileInput.resourceJSON ? URI.revive(serializedFileInput.resourceJSON) : URI.parse(serializedFileInput.resource) } as IResourceInput;
|
||||
}
|
||||
|
||||
return void 0;
|
||||
}).filter(input => !!input);
|
||||
}
|
||||
|
||||
public getLastActiveWorkspaceRoot(): URI {
|
||||
if (!this.contextService.hasWorkspace()) {
|
||||
return void 0;
|
||||
}
|
||||
|
||||
const history = this.getHistory();
|
||||
for (let i = 0; i < history.length; i++) {
|
||||
const input = history[i];
|
||||
if (input instanceof EditorInput) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const resourceInput = input as IResourceInput;
|
||||
const resourceWorkspace = this.contextService.getRoot(resourceInput.resource);
|
||||
if (resourceWorkspace) {
|
||||
return resourceWorkspace;
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to first workspace
|
||||
return this.contextService.getWorkspace().roots[0];
|
||||
}
|
||||
}
|
||||
63
src/vs/workbench/services/history/common/history.ts
Normal file
63
src/vs/workbench/services/history/common/history.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IEditorInput, ITextEditorOptions, IResourceInput } from 'vs/platform/editor/common/editor';
|
||||
import URI from 'vs/base/common/uri';
|
||||
|
||||
export const IHistoryService = createDecorator<IHistoryService>('historyService');
|
||||
|
||||
export interface IHistoryService {
|
||||
|
||||
_serviceBrand: ServiceIdentifier<any>;
|
||||
|
||||
/**
|
||||
* Re-opens the last closed editor if any.
|
||||
*/
|
||||
reopenLastClosedEditor(): void;
|
||||
|
||||
/**
|
||||
* Add an entry to the navigation stack of the history.
|
||||
*/
|
||||
add(input: IEditorInput, options?: ITextEditorOptions): void;
|
||||
|
||||
/**
|
||||
* Navigate forwards in history.
|
||||
*
|
||||
* @param acrossEditors instructs the history to skip navigation entries that
|
||||
* are only within the same document.
|
||||
*/
|
||||
forward(acrossEditors?: boolean): void;
|
||||
|
||||
/**
|
||||
* Navigate backwards in history.
|
||||
*
|
||||
* @param acrossEditors instructs the history to skip navigation entries that
|
||||
* are only within the same document.
|
||||
*/
|
||||
back(acrossEditors?: boolean): void;
|
||||
|
||||
/**
|
||||
* Removes an entry from history.
|
||||
*/
|
||||
remove(input: IEditorInput | IResourceInput): void;
|
||||
|
||||
/**
|
||||
* Clears all history.
|
||||
*/
|
||||
clear(): void;
|
||||
|
||||
/**
|
||||
* Get the entire history of opened editors.
|
||||
*/
|
||||
getHistory(): (IEditorInput | IResourceInput)[];
|
||||
|
||||
/**
|
||||
* Looking at the editor history, returns the workspace root of the last file that was
|
||||
* inside the workspace and part of the editor history.
|
||||
*/
|
||||
getLastActiveWorkspaceRoot(): URI;
|
||||
}
|
||||
Reference in New Issue
Block a user