From 77b351adf360bf47314cda731b08437958cc17f9 Mon Sep 17 00:00:00 2001 From: Anthony Dresser Date: Thu, 20 Jun 2019 16:10:00 -0700 Subject: [PATCH] Add query editor view state (#6018) * add query editor view state * change commnet * change state key name * wip * fix tests --- .../parts/query/browser/queryEditor.ts | 108 +++++++++++++++--- .../parts/query/editor/queryActions.test.ts | 18 +-- 2 files changed, 101 insertions(+), 25 deletions(-) diff --git a/src/sql/workbench/parts/query/browser/queryEditor.ts b/src/sql/workbench/parts/query/browser/queryEditor.ts index 4309ccb39c..a08488ea52 100644 --- a/src/sql/workbench/parts/query/browser/queryEditor.ts +++ b/src/sql/workbench/parts/query/browser/queryEditor.ts @@ -6,8 +6,8 @@ import 'vs/css!./media/queryEditor'; import * as DOM from 'vs/base/browser/dom'; -import { EditorOptions, IEditorControl } from 'vs/workbench/common/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorOptions, IEditorControl, IEditorMemento, IEditorCloseEvent } from 'vs/workbench/common/editor'; +import { BaseEditor, EditorMemento } from 'vs/workbench/browser/parts/editor/baseEditor'; import { Orientation } from 'vs/base/browser/ui/sash/sash'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -19,21 +19,29 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { CancellationToken } from 'vs/base/common/cancellation'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { SplitView, Sizing } from 'vs/base/browser/ui/splitview/splitview'; import { Event } from 'vs/base/common/event'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { ISelectionData } from 'azdata'; import { Action, IActionViewItem } from 'vs/base/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; +import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { URI } from 'vs/base/common/uri'; +import { IFileService, FileChangesEvent } from 'vs/platform/files/common/files'; import { QueryInput, IQueryEditorStateChange } from 'sql/workbench/parts/query/common/queryInput'; import { QueryResultsEditor } from 'sql/workbench/parts/query/browser/queryResultsEditor'; import * as queryContext from 'sql/workbench/parts/query/common/queryContext'; import { Taskbar, ITaskbarContent } from 'sql/base/browser/ui/taskbar/taskbar'; import * as actions from 'sql/workbench/parts/query/browser/queryActions'; -import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; + +const QUERY_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'queryEditorViewState'; + +interface IQueryEditorViewState { + resultsHeight: number | undefined; +} /** * Editor that hosts 2 sub-editors: A TextResourceEditor for SQL file editing, and a QueryResultsEditor @@ -66,6 +74,8 @@ export class QueryEditor extends BaseEditor { private queryEditorVisible: IContextKey; + private editorMemento: IEditorMemento; + //actions private _runQueryAction: actions.RunQueryAction; private _cancelQueryAction: actions.CancelQueryAction; @@ -81,13 +91,31 @@ export class QueryEditor extends BaseEditor { @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @IContextKeyService contextKeyService: IContextKeyService, + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IFileService fileService: IFileService, @IEditorService private readonly editorService: IEditorService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IConfigurationService private readonly configurationService: IConfigurationService ) { super(QueryEditor.ID, telemetryService, themeService, storageService); + this.editorMemento = this.getEditorMemento(editorGroupService, QUERY_EDITOR_VIEW_STATE_PREFERENCE_KEY, 100); + this.queryEditorVisible = queryContext.QueryEditorVisibleContext.bindTo(contextKeyService); + + // Clear view state for deleted files + this._register(fileService.onFileChanges(e => this.onFilesChanged(e))); + } + + private onFilesChanged(e: FileChangesEvent): void { + const deleted = e.getDeleted(); + if (deleted && deleted.length) { + this.clearTextEditorViewState(deleted.map(d => d.resource)); + } + } + + protected getEditorMemento(editorGroupService: IEditorGroupsService, key: string, limit: number = 10): IEditorMemento { + return new EditorMemento(this.getId(), key, Object.create(null), limit, editorGroupService); // do not persist in storage as results are never persisted } // PUBLIC METHODS //////////////////////////////////////////////////////////// @@ -251,6 +279,8 @@ export class QueryEditor extends BaseEditor { } if (oldInput) { + // Remember view settings if input changes + this.saveQueryEditorViewState(this.input); this.currentTextEditor.clearInput(); this.resultsEditor.clearInput(); } @@ -280,9 +310,59 @@ export class QueryEditor extends BaseEditor { this.inputDisposables = []; this.inputDisposables.push(this.input.state.onChange(c => this.updateState(c))); this.updateState({ connectingChange: true, connectedChange: true, executingChange: true, resultsVisibleChange: true }); + + const editorViewState = this.loadTextEditorViewState(this.input.getResource()); + + if (editorViewState && editorViewState.resultsHeight) { + this.splitview.resizeView(1, editorViewState.resultsHeight); + } } - public toggleResultsEditorVisibility() { + private saveQueryEditorViewState(input: QueryInput): void { + if (!input) { + return; // ensure we have an input to handle view state for + } + + // Otherwise we save the view state to restore it later + else if (!input.isDisposed()) { + this.saveTextEditorViewState(input.getResource()); + } + } + + private clearTextEditorViewState(resources: URI[], group?: IEditorGroup): void { + resources.forEach(resource => { + this.editorMemento.clearEditorState(resource, group); + }); + } + + private saveTextEditorViewState(resource: URI): void { + const editorViewState = { + resultsHeight: this.resultsVisible ? this.splitview.getViewSize(1) : undefined + } as IQueryEditorViewState; + + if (!this.group) { + return; + } + + this.editorMemento.saveEditorState(this.group, resource, editorViewState); + } + + /** + * Loads the text editor view state for the given resource and returns it. + */ + protected loadTextEditorViewState(resource: URI): IQueryEditorViewState | undefined { + return this.group ? this.editorMemento.loadEditorState(this.group, resource) : undefined; + } + + protected saveState(): void { + + // Update/clear editor view State + this.saveQueryEditorViewState(this.input); + + super.saveState(); + } + + public toggleResultsEditorVisibility(): void { if (this.resultsVisible) { this.removeResultsEditor(); } else { @@ -322,12 +402,14 @@ export class QueryEditor extends BaseEditor { } } - /** * Called to indicate to the editor that the input should be cleared and resources associated with the * input should be freed. */ public clearInput(): void { + + this.saveQueryEditorViewState(this.input); + this.currentTextEditor.clearInput(); this.resultsEditor.clearInput(); super.clearInput(); @@ -362,7 +444,7 @@ export class QueryEditor extends BaseEditor { this.currentTextEditor.setOptions(options); } - private removeResultsEditor() { + private removeResultsEditor(): void { if (this.resultsVisible) { this.splitview.removeView(1, Sizing.Distribute); this.resultsVisible = false; @@ -372,7 +454,7 @@ export class QueryEditor extends BaseEditor { } } - private addResultsEditor() { + private addResultsEditor(): void { if (!this.resultsVisible) { // size the results section to 65% of available height or at least 100px let initialViewSize = Math.round(Math.max(this.dimension.height * 0.65, 100)); @@ -390,12 +472,6 @@ export class QueryEditor extends BaseEditor { } } - public close(): void { - let queryInput: QueryInput = this.input; - queryInput.sql.close(); - queryInput.results.close(); - } - // helper functions public isSelectionEmpty(): boolean { @@ -525,7 +601,7 @@ export class QueryEditor extends BaseEditor { this.resultsEditor.registerQueryModelViewTab(title, componentId); } - public chart(dataId: { batchId: number, resultId: number }) { + public chart(dataId: { batchId: number, resultId: number }): void { this.resultsEditor.chart(dataId); } } diff --git a/src/sqltest/parts/query/editor/queryActions.test.ts b/src/sqltest/parts/query/editor/queryActions.test.ts index 4a864abf58..bd2c11fbaa 100644 --- a/src/sqltest/parts/query/editor/queryActions.test.ts +++ b/src/sqltest/parts/query/editor/queryActions.test.ts @@ -30,7 +30,7 @@ import { ConfigurationService } from 'vs/platform/configuration/node/configurati import * as TypeMoq from 'typemoq'; import * as assert from 'assert'; -import { TestStorageService, TestContextService } from 'vs/workbench/test/workbenchTestServices'; +import { TestStorageService, TestFileService } from 'vs/workbench/test/workbenchTestServices'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; let none: void; @@ -52,8 +52,8 @@ suite('SQL QueryAction Tests', () => { const contextkeyservice = new MockContextKeyService(); // Setup a reusable mock QueryEditor - editor = TypeMoq.Mock.ofType(QueryEditor, TypeMoq.MockBehavior.Strict, undefined, new TestThemeService(), new TestStorageService(), contextkeyservice, undefined, undefined, - undefined); + editor = TypeMoq.Mock.ofType(QueryEditor, TypeMoq.MockBehavior.Strict, undefined, new TestThemeService(), + new TestStorageService(), contextkeyservice, undefined, new TestFileService(), undefined); editor.setup(x => x.input).returns(() => testQueryInput.object); editor.setup(x => x.getSelection()).returns(() => undefined); @@ -89,8 +89,8 @@ suite('SQL QueryAction Tests', () => { const contextkeyservice = new MockContextKeyService(); // Setup a reusable mock QueryEditor - editor = TypeMoq.Mock.ofType(QueryEditor, TypeMoq.MockBehavior.Strict, undefined, new TestThemeService(), new TestStorageService(), contextkeyservice, undefined, undefined, - undefined); + editor = TypeMoq.Mock.ofType(QueryEditor, TypeMoq.MockBehavior.Strict, undefined, new TestThemeService(), + new TestStorageService(), contextkeyservice, undefined, new TestFileService(), undefined); editor.setup(x => x.input).returns(() => testQueryInput.object); // If I create a QueryTaskbarAction and I pass a non-connected editor to _getConnectedQueryEditorUri @@ -180,8 +180,8 @@ suite('SQL QueryAction Tests', () => { const contextkeyservice = new MockContextKeyService(); // Setup a reusable mock QueryEditor - let queryEditor = TypeMoq.Mock.ofType(QueryEditor, TypeMoq.MockBehavior.Strict, undefined, new TestThemeService(), new TestStorageService(), contextkeyservice, undefined, undefined, - undefined); + let queryEditor = TypeMoq.Mock.ofType(QueryEditor, TypeMoq.MockBehavior.Strict, undefined, new TestThemeService(), + new TestStorageService(), contextkeyservice, undefined, new TestFileService(), undefined); queryEditor.setup(x => x.input).returns(() => queryInput.object); queryEditor.setup(x => x.getSelection()).returns(() => undefined); queryEditor.setup(x => x.getSelection(false)).returns(() => undefined); @@ -248,8 +248,8 @@ suite('SQL QueryAction Tests', () => { const contextkeyservice = new MockContextKeyService(); // Setup a reusable mock QueryEditor - let queryEditor = TypeMoq.Mock.ofType(QueryEditor, TypeMoq.MockBehavior.Strict, undefined, new TestThemeService(), new TestStorageService(), contextkeyservice, undefined, undefined, - undefined); + let queryEditor = TypeMoq.Mock.ofType(QueryEditor, TypeMoq.MockBehavior.Strict, undefined, new TestThemeService(), + new TestStorageService(), contextkeyservice, undefined, new TestFileService(), undefined); queryEditor.setup(x => x.input).returns(() => queryInput.object); queryEditor.setup(x => x.isSelectionEmpty()).returns(() => false); queryEditor.setup(x => x.getSelection()).returns(() => {