mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-18 17:22:45 -05:00
971 lines
32 KiB
TypeScript
971 lines
32 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import 'vs/css!sql/parts/query/editor/media/queryEditor';
|
|
import { TPromise } from 'vs/base/common/winjs.base';
|
|
import * as strings from 'vs/base/common/strings';
|
|
import * as DOM from 'vs/base/browser/dom';
|
|
import * as types from 'vs/base/common/types';
|
|
|
|
import { EditorInput, EditorOptions, IEditorControl, IEditor, TextEditorOptions } from 'vs/workbench/common/editor';
|
|
import * as editorCommon from 'vs/editor/common/editorCommon';
|
|
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
|
|
import { VerticalFlexibleSash, HorizontalFlexibleSash, IFlexibleSash } from 'sql/parts/query/views/flexibleSash';
|
|
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';
|
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
|
|
|
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
|
|
import { TextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor';
|
|
|
|
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
|
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
|
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
|
import { IActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
|
|
import { Action } from 'vs/base/common/actions';
|
|
import { ISelectionData } from 'sqlops';
|
|
import { IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService';
|
|
import { IDisposable } from 'vs/base/common/lifecycle';
|
|
import { IRange } from 'vs/editor/common/core/range';
|
|
|
|
import { QueryResultsInput } from 'sql/parts/query/common/queryResultsInput';
|
|
import { QueryInput } from 'sql/parts/query/common/queryInput';
|
|
import { QueryResultsEditor } from 'sql/parts/query/editor/queryResultsEditor';
|
|
import * as queryContext from 'sql/parts/query/common/queryContext';
|
|
import { Taskbar, ITaskbarContent } from 'sql/base/browser/ui/taskbar/taskbar';
|
|
import {
|
|
RunQueryAction, CancelQueryAction, ListDatabasesAction, ListDatabasesActionItem,
|
|
ConnectDatabaseAction, ToggleConnectDatabaseAction, EstimatedQueryPlanAction,
|
|
ActualQueryPlanAction
|
|
} from 'sql/parts/query/execution/queryActions';
|
|
import { IQueryModelService } from 'sql/parts/query/execution/queryModel';
|
|
import { IEditorDescriptorService } from 'sql/parts/query/editor/editorDescriptorService';
|
|
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
|
|
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
|
import { CancellationToken } from 'vs/base/common/cancellation';
|
|
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
|
|
|
/**
|
|
* Editor that hosts 2 sub-editors: A TextResourceEditor for SQL file editing, and a QueryResultsEditor
|
|
* for viewing and editing query results. This editor is based off SideBySideEditor.
|
|
*/
|
|
export class QueryEditor extends BaseEditor {
|
|
|
|
public static ID: string = 'workbench.editor.queryEditor';
|
|
|
|
// The height of the tabs above the editor
|
|
private readonly _tabHeight: number = 35;
|
|
|
|
// The minimum width/height of the editors hosted in the QueryEditor
|
|
private readonly _minEditorSize: number = 220;
|
|
|
|
private _sash: IFlexibleSash;
|
|
private _editorTopOffset: number;
|
|
private _orientation: Orientation;
|
|
private _dimension: DOM.Dimension;
|
|
|
|
private _resultsEditor: QueryResultsEditor;
|
|
private _resultsEditorContainer: HTMLElement;
|
|
|
|
private _sqlEditor: TextResourceEditor;
|
|
private _sqlEditorContainer: HTMLElement;
|
|
|
|
private _taskbar: Taskbar;
|
|
private _taskbarContainer: HTMLElement;
|
|
private _listDatabasesActionItem: ListDatabasesActionItem;
|
|
|
|
|
|
private queryEditorVisible: IContextKey<boolean>;
|
|
|
|
private _runQueryAction: RunQueryAction;
|
|
private _cancelQueryAction: CancelQueryAction;
|
|
private _toggleConnectDatabaseAction: ToggleConnectDatabaseAction;
|
|
private _changeConnectionAction: ConnectDatabaseAction;
|
|
private _listDatabasesAction: ListDatabasesAction;
|
|
private _estimatedQueryPlanAction: EstimatedQueryPlanAction;
|
|
private _actualQueryPlanAction: ActualQueryPlanAction;
|
|
|
|
constructor(
|
|
@ITelemetryService _telemetryService: ITelemetryService,
|
|
@IThemeService themeService: IThemeService,
|
|
@IInstantiationService private _instantiationService: IInstantiationService,
|
|
@IEditorService private _editorService: IEditorService,
|
|
@IContextMenuService private _contextMenuService: IContextMenuService,
|
|
@IQueryModelService private _queryModelService: IQueryModelService,
|
|
@IEditorDescriptorService private _editorDescriptorService: IEditorDescriptorService,
|
|
@IContextKeyService contextKeyService: IContextKeyService,
|
|
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
|
@IConfigurationService private _configurationService: IConfigurationService
|
|
) {
|
|
super(QueryEditor.ID, _telemetryService, themeService);
|
|
|
|
this._orientation = Orientation.HORIZONTAL;
|
|
|
|
if (contextKeyService) {
|
|
this.queryEditorVisible = queryContext.QueryEditorVisibleContext.bindTo(contextKeyService);
|
|
}
|
|
|
|
if (_editorService) {
|
|
_editorService.overrideOpenEditor((editor, options, group) => {
|
|
if (this.isVisible() && (editor !== this.input || group !== this.group)) {
|
|
this.saveEditorViewState();
|
|
}
|
|
return {};
|
|
});
|
|
}
|
|
}
|
|
|
|
// PROPERTIES //////////////////////////////////////////////////////////
|
|
/**
|
|
* Returns the URI of this editor if it is connected.
|
|
* @returns {string} URI of the editor if connected, undefined otherwise
|
|
*/
|
|
public get connectedUri(): string {
|
|
return this._connectionManagementService.isConnected(this.uri)
|
|
? this.uri
|
|
: undefined;
|
|
}
|
|
|
|
/**
|
|
* Returns the URI of this editor if an input is associated with it
|
|
* @return {string} URI of this if input is associated, undefined otherwise
|
|
*/
|
|
get uri(): string {
|
|
let input: QueryInput = <QueryInput>this.input;
|
|
return input
|
|
? input.getQueryResultsInputResource()
|
|
: undefined;
|
|
}
|
|
|
|
// PUBLIC METHODS ////////////////////////////////////////////////////////////
|
|
public get currentQueryInput(): QueryInput {
|
|
return <QueryInput>this.input;
|
|
}
|
|
|
|
/**
|
|
* Called to create the editor in the parent element.
|
|
*/
|
|
public createEditor(parent: HTMLElement): void {
|
|
const parentElement = parent;
|
|
DOM.addClass(parentElement, 'side-by-side-editor');
|
|
this._createTaskbar(parentElement);
|
|
}
|
|
|
|
/**
|
|
* Sets the input data for this editor.
|
|
*/
|
|
public setInput(newInput: QueryInput, options?: EditorOptions): Thenable<void> {
|
|
const oldInput = <QueryInput>this.input;
|
|
|
|
if (newInput.matches(oldInput)) {
|
|
return TPromise.as(undefined);
|
|
}
|
|
|
|
// Make sure all event callbacks will be sent to this QueryEditor in the case that this QueryInput was moved from
|
|
// another QueryEditor
|
|
let taskbarCallback: IDisposable = newInput.updateTaskbarEvent(() => this._updateTaskbar());
|
|
let showResultsCallback: IDisposable = newInput.showQueryResultsEditorEvent(() => this._showQueryResultsEditor());
|
|
let selectionCallback: IDisposable = newInput.updateSelectionEvent((selection) => this._setSelection(selection));
|
|
newInput.setEventCallbacks([taskbarCallback, showResultsCallback, selectionCallback]);
|
|
|
|
return super.setInput(newInput, options, CancellationToken.None)
|
|
.then(() => this._updateInput(oldInput, newInput, options));
|
|
}
|
|
|
|
/**
|
|
* Sets this editor and the 2 sub-editors to visible.
|
|
*/
|
|
public setEditorVisible(visible: boolean, group: IEditorGroup): void {
|
|
if (this._resultsEditor) {
|
|
this._resultsEditor.setVisible(visible, group);
|
|
}
|
|
if (this._sqlEditor) {
|
|
this._sqlEditor.setVisible(visible, group);
|
|
}
|
|
super.setEditorVisible(visible, group);
|
|
|
|
// Note: must update after calling super.setEditorVisible so that the accurate count is handled
|
|
this.updateQueryEditorVisible(visible);
|
|
}
|
|
|
|
|
|
private updateQueryEditorVisible(currentEditorIsVisible: boolean): void {
|
|
if (this.queryEditorVisible) {
|
|
let visible = currentEditorIsVisible;
|
|
if (!currentEditorIsVisible) {
|
|
// Current editor is closing but still tracked as visible. Check if any other editor is visible
|
|
const candidates = [...this._editorService.visibleControls].filter(e => {
|
|
if (e && e.getId) {
|
|
return e.getId() === QueryEditor.ID;
|
|
}
|
|
return false;
|
|
});
|
|
// Note: require 2 or more candidates since current is closing but still
|
|
// counted as visible
|
|
visible = candidates.length > 1;
|
|
}
|
|
this.queryEditorVisible.set(visible);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called to indicate to the editor that the input should be cleared and resources associated with the
|
|
* input should be freed.
|
|
*/
|
|
public clearInput(): void {
|
|
if (this._resultsEditor) {
|
|
this._resultsEditor.clearInput();
|
|
}
|
|
if (this._sqlEditor) {
|
|
this._sqlEditor.clearInput();
|
|
}
|
|
this._disposeEditors();
|
|
super.clearInput();
|
|
}
|
|
|
|
/**
|
|
* Sets focus on this editor. Specifically, it sets the focus on the hosted text editor.
|
|
*/
|
|
public focus(): void {
|
|
if (this._sqlEditor) {
|
|
this._sqlEditor.focus();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the internal variable keeping track of the editor's size, and re-calculates the sash position.
|
|
* To be called when the container of this editor changes size.
|
|
*/
|
|
public layout(dimension: DOM.Dimension): void {
|
|
this._dimension = dimension;
|
|
|
|
if (this._sash) {
|
|
this._setSashDimension();
|
|
this.sash.layout();
|
|
}
|
|
|
|
this._doLayout();
|
|
this._resizeGridContents();
|
|
}
|
|
|
|
/**
|
|
* Returns the editor control for the text editor.
|
|
*/
|
|
public getControl(): IEditorControl {
|
|
if (this._sqlEditor) {
|
|
return this._sqlEditor.getControl();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public getQueryResultsEditor(): QueryResultsEditor {
|
|
return this._resultsEditor;
|
|
}
|
|
|
|
public getSqlEditor(): TextResourceEditor {
|
|
return this._sqlEditor;
|
|
}
|
|
|
|
public dispose(): void {
|
|
this._disposeEditors();
|
|
super.dispose();
|
|
}
|
|
|
|
public close(): void {
|
|
let queryInput: QueryInput = <QueryInput>this.input;
|
|
queryInput.sql.close();
|
|
queryInput.results.close();
|
|
}
|
|
|
|
/**
|
|
* Makes visible the QueryResultsEditor for the current QueryInput (if it is not
|
|
* already visible).
|
|
*/
|
|
public _showQueryResultsEditor(): void {
|
|
if (this._isResultsEditorVisible()) {
|
|
return;
|
|
}
|
|
|
|
const activeControl = this._editorService.activeControl;
|
|
activeControl.group.pinEditor(activeControl.input);
|
|
|
|
let input = <QueryInput>this.input;
|
|
this._createResultsEditorContainer();
|
|
|
|
this._createEditor(<QueryResultsInput>input.results, this._resultsEditorContainer, this.group)
|
|
.then(result => {
|
|
this._onResultsEditorCreated(<any>result, input.results, this.options);
|
|
this.resultsEditorVisibility = true;
|
|
this.hideQueryResultsView = false;
|
|
this._doLayout(true);
|
|
});
|
|
}
|
|
|
|
private hideQueryResultsView = false;
|
|
|
|
/**
|
|
* Toggle the visibility of the view state of results
|
|
*/
|
|
public toggleResultsEditorVisibility(): void {
|
|
let input = <QueryInput>this.input;
|
|
let hideResults = this.hideQueryResultsView;
|
|
this.hideQueryResultsView = !this.hideQueryResultsView;
|
|
if (!input.results) {
|
|
return;
|
|
}
|
|
this.resultsEditorVisibility = hideResults;
|
|
this._doLayout();
|
|
}
|
|
|
|
/**
|
|
* Returns the underlying SQL editor's text selection in a 0-indexed format. Returns undefined if there
|
|
* is no selected text.
|
|
*/
|
|
public getSelection(checkIfRange: boolean = true): ISelectionData {
|
|
if (this._sqlEditor && this._sqlEditor.getControl()) {
|
|
let vscodeSelection = this._sqlEditor.getControl().getSelection();
|
|
|
|
// If the selection is a range of characters rather than just a cursor position, return the range
|
|
let isRange: boolean =
|
|
!(vscodeSelection.getStartPosition().lineNumber === vscodeSelection.getEndPosition().lineNumber &&
|
|
vscodeSelection.getStartPosition().column === vscodeSelection.getEndPosition().column);
|
|
if (!checkIfRange || isRange) {
|
|
let sqlToolsServiceSelection: ISelectionData = {
|
|
startLine: vscodeSelection.getStartPosition().lineNumber - 1,
|
|
startColumn: vscodeSelection.getStartPosition().column - 1,
|
|
endLine: vscodeSelection.getEndPosition().lineNumber - 1,
|
|
endColumn: vscodeSelection.getEndPosition().column - 1,
|
|
};
|
|
return sqlToolsServiceSelection;
|
|
}
|
|
}
|
|
|
|
// Otherwise return undefined because there is no selected text
|
|
return undefined;
|
|
}
|
|
|
|
public isSelectionEmpty(): boolean {
|
|
if (this._sqlEditor && this._sqlEditor.getControl()) {
|
|
let control = this._sqlEditor.getControl();
|
|
let codeEditor: ICodeEditor = <ICodeEditor>control;
|
|
|
|
if (codeEditor) {
|
|
let value = codeEditor.getValue();
|
|
if (value !== undefined && value.length > 0) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public getAllText(): string {
|
|
if (this._sqlEditor && this._sqlEditor.getControl()) {
|
|
let control = this._sqlEditor.getControl();
|
|
let codeEditor: ICodeEditor = <ICodeEditor>control;
|
|
if (codeEditor) {
|
|
let value = codeEditor.getValue();
|
|
if (value !== undefined && value.length > 0) {
|
|
return value;
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
public getAllSelection(): ISelectionData {
|
|
if (this._sqlEditor && this._sqlEditor.getControl()) {
|
|
let control = this._sqlEditor.getControl();
|
|
let codeEditor: ICodeEditor = <ICodeEditor>control;
|
|
if (codeEditor) {
|
|
let model = codeEditor.getModel();
|
|
let totalLines = model.getLineCount();
|
|
let endColumn = model.getLineMaxColumn(totalLines);
|
|
let selection: ISelectionData = {
|
|
startLine: 0,
|
|
startColumn: 0,
|
|
endLine: totalLines - 1,
|
|
endColumn: endColumn - 1,
|
|
};
|
|
return selection;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
public getSelectionText(): string {
|
|
if (this._sqlEditor && this._sqlEditor.getControl()) {
|
|
let control = this._sqlEditor.getControl();
|
|
let codeEditor: ICodeEditor = <ICodeEditor>control;
|
|
let vscodeSelection = control.getSelection();
|
|
|
|
if (codeEditor && vscodeSelection) {
|
|
let model = codeEditor.getModel();
|
|
let value = model.getValueInRange(vscodeSelection);
|
|
if (value !== undefined && value.length > 0) {
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Calls the run method of this editor's RunQueryAction
|
|
*/
|
|
public runQuery(): void {
|
|
this._runQueryAction.run();
|
|
}
|
|
|
|
/**
|
|
* Calls the runCurrent method of this editor's RunQueryAction
|
|
*/
|
|
public runCurrentQuery(): void {
|
|
this._runQueryAction.runCurrent();
|
|
}
|
|
|
|
/**
|
|
* Calls the runCurrentQueryWithActualPlan method of this editor's ActualQueryPlanAction
|
|
*/
|
|
public runCurrentQueryWithActualPlan(): void {
|
|
this._actualQueryPlanAction.run();
|
|
}
|
|
|
|
/**
|
|
* Calls the run method of this editor's CancelQueryAction
|
|
*/
|
|
public cancelQuery(): void {
|
|
this._cancelQueryAction.run();
|
|
}
|
|
|
|
public rebuildIntelliSenseCache(): void {
|
|
this._connectionManagementService.rebuildIntelliSenseCache(this.connectedUri);
|
|
}
|
|
|
|
public setOptions(options: EditorOptions): void {
|
|
const textOptions = <TextEditorOptions>options;
|
|
if (textOptions && types.isFunction(textOptions.apply)) {
|
|
textOptions.apply(this.getControl() as editorCommon.IEditor, editorCommon.ScrollType.Smooth);
|
|
}
|
|
}
|
|
|
|
// PRIVATE METHODS ////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* Creates the query execution taskbar that appears at the top of the QueryEditor
|
|
*/
|
|
private _createTaskbar(parentElement: HTMLElement): void {
|
|
// Create QueryTaskbar
|
|
this._taskbarContainer = DOM.append(parentElement, DOM.$('div'));
|
|
this._taskbar = new Taskbar(this._taskbarContainer, this._contextMenuService, {
|
|
actionItemProvider: (action: Action) => this._getActionItemForAction(action),
|
|
});
|
|
|
|
// Create Actions for the toolbar
|
|
this._runQueryAction = this._instantiationService.createInstance(RunQueryAction, this);
|
|
this._cancelQueryAction = this._instantiationService.createInstance(CancelQueryAction, this);
|
|
this._toggleConnectDatabaseAction = this._instantiationService.createInstance(ToggleConnectDatabaseAction, this, false);
|
|
this._changeConnectionAction = this._instantiationService.createInstance(ConnectDatabaseAction, this, true);
|
|
this._listDatabasesAction = this._instantiationService.createInstance(ListDatabasesAction, this);
|
|
this._estimatedQueryPlanAction = this._instantiationService.createInstance(EstimatedQueryPlanAction, this);
|
|
this._actualQueryPlanAction = this._instantiationService.createInstance(ActualQueryPlanAction, this);
|
|
|
|
this.setTaskbarContent();
|
|
|
|
this._configurationService.onDidChangeConfiguration(e => {
|
|
if (e.affectedKeys.includes('workbench.enablePreviewFeatures')) {
|
|
this.setTaskbarContent();
|
|
}
|
|
});
|
|
}
|
|
|
|
private setTaskbarContent(): void {
|
|
// Create HTML Elements for the taskbar
|
|
let separator = Taskbar.createTaskbarSeparator();
|
|
|
|
// Set the content in the order we desire
|
|
let content: ITaskbarContent[] = [
|
|
{ action: this._runQueryAction },
|
|
{ action: this._cancelQueryAction },
|
|
{ element: separator },
|
|
{ action: this._toggleConnectDatabaseAction },
|
|
{ action: this._changeConnectionAction },
|
|
{ action: this._listDatabasesAction },
|
|
{ element: separator },
|
|
{ action: this._estimatedQueryPlanAction }
|
|
];
|
|
|
|
// Remove the estimated query plan action if preview features are not enabled
|
|
let previewFeaturesEnabled = this._configurationService.getValue('workbench')['enablePreviewFeatures'];
|
|
if (!previewFeaturesEnabled) {
|
|
content = content.slice(0, -2);
|
|
}
|
|
|
|
this._taskbar.setContent(content);
|
|
}
|
|
|
|
/**
|
|
* Gets the IActionItem for the List Databases dropdown if provided the associated Action.
|
|
* Otherwise returns null.
|
|
*/
|
|
private _getActionItemForAction(action: Action): IActionItem {
|
|
if (action.id === ListDatabasesAction.ID) {
|
|
return this.listDatabasesActionItem;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Public for testing purposes only
|
|
*/
|
|
public get listDatabasesActionItem(): ListDatabasesActionItem {
|
|
if (!this._listDatabasesActionItem) {
|
|
this._listDatabasesActionItem = this._instantiationService.createInstance(ListDatabasesActionItem, this, this._listDatabasesAction);
|
|
this._register(this._listDatabasesActionItem.attachStyler(this.themeService));
|
|
}
|
|
return this._listDatabasesActionItem;
|
|
}
|
|
|
|
/**
|
|
* Handles setting input for this editor.
|
|
*/
|
|
private _updateInput(oldInput: QueryInput, newInput: QueryInput, options?: EditorOptions): TPromise<void> {
|
|
if (this._sqlEditor) {
|
|
this._sqlEditor.clearInput();
|
|
}
|
|
|
|
if (oldInput) {
|
|
this._disposeEditors();
|
|
}
|
|
|
|
this._createSqlEditorContainer();
|
|
if (this._isResultsEditorVisible()) {
|
|
this._createResultsEditorContainer();
|
|
|
|
let uri: string = newInput.getQueryResultsInputResource();
|
|
if (uri) {
|
|
this._queryModelService.refreshResultsets(uri);
|
|
}
|
|
}
|
|
|
|
if (this._sash) {
|
|
if (this._isResultsEditorVisible()) {
|
|
this._sash.show();
|
|
} else {
|
|
this._sash.hide();
|
|
}
|
|
}
|
|
|
|
this._updateTaskbar();
|
|
return this._setNewInput(newInput, options);
|
|
}
|
|
|
|
/**
|
|
* Handles setting input and creating editors when this QueryEditor is either:
|
|
* - Opened for the first time
|
|
* - Opened with a new QueryInput
|
|
* This will create only the SQL editor if the results editor does not yet exist for the
|
|
* given QueryInput.
|
|
*/
|
|
private _setNewInput(newInput: QueryInput, options?: EditorOptions): TPromise<any> {
|
|
|
|
// Promises that will ensure proper ordering of editor creation logic
|
|
let createEditors: () => TPromise<any>;
|
|
let onEditorsCreated: (result) => TPromise<any>;
|
|
|
|
// If both editors exist, create joined promises - one for each editor
|
|
if (this._isResultsEditorVisible()) {
|
|
createEditors = () => {
|
|
return TPromise.join([
|
|
this._createEditor(<QueryResultsInput>newInput.results, this._resultsEditorContainer, this.group),
|
|
this._createEditor(<UntitledEditorInput>newInput.sql, this._sqlEditorContainer, this.group)
|
|
]);
|
|
};
|
|
onEditorsCreated = (result: IEditor[]) => {
|
|
return TPromise.join([
|
|
this._onResultsEditorCreated(<QueryResultsEditor>result[0], newInput.results, options),
|
|
this._onSqlEditorCreated(<TextResourceEditor>result[1], newInput.sql, options)
|
|
]);
|
|
};
|
|
|
|
// If only the sql editor exists, create a promise and wait for the sql editor to be created
|
|
} else {
|
|
createEditors = () => {
|
|
return this._createEditor(<UntitledEditorInput>newInput.sql, this._sqlEditorContainer, this.group);
|
|
};
|
|
onEditorsCreated = (result: TextResourceEditor) => {
|
|
return TPromise.join([
|
|
this._onSqlEditorCreated(result, newInput.sql, options)
|
|
]);
|
|
};
|
|
}
|
|
|
|
// Create a promise to re render the layout after the editor creation logic
|
|
let doLayout: () => TPromise<any> = () => {
|
|
this._doLayout();
|
|
return TPromise.as(undefined);
|
|
};
|
|
|
|
// Run all three steps synchronously
|
|
return createEditors()
|
|
.then(onEditorsCreated)
|
|
.then(doLayout)
|
|
.then(() => {
|
|
if (newInput.results) {
|
|
newInput.results.onRestoreViewStateEmitter.fire();
|
|
}
|
|
if (newInput.savedViewState) {
|
|
this._sqlEditor.getControl().restoreViewState(newInput.savedViewState);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a single editor based on the type of the given EditorInput.
|
|
*/
|
|
private _createEditor(editorInput: EditorInput, container: HTMLElement, group: IEditorGroup): TPromise<BaseEditor> {
|
|
const descriptor = this._editorDescriptorService.getEditor(editorInput);
|
|
if (!descriptor) {
|
|
return TPromise.wrapError(new Error(strings.format('Can not find a registered editor for the input {0}', editorInput)));
|
|
}
|
|
|
|
let editor = descriptor.instantiate(this._instantiationService);
|
|
editor.create(container);
|
|
editor.setVisible(this.isVisible(), group);
|
|
return TPromise.as(editor);
|
|
}
|
|
|
|
/**
|
|
* Sets input for the SQL editor after it has been created.
|
|
*/
|
|
private _onSqlEditorCreated(sqlEditor: TextResourceEditor, sqlInput: UntitledEditorInput, options: EditorOptions): Thenable<void> {
|
|
this._sqlEditor = sqlEditor;
|
|
return this._sqlEditor.setInput(sqlInput, options, CancellationToken.None);
|
|
}
|
|
|
|
/**
|
|
* Sets input for the results editor after it has been created.
|
|
*/
|
|
private _onResultsEditorCreated(resultsEditor: QueryResultsEditor, resultsInput: QueryResultsInput, options: EditorOptions): TPromise<void> {
|
|
this._resultsEditor = resultsEditor;
|
|
return this._resultsEditor.setInput(resultsInput, options);
|
|
}
|
|
|
|
/**
|
|
* Appends the HTML for the SQL editor. Creates new HTML every time.
|
|
*/
|
|
private _createSqlEditorContainer() {
|
|
const parentElement = this.getContainer();
|
|
this._sqlEditorContainer = DOM.append(parentElement, DOM.$('.details-editor-container'));
|
|
this._sqlEditorContainer.style.position = 'absolute';
|
|
}
|
|
|
|
/**
|
|
* Appends the HTML for the QueryResultsEditor to the QueryEditor. If the HTML has not yet been
|
|
* created, it creates it and appends it. If it has already been created, it locates it and
|
|
* appends it.
|
|
*/
|
|
private _createResultsEditorContainer() {
|
|
this._createSash();
|
|
|
|
const parentElement = this.getContainer();
|
|
let input = <QueryInput>this.input;
|
|
|
|
if (!input.results.container) {
|
|
let cssClass: string = '.master-editor-container';
|
|
if (this._orientation === Orientation.HORIZONTAL) {
|
|
cssClass = '.master-editor-container-horizontal';
|
|
}
|
|
|
|
this._resultsEditorContainer = DOM.append(parentElement, DOM.$(cssClass));
|
|
this._resultsEditorContainer.style.position = 'absolute';
|
|
|
|
input.results.container = this._resultsEditorContainer;
|
|
} else {
|
|
this._resultsEditorContainer = DOM.append(parentElement, input.results.container);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates the sash with the requested orientation and registers sash callbacks
|
|
*/
|
|
private _createSash(): void {
|
|
if (!this._sash) {
|
|
let parentElement: HTMLElement = this.getContainer();
|
|
|
|
if (this._orientation === Orientation.HORIZONTAL) {
|
|
this._sash = this._register(new HorizontalFlexibleSash(parentElement, this._minEditorSize));
|
|
} else {
|
|
this._sash = this._register(new VerticalFlexibleSash(parentElement, this._minEditorSize));
|
|
this._sash.setEdge(this.getTaskBarHeight() + this._tabHeight);
|
|
}
|
|
this._setSashDimension();
|
|
|
|
this._register(this._sash.onPositionChange(position => this._doLayout()));
|
|
}
|
|
|
|
this.sash.show();
|
|
}
|
|
|
|
private _setSashDimension(): void {
|
|
if (!this._dimension) {
|
|
return;
|
|
}
|
|
if (this._orientation === Orientation.HORIZONTAL) {
|
|
this._sash.setDimenesion(this._dimension);
|
|
} else {
|
|
this._sash.setDimenesion(new DOM.Dimension(this._dimension.width, this._dimension.height - this.getTaskBarHeight()));
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Updates the size of the 2 sub-editors. Uses agnostic dimensions due to the fact that
|
|
* the IFlexibleSash could be horizontal or vertical. The same logic is used for horizontal
|
|
* and vertical sashes.
|
|
*/
|
|
private _doLayout(skipResizeGridContent: boolean = false): void {
|
|
if (!this._isResultsEditorVisible() && this._sqlEditor) {
|
|
this._doLayoutSql();
|
|
return;
|
|
}
|
|
if (!this._sqlEditor || !this._resultsEditor || !this._dimension || !this._sash) {
|
|
return;
|
|
}
|
|
|
|
if (this._orientation === Orientation.HORIZONTAL) {
|
|
this._doLayoutHorizontal();
|
|
} else {
|
|
this._doLayoutVertical();
|
|
}
|
|
|
|
if (!skipResizeGridContent) {
|
|
this._resizeGridContents();
|
|
}
|
|
}
|
|
|
|
private getTaskBarHeight(): number {
|
|
let taskBarElement = this.taskbar.getContainer().getHTMLElement();
|
|
return DOM.getContentHeight(taskBarElement);
|
|
}
|
|
|
|
private _doLayoutHorizontal(): void {
|
|
let splitPointTop: number = this._sash.getSplitPoint();
|
|
let parent: ClientRect = this.getContainer().getBoundingClientRect();
|
|
|
|
let sqlEditorHeight = splitPointTop - (parent.top + this.getTaskBarHeight());
|
|
|
|
let titleBar = document.getElementById('workbench.parts.titlebar');
|
|
if (titleBar) {
|
|
sqlEditorHeight += DOM.getContentHeight(titleBar);
|
|
}
|
|
|
|
let queryResultsEditorHeight = parent.bottom - splitPointTop;
|
|
this._resultsEditorContainer.hidden = false;
|
|
this._sqlEditorContainer.style.height = `${sqlEditorHeight}px`;
|
|
this._sqlEditorContainer.style.width = `${this._dimension.width}px`;
|
|
this._sqlEditorContainer.style.top = `${this._editorTopOffset}px`;
|
|
|
|
this._resultsEditorContainer.style.height = `${queryResultsEditorHeight}px`;
|
|
this._resultsEditorContainer.style.width = `${this._dimension.width}px`;
|
|
this._resultsEditorContainer.style.top = `${splitPointTop}px`;
|
|
|
|
this._sqlEditor.layout(new DOM.Dimension(this._dimension.width, sqlEditorHeight));
|
|
this._resultsEditor.layout(new DOM.Dimension(this._dimension.width, queryResultsEditorHeight));
|
|
}
|
|
|
|
private _doLayoutVertical(): void {
|
|
let splitPointLeft: number = this._sash.getSplitPoint();
|
|
let parent: ClientRect = this.getContainer().getBoundingClientRect();
|
|
|
|
let sqlEditorWidth = splitPointLeft;
|
|
let queryResultsEditorWidth = parent.width - splitPointLeft;
|
|
|
|
let taskbarHeight = this.getTaskBarHeight();
|
|
this._sqlEditorContainer.style.width = `${sqlEditorWidth}px`;
|
|
this._sqlEditorContainer.style.height = `${this._dimension.height - taskbarHeight}px`;
|
|
this._sqlEditorContainer.style.left = `0px`;
|
|
|
|
this._resultsEditorContainer.hidden = false;
|
|
this._resultsEditorContainer.style.width = `${queryResultsEditorWidth}px`;
|
|
this._resultsEditorContainer.style.height = `${this._dimension.height - taskbarHeight}px`;
|
|
this._resultsEditorContainer.style.left = `${splitPointLeft}px`;
|
|
|
|
this._sqlEditor.layout(new DOM.Dimension(sqlEditorWidth, this._dimension.height - taskbarHeight));
|
|
this._resultsEditor.layout(new DOM.Dimension(queryResultsEditorWidth, this._dimension.height - taskbarHeight));
|
|
}
|
|
|
|
private _doLayoutSql() {
|
|
if (this._resultsEditorContainer) {
|
|
this._resultsEditorContainer.style.width = '0px';
|
|
this._resultsEditorContainer.style.height = '0px';
|
|
this._resultsEditorContainer.style.left = '0px';
|
|
this._resultsEditorContainer.hidden = true;
|
|
}
|
|
|
|
if (this._dimension) {
|
|
this._sqlEditor.layout(new DOM.Dimension(this._dimension.width, this._dimension.height - this.getTaskBarHeight()));
|
|
}
|
|
}
|
|
|
|
private _resizeGridContents(): void {
|
|
if (this._isResultsEditorVisible()) {
|
|
let queryInput: QueryInput = <QueryInput>this.input;
|
|
let uri: string = queryInput.getQueryResultsInputResource();
|
|
if (uri) {
|
|
this._queryModelService.resizeResultsets(uri);
|
|
}
|
|
}
|
|
}
|
|
|
|
private _disposeEditors(): void {
|
|
if (this._sqlEditor) {
|
|
this._sqlEditor.dispose();
|
|
this._sqlEditor = null;
|
|
}
|
|
if (this._resultsEditor) {
|
|
this._resultsEditor.dispose();
|
|
this._resultsEditor = null;
|
|
}
|
|
|
|
let thisEditorParent: HTMLElement = this.getContainer();
|
|
|
|
if (this._sqlEditorContainer) {
|
|
let sqlEditorParent: HTMLElement = this._sqlEditorContainer.parentElement;
|
|
if (sqlEditorParent && sqlEditorParent === thisEditorParent) {
|
|
this._sqlEditorContainer.parentElement.removeChild(this._sqlEditorContainer);
|
|
}
|
|
this._sqlEditorContainer = null;
|
|
}
|
|
|
|
if (this._resultsEditorContainer) {
|
|
let resultsEditorParent: HTMLElement = this._resultsEditorContainer.parentElement;
|
|
if (resultsEditorParent && resultsEditorParent === thisEditorParent) {
|
|
this._resultsEditorContainer.parentElement.removeChild(this._resultsEditorContainer);
|
|
}
|
|
this._resultsEditorContainer = null;
|
|
this.hideQueryResultsView = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if the QueryResultsInput has denoted that the results editor
|
|
* should be visible.
|
|
* Public for testing only.
|
|
*/
|
|
public _isResultsEditorVisible(): boolean {
|
|
let input: QueryInput = <QueryInput>this.input;
|
|
|
|
if (!input) {
|
|
return false;
|
|
}
|
|
return input.results.visible;
|
|
}
|
|
|
|
set resultsEditorVisibility(isVisible: boolean) {
|
|
let input: QueryInput = <QueryInput>this.input;
|
|
input.results.visible = isVisible;
|
|
}
|
|
|
|
/**
|
|
* Update the buttons on the taskbar to reflect the state of the current input.
|
|
*/
|
|
private _updateTaskbar(): void {
|
|
let queryInput: QueryInput = <QueryInput>this.input;
|
|
|
|
if (queryInput) {
|
|
this._cancelQueryAction.enabled = queryInput.cancelQueryEnabled;
|
|
this._changeConnectionAction.enabled = queryInput.changeConnectionEnabled;
|
|
|
|
// For the toggle database action, it should always be enabled since it's a toggle.
|
|
// We use inverse of connect enabled state for now, should refactor queryInput in the future to
|
|
// define connected as a boolean instead of using the enabled flag
|
|
this._toggleConnectDatabaseAction.enabled = true;
|
|
this._toggleConnectDatabaseAction.connected = !queryInput.connectEnabled;
|
|
this._runQueryAction.enabled = queryInput.runQueryEnabled;
|
|
if (queryInput.listDatabasesConnected) {
|
|
this.listDatabasesActionItem.onConnected();
|
|
} else {
|
|
this.listDatabasesActionItem.onDisconnect();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the text selection for the SQL editor based on the given ISelectionData.
|
|
*/
|
|
private _setSelection(selection: ISelectionData): void {
|
|
let rangeConversion: IRange = {
|
|
startLineNumber: selection.startLine + 1,
|
|
startColumn: selection.startColumn + 1,
|
|
endLineNumber: selection.endLine + 1,
|
|
endColumn: selection.endColumn + 1
|
|
};
|
|
let editor = this._sqlEditor.getControl();
|
|
editor.revealRange(rangeConversion);
|
|
editor.setSelection(rangeConversion);
|
|
editor.focus();
|
|
}
|
|
|
|
private saveEditorViewState(): void {
|
|
let queryInput = this.input as QueryInput;
|
|
if (queryInput) {
|
|
if (this._sqlEditor) {
|
|
queryInput.savedViewState = this._sqlEditor.getControl().saveViewState();
|
|
}
|
|
if (queryInput.results) {
|
|
queryInput.results.onSaveViewStateEmitter.fire();
|
|
}
|
|
}
|
|
}
|
|
|
|
// TESTING PROPERTIES ////////////////////////////////////////////////////////////
|
|
|
|
public get resultsEditor(): QueryResultsEditor {
|
|
return this._resultsEditor;
|
|
}
|
|
|
|
public get sqlEditor(): TextResourceEditor {
|
|
return this._sqlEditor;
|
|
}
|
|
|
|
public get taskbar(): Taskbar {
|
|
return this._taskbar;
|
|
}
|
|
|
|
public get sash(): IFlexibleSash {
|
|
return this._sash;
|
|
}
|
|
|
|
public get resultsEditorContainer(): HTMLElement {
|
|
return this._resultsEditorContainer;
|
|
}
|
|
|
|
public get sqlEditorContainer(): HTMLElement {
|
|
return this._sqlEditorContainer;
|
|
}
|
|
|
|
public get taskbarContainer(): HTMLElement {
|
|
return this._taskbarContainer;
|
|
}
|
|
|
|
public get runQueryAction(): RunQueryAction {
|
|
return this._runQueryAction;
|
|
}
|
|
|
|
public get cancelQueryAction(): CancelQueryAction {
|
|
return this._cancelQueryAction;
|
|
}
|
|
|
|
public get changeConnectionAction(): ConnectDatabaseAction {
|
|
return this._changeConnectionAction;
|
|
}
|
|
}
|