SQL Operations Studio Public Preview 1 (0.23) release source code

This commit is contained in:
Karl Burtram
2017-11-09 14:30:27 -08:00
parent b88ecb8d93
commit 3cdac41339
8829 changed files with 759707 additions and 286 deletions

View 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];
}
}

View 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;
}