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,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const copyIncludeHeaders = 'copyIncludeHeaders';
export const configSaveAsCsv = 'saveAsCsv';
export const configCopyRemoveNewLine = 'copyRemoveNewLine';
export const configShowBatchTime = 'showBatchTime';

View File

@@ -0,0 +1,218 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { $, append, show, hide } from 'vs/base/browser/dom';
import { IDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar';
import { IEditorCloseEvent } from 'vs/workbench/common/editor';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
import { IQuickOpenService, IPickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen';
import { IMessageService, Severity } from 'vs/platform/message/common/message';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Action } from 'vs/base/common/actions';
import errors = require('vs/base/common/errors');
import { TPromise } from 'vs/base/common/winjs.base';
import { getCodeEditor as getEditorWidget } from 'vs/editor/common/services/codeEditorService';
import nls = require('vs/nls');
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
import { DidChangeLanguageFlavorParams } from 'data';
export interface ISqlProviderEntry extends IPickOpenEntry {
providerId: string;
}
// Query execution status
class SqlProviderEntry implements ISqlProviderEntry {
constructor(public providerId: string, private _providerDisplayName?: string) {
}
public get label(): string {
// If display name is provided, use it. Else use default
if (this._providerDisplayName) {
return this._providerDisplayName;
}
if (!this.providerId) {
return SqlProviderEntry.getDefaultLabel();
}
// Note: consider adding API to connection management service to
// support getting display name for provider so this is consistent
switch (this.providerId) {
case 'MSSQL':
return 'MSSQL';
default:
return this.providerId;
}
}
public static getDefaultLabel(): string {
return nls.localize('chooseSqlLang', 'Choose SQL Language');
}
}
// Shows SQL flavor status in the editor
export class SqlFlavorStatusbarItem implements IStatusbarItem {
private _element: HTMLElement;
private _flavorElement: HTMLElement;
private _sqlStatusEditors: { [editorUri: string]: SqlProviderEntry };
private _toDispose: IDisposable[];
constructor(
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IWorkbenchEditorService private _editorService: IWorkbenchEditorService,
@IEditorGroupService private _editorGroupService: IEditorGroupService,
@IQuickOpenService private _quickOpenService: IQuickOpenService,
@IInstantiationService private _instantiationService: IInstantiationService,
) {
this._sqlStatusEditors = {};
}
public render(container: HTMLElement): IDisposable {
this._element = append(container, $('.query-statusbar-group'));
this._flavorElement = append(this._element, $('a.editor-status-selection'));
this._flavorElement.title = nls.localize('changeProvider', "Change SQL language provider");
this._flavorElement.onclick = () => this._onSelectionClick();
hide(this._flavorElement);
this._toDispose = [];
this._toDispose.push(
this._connectionManagementService.onLanguageFlavorChanged((changeParams: DidChangeLanguageFlavorParams) => this._onFlavorChanged(changeParams)),
this._editorGroupService.onEditorsChanged(() => this._onEditorsChanged()),
this._editorGroupService.getStacksModel().onEditorClosed(event => this._onEditorClosed(event))
);
return combinedDisposable(this._toDispose);
}
private _onSelectionClick() {
const action = this._instantiationService.createInstance(ChangeFlavorAction, ChangeFlavorAction.ID, ChangeFlavorAction.LABEL);
action.run().done(null, errors.onUnexpectedError);
action.dispose();
}
private _onEditorClosed(event: IEditorCloseEvent): void {
let uri = WorkbenchUtils.getEditorUri(event.editor);
if (uri && uri in this._sqlStatusEditors) {
// If active editor is being closed, hide the query status.
let activeEditor = this._editorService.getActiveEditor();
if (activeEditor) {
let currentUri = WorkbenchUtils.getEditorUri(activeEditor.input);
if (uri === currentUri) {
hide(this._flavorElement);
}
}
// note: intentionally not removing language flavor. This is preserved across close/open events at present
// delete this._sqlStatusEditors[uri];
}
}
private _onEditorsChanged(): void {
let activeEditor = this._editorService.getActiveEditor();
if (activeEditor) {
let uri = WorkbenchUtils.getEditorUri(activeEditor.input);
// Show active editor's language flavor status
if (uri) {
this._showStatus(uri);
} else {
hide(this._flavorElement);
}
} else {
hide(this._flavorElement);
}
}
private _onFlavorChanged(changeParams: DidChangeLanguageFlavorParams): void {
if (changeParams) {
this._updateStatus(changeParams.uri, new SqlProviderEntry(changeParams.flavor));
}
}
// Update query status for the editor
private _updateStatus(uri: string, newStatus: SqlProviderEntry): void {
if (uri) {
this._sqlStatusEditors[uri] = newStatus;
this._showStatus(uri);
}
}
// Show/hide query status for active editor
private _showStatus(uri: string): void {
let activeEditor = this._editorService.getActiveEditor();
if (activeEditor) {
let currentUri = WorkbenchUtils.getEditorUri(activeEditor.input);
if (uri === currentUri) {
let flavor: SqlProviderEntry = this._sqlStatusEditors[uri];
if (flavor) {
this._flavorElement.textContent = flavor.label;
} else {
this._flavorElement.textContent = SqlProviderEntry.getDefaultLabel();
}
show(this._flavorElement);
}
}
}
}
export class ChangeFlavorAction extends Action {
public static ID = 'sql.action.editor.changeProvider';
public static LABEL = nls.localize('changeSqlProvider', "Change SQL Engine Provider");
constructor(
actionId: string,
actionLabel: string,
@IWorkbenchEditorService private _editorService: IWorkbenchEditorService,
@IQuickOpenService private _quickOpenService: IQuickOpenService,
@IMessageService private _messageService: IMessageService,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService
) {
super(actionId, actionLabel);
}
public run(): TPromise<any> {
let activeEditor = this._editorService.getActiveEditor();
let currentUri = WorkbenchUtils.getEditorUri(activeEditor.input);
if (this._connectionManagementService.isConnected(currentUri)) {
let currentProvider = this._connectionManagementService.getProviderIdFromUri(currentUri);
return this._showMessage(Severity.Info, nls.localize('alreadyConnected',
"A connection using engine {0} exists. To change please disconnect or change connection", currentProvider));
}
const editorWidget = getEditorWidget(activeEditor);
if (!editorWidget) {
return this._showMessage(Severity.Info, nls.localize('noEditor', "No text editor active at this time"));
}
// TODO #1334 use connectionManagementService.GetProviderNames here. The challenge is that the credentials provider is returned
// so we need a way to filter this using a capabilities check, with isn't yet implemented
const ProviderOptions: ISqlProviderEntry[] = [
new SqlProviderEntry('MSSQL')
];
// TODO: select the current language flavor
return this._quickOpenService.pick(ProviderOptions, { placeHolder: nls.localize('pickSqlProvider', "Select SQL Language Provider"), autoFocus: { autoFocusIndex: 0 } }).then(provider => {
if (provider) {
activeEditor = this._editorService.getActiveEditor();
const editorWidget = getEditorWidget(activeEditor);
if (editorWidget) {
if (currentUri) {
this._connectionManagementService.doChangeLanguageFlavor(currentUri, 'sql', provider.providerId);
}
}
}
});
}
private _showMessage(sev: Severity, message: string): TPromise<any> {
this._messageService.show(sev, message);
return TPromise.as(undefined);
}
}

View File

@@ -0,0 +1,49 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
// localizable strings
export const runQueryBatchStartMessage = localize('runQueryBatchStartMessage', 'Started executing query at ');
export const runQueryBatchStartLine = localize('runQueryBatchStartLine', 'Line {0}');
export const msgCancelQueryFailed = localize('msgCancelQueryFailed', 'Canceling the query failed: {0}');
export const filepathPrompt = localize('filepathPrompt', 'File path');
export const filepathPlaceholder = localize('filepathPlaceholder', 'File name');
export const filepathMessage = localize('filepathMessage', 'File name');
export const overwritePrompt = localize('overwritePrompt', 'A file with this name already exists. Do you want to replace the existing file?');
export const overwritePlaceholder = localize('overwritePlaceholder', 'A file with this name already exists');
export const msgSaveResultInProgress = localize('msgSaveResultInProgress', 'A save request is already executing. Please wait for its completion.');
export const msgCannotOpenContent = localize('msgCannotOpenContent', 'Error occurred opening content in editor.');
export const msgSaveStarted = localize('msgSaveStarted', 'Started saving results to ');
export const msgSaveFailed = localize('msgSaveFailed', 'Failed to save results. ');
export const msgSaveSucceeded = localize('msgSaveSucceeded', 'Successfully saved results to ');
export const msgStatusRunQueryInProgress = localize('msgStatusRunQueryInProgress', 'Executing query...');
// /** Results Pane Labels */
export const maximizeLabel = localize('maximizeLabel', 'Maximize');
export const restoreLabel = localize('restoreLabel', 'Restore');
export const saveCSVLabel = localize('saveCSVLabel', 'Save as CSV');
export const saveJSONLabel = localize('saveJSONLabel', 'Save as JSON');
export const saveExcelLabel = localize('saveExcelLabel', 'Save as Excel');
export const viewChartLabel = localize('viewChartLabel', 'View as Chart');
export const resultPaneLabel = localize('resultPaneLabel', 'Results');
export const selectAll = localize('selectAll', 'Select all');
export const copyLabel = localize('copyLabel', 'Copy');
export const executeQueryLabel = localize('executeQueryLabel', 'Executing query ');
/** Messages Pane Labels */
export const messagePaneLabel = localize('messagePaneLabel', 'Messages');
export const elapsedTimeLabel = localize('elapsedTimeLabel', 'Total execution time: {0}');
/** Warning message for save icons */
export const msgCannotSaveMultipleSelections = localize('msgCannotSaveMultipleSelections', 'Save results command cannot be used with multiple selections.');

View File

@@ -0,0 +1,262 @@
/*---------------------------------------------------------------------------------------------
* 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 { Registry } from 'vs/platform/registry/common/platform';
import { EditorDescriptor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actionRegistry';
import { IConfigurationRegistry, Extensions as ConfigExtensions } from 'vs/platform/configuration/common/configurationRegistry';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes';
import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { QueryEditor } from 'sql/parts/query/editor/queryEditor';
import { QueryResultsEditor } from 'sql/parts/query/editor/queryResultsEditor';
import { QueryResultsInput } from 'sql/parts/query/common/queryResultsInput';
import * as queryContext from 'sql/parts/query/common/queryContext';
import { QueryInput } from 'sql/parts/query/common/queryInput';
import { EditDataEditor } from 'sql/parts/editData/editor/editDataEditor';
import { EditDataInput } from 'sql/parts/editData/common/editDataInput';
import { RunQueryKeyboardAction, RunCurrentQueryKeyboardAction, CancelQueryKeyboardAction, RefreshIntellisenseKeyboardAction } from 'sql/parts/query/execution/keyboardQueryActions';
import * as gridActions from 'sql/parts/grid/views/gridActions';
import * as gridCommands from 'sql/parts/grid/views/gridCommands';
import { QueryPlanEditor } from 'sql/parts/queryPlan/queryPlanEditor';
import { QueryPlanInput } from 'sql/parts/queryPlan/queryPlanInput';
import { localize } from 'vs/nls';
const gridCommandsWeightBonus = 100; // give our commands a little bit more weight over other default list/tree commands
export const QueryEditorVisibleCondition = ContextKeyExpr.has(queryContext.queryEditorVisibleId);
export const ResultsGridFocusCondition = ContextKeyExpr.and(ContextKeyExpr.has(queryContext.resultsVisibleId), ContextKeyExpr.has(queryContext.resultsGridFocussedId));
export const ResultsMessagesFocusCondition = ContextKeyExpr.and(ContextKeyExpr.has(queryContext.resultsVisibleId), ContextKeyExpr.has(queryContext.resultsMessagesFocussedId));
// Editor
const queryResultsEditorDescriptor = new EditorDescriptor(
QueryResultsEditor.ID,
'QueryResults',
'sql/parts/query/editor/queryResultsEditor',
'QueryResultsEditor'
);
Registry.as<IEditorRegistry>(EditorExtensions.Editors)
.registerEditor(queryResultsEditorDescriptor, [new SyncDescriptor(QueryResultsInput)]);
// Editor
const queryEditorDescriptor = new EditorDescriptor(
QueryEditor.ID,
'Query',
'sql/parts/query/editor/queryEditor',
'QueryEditor'
);
Registry.as<IEditorRegistry>(EditorExtensions.Editors)
.registerEditor(queryEditorDescriptor, [new SyncDescriptor(QueryInput)]);
// Query Plan editor registration
const createLoginEditorDescriptor = new EditorDescriptor(
QueryPlanEditor.ID,
'QueryPlan',
'sql/parts/queryPlan/queryPlanEditor',
'QueryPlanEditor'
);
Registry.as<IEditorRegistry>(EditorExtensions.Editors)
.registerEditor(createLoginEditorDescriptor, [new SyncDescriptor(QueryPlanInput)]);
// Editor
const editDataEditorDescriptor = new EditorDescriptor(
EditDataEditor.ID,
'EditData',
'sql/parts/editData/editor/editDataEditor',
'EditDataEditor'
);
Registry.as<IEditorRegistry>(EditorExtensions.Editors)
.registerEditor(editDataEditorDescriptor, [new SyncDescriptor(EditDataInput)]);
let actionRegistry = <IWorkbenchActionRegistry>Registry.as(Extensions.WorkbenchActions);
// Query Actions
actionRegistry.registerWorkbenchAction(
new SyncActionDescriptor(
RunQueryKeyboardAction,
RunQueryKeyboardAction.ID,
RunQueryKeyboardAction.LABEL,
{ primary: KeyCode.F5 }
),
RunQueryKeyboardAction.LABEL
);
actionRegistry.registerWorkbenchAction(
new SyncActionDescriptor(
RunCurrentQueryKeyboardAction,
RunCurrentQueryKeyboardAction.ID,
RunCurrentQueryKeyboardAction.LABEL,
{ primary:KeyMod.CtrlCmd | KeyCode.F5 }
),
RunCurrentQueryKeyboardAction.LABEL
);
actionRegistry.registerWorkbenchAction(
new SyncActionDescriptor(
CancelQueryKeyboardAction,
CancelQueryKeyboardAction.ID,
CancelQueryKeyboardAction.LABEL,
{ primary: KeyMod.Alt | KeyCode.PauseBreak }
),
CancelQueryKeyboardAction.LABEL
);
actionRegistry.registerWorkbenchAction(
new SyncActionDescriptor(
RefreshIntellisenseKeyboardAction,
RefreshIntellisenseKeyboardAction.ID,
RefreshIntellisenseKeyboardAction.LABEL
),
RefreshIntellisenseKeyboardAction.LABEL
);
// Grid actions
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: gridActions.GRID_COPY_ID,
weight: KeybindingsRegistry.WEIGHT.workbenchContrib(gridCommandsWeightBonus),
when: ResultsGridFocusCondition,
primary: KeyMod.CtrlCmd | KeyCode.KEY_C,
handler: gridCommands.copySelection
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: gridActions.MESSAGES_SELECTALL_ID,
weight: KeybindingsRegistry.WEIGHT.workbenchContrib(gridCommandsWeightBonus),
when: ResultsMessagesFocusCondition,
primary: KeyMod.CtrlCmd | KeyCode.KEY_A,
handler: gridCommands.selectAllMessages
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: gridActions.GRID_SELECTALL_ID,
weight: KeybindingsRegistry.WEIGHT.workbenchContrib(gridCommandsWeightBonus),
when: ResultsGridFocusCondition,
primary: KeyMod.CtrlCmd | KeyCode.KEY_A,
handler: gridCommands.selectAll
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: gridActions.MESSAGES_COPY_ID,
weight: KeybindingsRegistry.WEIGHT.workbenchContrib(gridCommandsWeightBonus),
when: ResultsMessagesFocusCondition,
primary: KeyMod.CtrlCmd | KeyCode.KEY_C,
handler: gridCommands.copyMessagesSelection
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: gridActions.GRID_SAVECSV_ID,
weight: KeybindingsRegistry.WEIGHT.workbenchContrib(gridCommandsWeightBonus),
when: ResultsGridFocusCondition,
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_R, KeyMod.CtrlCmd | KeyCode.KEY_C),
handler: gridCommands.saveAsCsv
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: gridActions.GRID_SAVEJSON_ID,
weight: KeybindingsRegistry.WEIGHT.workbenchContrib(gridCommandsWeightBonus),
when: ResultsGridFocusCondition,
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_R, KeyMod.CtrlCmd | KeyCode.KEY_J),
handler: gridCommands.saveAsJson
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: gridActions.GRID_SAVEEXCEL_ID,
weight: KeybindingsRegistry.WEIGHT.workbenchContrib(gridCommandsWeightBonus),
when: ResultsGridFocusCondition,
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_R, KeyMod.CtrlCmd | KeyCode.KEY_E),
handler: gridCommands.saveAsExcel
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: gridActions.TOGGLERESULTS_ID,
weight: KeybindingsRegistry.WEIGHT.workbenchContrib(gridCommandsWeightBonus),
when: QueryEditorVisibleCondition,
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_R,
handler: gridCommands.toggleResultsPane
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: gridActions.TOGGLEMESSAGES_ID,
weight: KeybindingsRegistry.WEIGHT.workbenchContrib(gridCommandsWeightBonus),
when: QueryEditorVisibleCondition,
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_Y,
handler: gridCommands.toggleMessagePane
});
// Intellisense and other configuration options
let configurationRegistry = <IConfigurationRegistry>Registry.as(ConfigExtensions.Configuration);
configurationRegistry.registerConfiguration({
'id': 'sqlEditor',
'title': 'SQL Editor',
'type': 'object',
'properties': {
'sql.messagesDefaultOpen': {
'type': 'boolean',
'description': localize('sql.messagesDefaultOpen', 'True for the messages pane to be open by default; false for closed'),
'default': true
},
'sql.saveAsCsv.includeHeaders': {
'type': 'boolean',
'description': localize('sql.saveAsCsv.includeHeaders', '[Optional] When true, column headers are included when saving results as CSV'),
'default': true
},
'sql.copyIncludeHeaders': {
'type': 'boolean',
'description': localize('sql.copyIncludeHeaders', '[Optional] Configuration options for copying results from the Results View'),
'default': false
},
'sql.copyRemoveNewLine': {
'type': 'boolean',
'description': localize('sql.copyRemoveNewLine', '[Optional] Configuration options for copying multi-line results from the Results View'),
'default': true
},
'sql.showBatchTime': {
'type': 'boolean',
'description': localize('sql.showBatchTime', '[Optional] Should execution time be shown for individual batches'),
'default': false
},
'sql.intelliSense.enableIntelliSense': {
'type': 'boolean',
'default': true,
'description': localize('sql.intelliSense.enableIntelliSense', 'Should IntelliSense be enabled')
},
'sql.intelliSense.enableErrorChecking': {
'type': 'boolean',
'default': true,
'description': localize('sql.intelliSense.enableErrorChecking', 'Should IntelliSense error checking be enabled')
},
'sql.intelliSense.enableSuggestions': {
'type': 'boolean',
'default': true,
'description': localize('sql.intelliSense.enableSuggestions', 'Should IntelliSense suggestions be enabled')
},
'sql.intelliSense.enableQuickInfo': {
'type': 'boolean',
'default': true,
'description': localize('sql.intelliSense.enableQuickInfo', 'Should IntelliSense quick info be enabled')
},
'sql.intelliSense.lowerCaseSuggestions': {
'type': 'boolean',
'default': false,
'description': localize('sql.intelliSense.lowerCaseSuggestions', 'Should IntelliSense suggestions be lowercase')
}
}
});

View File

@@ -0,0 +1,21 @@
/*---------------------------------------------------------------------------------------------
* 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 { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
/**
* Context Keys to use with keybindings for the results grid and messages used in query and edit data views
*/
export const queryEditorVisibleId = 'queryEditorVisible';
export const resultsVisibleId = 'resultsVisible';
export const resultsGridFocussedId = 'resultsGridFocussed';
export const resultsMessagesFocussedId = 'resultsMessagesFocussed';
export const QueryEditorVisibleContext = new RawContextKey<boolean>(queryEditorVisibleId, false);
export const ResultsVisibleContext = new RawContextKey<boolean>(resultsVisibleId, false);
export const ResultsGridFocussedContext = new RawContextKey<boolean>(resultsGridFocussedId, false);
export const ResultsMessagesFocussedContext = new RawContextKey<boolean>(resultsMessagesFocussedId, false);

View File

@@ -0,0 +1,35 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IConnectableInput } from 'sql/parts/connection/common/connectionManagement';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { QueryEditorService } from 'sql/parts/query/services/queryEditorService';
export interface IQueryEditorOptions extends IEditorOptions {
// Tells IQueryEditorService.queryEditorCheck to not open this input in the QueryEditor.
// Used when the user changes the Language Mode to not-SQL for files with .sql extensions.
denyQueryEditor?: boolean;
}
export const IQueryEditorService = createDecorator<QueryEditorService>('QueryEditorService');
export interface IQueryEditorService {
_serviceBrand: any;
// Creates new untitled document for SQL queries and opens it in a new editor tab
newSqlEditor(sqlContent?: string, connectionProviderName?: string): Promise<IConnectableInput>;
// Creates a new query plan document
newQueryPlanEditor(xmlShowPlan: string): Promise<any>;
// Creates new edit data session
newEditDataEditor(schemaName: string, tableName: string): Promise<IConnectableInput>;
// Clears any QueryEditor data for the given URI held by this service
onQueryInputClosed(uri: string): void;
}

View File

@@ -0,0 +1,249 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TPromise } from 'vs/base/common/winjs.base';
import { EditorInput, EditorModel, ConfirmResult, EncodingMode, IEncodingSupport } from 'vs/workbench/common/editor';
import { IConnectionManagementService, IConnectableInput, INewConnectionParams, RunQueryOnConnectionMode } from 'sql/parts/connection/common/connectionManagement';
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import { QueryResultsInput } from 'sql/parts/query/common/queryResultsInput';
import { IQueryModelService } from 'sql/parts/query/execution/queryModel';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import Event, { Emitter } from 'vs/base/common/event';
import URI from 'vs/base/common/uri';
import { ISelectionData, ExecutionPlanOptions } from 'data';
import { IQueryEditorService } from 'sql/parts/query/common/queryEditorService';
/**
* Input for the QueryEditor. This input is simply a wrapper around a QueryResultsInput for the QueryResultsEditor
* and a UntitledEditorInput for the SQL File Editor.
*/
export class QueryInput extends EditorInput implements IEncodingSupport, IConnectableInput, IDisposable {
public static ID: string = 'workbench.editorinputs.queryInput';
public static SCHEMA: string = 'sql';
private _runQueryEnabled: boolean;
private _cancelQueryEnabled: boolean;
private _connectEnabled: boolean;
private _disconnectEnabled: boolean;
private _changeConnectionEnabled: boolean;
private _listDatabasesConnected: boolean;
private _updateTaskbar: Emitter<void>;
private _showQueryResultsEditor: Emitter<void>;
private _updateSelection: Emitter<ISelectionData>;
private _toDispose: IDisposable[];
private _currentEventCallbacks: IDisposable[];
constructor(
private _name: string,
private _description: string,
private _sql: UntitledEditorInput,
private _results: QueryResultsInput,
private _connectionProviderName: string,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IQueryModelService private _queryModelService: IQueryModelService,
@IQueryEditorService private _queryEditorService: IQueryEditorService
) {
super();
let self = this;
this._updateTaskbar = new Emitter<void>();
this._showQueryResultsEditor = new Emitter<void>();
this._updateSelection = new Emitter<ISelectionData>();
this._toDispose = [];
this._currentEventCallbacks = [];
// re-emit sql editor events through this editor if it exists
if (this._sql) {
this._toDispose.push(this._sql.onDidChangeDirty(() => this._onDidChangeDirty.fire()));
}
// Attach to event callbacks
if (this._queryModelService) {
// Register callbacks for the Actions
this._toDispose.push(
this._queryModelService.onRunQueryStart(uri => {
if (self.uri === uri) {
self.onRunQuery();
}
})
);
this._toDispose.push(
this._queryModelService.onRunQueryComplete(uri => {
if (self.uri === uri) {
self.onQueryComplete();
}
})
);
}
if (this._connectionManagementService) {
this._toDispose.push(self._connectionManagementService.onDisconnect(result => {
if (result.connectionUri === self.uri) {
self.onDisconnect();
}
}));
if (self.uri) {
if (this._connectionProviderName) {
this._connectionManagementService.doChangeLanguageFlavor(self.uri, 'sql', this._connectionProviderName);
} else {
this._connectionManagementService.ensureDefaultLanguageFlavor(self.uri);
}
}
}
this.onDisconnect();
this.onQueryComplete();
}
// Getters for private properties
public get uri(): string { return this.getResource().toString(); }
public get sql(): UntitledEditorInput { return this._sql; }
public get results(): QueryResultsInput { return this._results; }
public get updateTaskbarEvent(): Event<void> { return this._updateTaskbar.event; }
public get showQueryResultsEditorEvent(): Event<void> { return this._showQueryResultsEditor.event; }
public get updateSelectionEvent(): Event<ISelectionData> { return this._updateSelection.event; }
public get runQueryEnabled(): boolean { return this._runQueryEnabled; }
public get cancelQueryEnabled(): boolean { return this._cancelQueryEnabled; }
public get connectEnabled(): boolean { return this._connectEnabled; }
public get disconnectEnabled(): boolean { return this._disconnectEnabled; }
public get changeConnectionEnabled(): boolean { return this._changeConnectionEnabled; }
public get listDatabasesConnected(): boolean { return this._listDatabasesConnected; }
public getQueryResultsInputResource(): string { return this._results.uri; }
public showQueryResultsEditor(): void { this._showQueryResultsEditor.fire(); }
public updateSelection(selection: ISelectionData): void { this._updateSelection.fire(selection); }
public getTypeId(): string { return UntitledEditorInput.ID; }
public getDescription(): string { return this._description; }
public supportsSplitEditor(): boolean { return false; }
public getModeId(): string { return QueryInput.SCHEMA; }
public revert(): TPromise<boolean> { return this._sql.revert(); }
public matches(otherInput: any): boolean {
if (otherInput instanceof QueryInput) {
return this._sql.matches(otherInput.sql);
}
return this._sql.matches(otherInput);
}
// Forwarding resource functions to the inline sql file editor
public get onDidModelChangeContent(): Event<void> { return this._sql.onDidModelChangeContent; }
public get onDidModelChangeEncoding(): Event<void> { return this._sql.onDidModelChangeEncoding; }
public resolve(refresh?: boolean): TPromise<EditorModel> { return this._sql.resolve(); }
public save(): TPromise<boolean> { return this._sql.save(); }
public isDirty(): boolean { return this._sql.isDirty(); }
public confirmSave(): ConfirmResult { return this._sql.confirmSave(); }
public getResource(): URI { return this._sql.getResource(); }
public getEncoding(): string { return this._sql.getEncoding(); }
public suggestFileName(): string { return this._sql.suggestFileName(); }
public getName(): string { return this._sql.getName(); }
public hasAssociatedFilePath(): boolean { return this._sql.hasAssociatedFilePath; }
public setEncoding(encoding: string, mode: EncodingMode /* ignored, we only have Encode */): void {
this._sql.setEncoding(encoding, mode);
}
// State update funtions
public runQuery(selection: ISelectionData, executePlanOptions?: ExecutionPlanOptions): void {
this._queryModelService.runQuery(this.uri, selection, this.uri, this, executePlanOptions);
this.showQueryResultsEditor();
}
public runQueryStatement(selection: ISelectionData): void {
this._queryModelService.runQueryStatement(this.uri, selection, this.uri, this);
this.showQueryResultsEditor();
}
public onConnectStart(): void {
this._runQueryEnabled = false;
this._cancelQueryEnabled = false;
this._connectEnabled = false;
this._disconnectEnabled = true;
this._changeConnectionEnabled = false;
this._listDatabasesConnected = false;
this._updateTaskbar.fire();
}
public onConnectReject(): void {
this.onDisconnect();
this._updateTaskbar.fire();
}
public onConnectSuccess(params?: INewConnectionParams): void {
this._runQueryEnabled = true;
this._connectEnabled = false;
this._disconnectEnabled = true;
this._changeConnectionEnabled = true;
this._listDatabasesConnected = true;
let isRunningQuery = this._queryModelService.isRunningQuery(this.uri);
if (!isRunningQuery && params && params.runQueryOnCompletion) {
let selection: ISelectionData = params ? params.querySelection : undefined;
if (params.runQueryOnCompletion === RunQueryOnConnectionMode.executeCurrentQuery) {
this.runQueryStatement(selection);
} else if (params.runQueryOnCompletion === RunQueryOnConnectionMode.executeQuery) {
this.runQuery(selection);
} else if (params.runQueryOnCompletion === RunQueryOnConnectionMode.estimatedQueryPlan) {
this.runQuery(selection, { displayEstimatedQueryPlan: true });
}
}
this._updateTaskbar.fire();
}
public onDisconnect(): void {
this._runQueryEnabled = true;
this._cancelQueryEnabled = false;
this._connectEnabled = true;
this._disconnectEnabled = false;
this._changeConnectionEnabled = false;
this._listDatabasesConnected = false;
this._updateTaskbar.fire();
}
public onRunQuery(): void {
this._runQueryEnabled = false;
this._cancelQueryEnabled = true;
this._updateTaskbar.fire();
}
public onQueryComplete(): void {
this._runQueryEnabled = true;
this._cancelQueryEnabled = false;
this._updateTaskbar.fire();
}
// Clean up functions
public dispose(): void {
this._queryModelService.disposeQuery(this.uri);
this._sql.dispose();
this._results.dispose();
this._toDispose = dispose(this._toDispose);
this._currentEventCallbacks = dispose(this._currentEventCallbacks);
super.dispose();
}
public close(): void {
this._queryEditorService.onQueryInputClosed(this.uri);
this._connectionManagementService.disconnectEditor(this, true);
this._sql.close();
this._results.close();
super.close();
}
/**
* Unsubscribe all events in _currentEventCallbacks and set the new callbacks
* to be unsubscribed the next time this method is called.
*
* This method is used to ensure that all callbacks point to the current QueryEditor
* in the case that this QueryInput is moved between different QueryEditors.
*/
public setEventCallbacks(callbacks: IDisposable[]): void {
this._currentEventCallbacks = dispose(this._currentEventCallbacks);
this._currentEventCallbacks = callbacks;
}
}

View File

@@ -0,0 +1,306 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import QueryRunner from 'sql/parts/query/execution/queryRunner';
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IDisposable } from 'vs/base/common/lifecycle';
import data = require('data');
import { TPromise } from 'vs/base/common/winjs.base';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
import * as TelemetryUtils from 'sql/common/telemetryUtilities';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
export const SERVICE_ID = 'queryManagementService';
export const IQueryManagementService = createDecorator<IQueryManagementService>(SERVICE_ID);
export interface IQueryManagementService {
_serviceBrand: any;
addQueryRequestHandler(queryType: string, runner: QueryRequestHandler): IDisposable;
registerRunner(runner: QueryRunner, uri: string): void;
cancelQuery(ownerUri: string): Thenable<data.QueryCancelResult>;
runQuery(ownerUri: string, selection: data.ISelectionData, runOptions?: data.ExecutionPlanOptions): Thenable<void>;
runQueryStatement(ownerUri: string, line: number, column: number): Thenable<void>;
runQueryString(ownerUri: string, queryString: string): Thenable<void>;
runQueryAndReturn(ownerUri: string, queryString: string): Thenable<data.SimpleExecuteResult>;
getQueryRows(rowData: data.QueryExecuteSubsetParams): Thenable<data.QueryExecuteSubsetResult>;
disposeQuery(ownerUri: string): Thenable<void>;
saveResults(requestParams: data.SaveResultsRequestParams): Thenable<data.SaveResultRequestResult>;
// Callbacks
onQueryComplete(result: data.QueryExecuteCompleteNotificationResult): void;
onBatchStart(batchInfo: data.QueryExecuteBatchNotificationParams): void;
onBatchComplete(batchInfo: data.QueryExecuteBatchNotificationParams): void;
onResultSetComplete(resultSetInfo: data.QueryExecuteResultSetCompleteNotificationParams): void;
onMessage(message: data.QueryExecuteMessageParams): void;
// Edit Data Callbacks
onEditSessionReady(ownerUri: string, success: boolean, message: string): void;
// Edit Data Functions
initializeEdit(ownerUri: string, schemaName: string, objectName: string, objectType: string, rowLimit: number): Thenable<void>;
disposeEdit(ownerUri: string): Thenable<void>;
updateCell(ownerUri: string, rowId: number, columnId: number, newValue: string): Thenable<data.EditUpdateCellResult>;
commitEdit(ownerUri): Thenable<void>;
createRow(ownerUri: string): Thenable<data.EditCreateRowResult>;
deleteRow(ownerUri: string, rowId: number): Thenable<void>;
revertCell(ownerUri: string, rowId: number, columnId: number): Thenable<data.EditRevertCellResult>;
revertRow(ownerUri: string, rowId: number): Thenable<void>;
getEditRows(rowData: data.EditSubsetParams): Thenable<data.EditSubsetResult>;
}
/*
* An object that can handle basic request-response actions related to queries
*/
export interface QueryRequestHandler {
cancelQuery(ownerUri: string): Thenable<data.QueryCancelResult>;
runQuery(ownerUri: string, selection: data.ISelectionData, runOptions?: data.ExecutionPlanOptions): Thenable<void>;
runQueryStatement(ownerUri: string, line: number, column: number): Thenable<void>;
runQueryString(ownerUri: string, queryString: string): Thenable<void>;
runQueryAndReturn(ownerUri: string, queryString: string): Thenable<data.SimpleExecuteResult>;
getQueryRows(rowData: data.QueryExecuteSubsetParams): Thenable<data.QueryExecuteSubsetResult>;
disposeQuery(ownerUri: string): Thenable<void>;
saveResults(requestParams: data.SaveResultsRequestParams): Thenable<data.SaveResultRequestResult>;
// Edit Data actions
initializeEdit(ownerUri: string, schemaName: string, objectName: string, objectType: string, rowLimit: number): Thenable<void>;
disposeEdit(ownerUri: string): Thenable<void>;
updateCell(ownerUri: string, rowId: number, columnId: number, newValue: string): Thenable<data.EditUpdateCellResult>;
commitEdit(ownerUri): Thenable<void>;
createRow(ownerUri: string): Thenable<data.EditCreateRowResult>;
deleteRow(ownerUri: string, rowId: number): Thenable<void>;
revertCell(ownerUri: string, rowId: number, columnId: number): Thenable<data.EditRevertCellResult>;
revertRow(ownerUri: string, rowId: number): Thenable<void>;
getEditRows(rowData: data.EditSubsetParams): Thenable<data.EditSubsetResult>;
}
export class QueryManagementService implements IQueryManagementService {
public static readonly DefaultQueryType = 'MSSQL';
public _serviceBrand: any;
private _requestHandlers = new Map<string, QueryRequestHandler>();
// public for testing only
public _queryRunners = new Map<string, QueryRunner>();
// public for testing only
public _handlerCallbackQueue: ((run: QueryRunner) => void)[] = [];
constructor(
@IConnectionManagementService private _connectionService: IConnectionManagementService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
}
// Registers queryRunners with their uris to distribute notifications.
// Ensures that notifications are handled in the correct order by handling
// enqueued handlers first.
// public for testing only
public registerRunner(runner: QueryRunner, uri: string): void {
// If enqueueOrRun was called before registerRunner for the current query,
// _handlerCallbackQueue will be non-empty. Run all handlers in the queue first
// so that notifications are handled in order they arrived
while (this._handlerCallbackQueue.length > 0) {
let handler = this._handlerCallbackQueue.shift();
handler(runner);
}
// Set the runner for any other handlers if the runner is in use by the
// current query or a subsequent query
if (!runner.hasCompleted) {
this._queryRunners.set(uri, runner);
}
}
// Handles logic to run the given handlerCallback at the appropriate time. If the given runner is
// undefined, the handlerCallback is put on the _handlerCallbackQueue to be run once the runner is set
// public for testing only
private enqueueOrRun(handlerCallback: (runnerParam: QueryRunner) => void, runner: QueryRunner): void {
if (runner === undefined) {
this._handlerCallbackQueue.push(handlerCallback);
} else {
handlerCallback(runner);
}
}
private _notify(ownerUri: string, sendNotification: (runner: QueryRunner) => void): void {
let runner = this._queryRunners.get(ownerUri);
this.enqueueOrRun(sendNotification, runner);
}
public addQueryRequestHandler(queryType: string, handler: QueryRequestHandler): IDisposable {
this._requestHandlers.set(queryType, handler);
return {
dispose: () => {
}
};
}
private addTelemetry(eventName: string, ownerUri: string, runOptions?: data.ExecutionPlanOptions): void {
let providerId: string = this._connectionService.getProviderIdFromUri(ownerUri);
let data: TelemetryUtils.IConnectionTelemetryData = {
provider: providerId,
};
if (runOptions) {
data = Object.assign({}, data, {
displayEstimatedQueryPlan: runOptions.displayEstimatedQueryPlan,
displayActualQueryPlan: runOptions.displayActualQueryPlan
});
}
TelemetryUtils.addTelemetry(this._telemetryService, eventName, data);
}
private _runAction<T>(uri: string, action: (handler: QueryRequestHandler) => Thenable<T>): Thenable<T> {
let providerId: string = this._connectionService.getProviderIdFromUri(uri);
if (!providerId) {
return TPromise.wrapError(new Error('Connection is required in order to interact with queries'));
}
let handler = this._requestHandlers.get(providerId);
if (handler) {
return action(handler);
} else {
return TPromise.wrapError(new Error('No Handler Registered'));
}
}
public cancelQuery(ownerUri: string): Thenable<data.QueryCancelResult> {
this.addTelemetry(TelemetryKeys.CancelQuery, ownerUri);
return this._runAction(ownerUri, (runner) => {
return runner.cancelQuery(ownerUri);
});
}
public runQuery(ownerUri: string, selection: data.ISelectionData, runOptions?: data.ExecutionPlanOptions): Thenable<void> {
this.addTelemetry(TelemetryKeys.RunQuery, ownerUri, runOptions);
return this._runAction(ownerUri, (runner) => {
return runner.runQuery(ownerUri, selection, runOptions);
});
}
public runQueryStatement(ownerUri: string, line: number, column: number): Thenable<void> {
this.addTelemetry(TelemetryKeys.RunQueryStatement, ownerUri);
return this._runAction(ownerUri, (runner) => {
return runner.runQueryStatement(ownerUri, line, column);
});
}
public runQueryString(ownerUri: string, queryString: string): Thenable<void> {
return this._runAction(ownerUri, (runner) => {
return runner.runQueryString(ownerUri, queryString);
});
}
public runQueryAndReturn(ownerUri: string, queryString: string): Thenable<data.SimpleExecuteResult> {
return this._runAction(ownerUri, (runner) => {
return runner.runQueryAndReturn(ownerUri, queryString);
});
}
public getQueryRows(rowData: data.QueryExecuteSubsetParams): Thenable<data.QueryExecuteSubsetResult> {
return this._runAction(rowData.ownerUri, (runner) => {
return runner.getQueryRows(rowData);
});
}
public disposeQuery(ownerUri: string): Thenable<void> {
return this._runAction(ownerUri, (runner) => {
return runner.disposeQuery(ownerUri);
});
}
public saveResults(requestParams: data.SaveResultsRequestParams): Thenable<data.SaveResultRequestResult> {
return this._runAction(requestParams.ownerUri, (runner) => {
return runner.saveResults(requestParams);
});
}
public onQueryComplete(result: data.QueryExecuteCompleteNotificationResult): void {
this._notify(result.ownerUri, (runner: QueryRunner) => {
runner.handleQueryComplete(result);
});
}
public onBatchStart(batchInfo: data.QueryExecuteBatchNotificationParams): void {
this._notify(batchInfo.ownerUri, (runner: QueryRunner) => {
runner.handleBatchStart(batchInfo);
});
}
public onBatchComplete(batchInfo: data.QueryExecuteBatchNotificationParams): void {
this._notify(batchInfo.ownerUri, (runner: QueryRunner) => {
runner.handleBatchComplete(batchInfo);
});
}
public onResultSetComplete(resultSetInfo: data.QueryExecuteResultSetCompleteNotificationParams): void {
this._notify(resultSetInfo.ownerUri, (runner: QueryRunner) => {
runner.handleResultSetComplete(resultSetInfo);
});
}
public onMessage(message: data.QueryExecuteMessageParams): void {
this._notify(message.ownerUri, (runner: QueryRunner) => {
runner.handleMessage(message);
});
}
// Edit Data Functions
public initializeEdit(ownerUri: string, schemaName: string, objectName: string, objectType: string, rowLimit: number): Thenable<void> {
return this._runAction(ownerUri, (runner) => {
return runner.initializeEdit(ownerUri, schemaName, objectName, objectType, rowLimit);
});
}
public onEditSessionReady(ownerUri: string, success: boolean, message: string): void {
this._notify(ownerUri, (runner: QueryRunner) => {
runner.handleEditSessionReady(ownerUri, success, message);
});
}
public updateCell(ownerUri: string, rowId: number, columnId: number, newValue: string): Thenable<data.EditUpdateCellResult> {
return this._runAction(ownerUri, (runner) => {
return runner.updateCell(ownerUri, rowId, columnId, newValue);
});
}
public commitEdit(ownerUri: string): Thenable<void> {
return this._runAction(ownerUri, (runner) => {
return runner.commitEdit(ownerUri);
});
}
public createRow(ownerUri: string): Thenable<data.EditCreateRowResult> {
return this._runAction(ownerUri, (runner) => {
return runner.createRow(ownerUri);
});
}
public deleteRow(ownerUri: string, rowId: number): Thenable<void> {
return this._runAction(ownerUri, (runner) => {
return runner.deleteRow(ownerUri, rowId);
});
}
public disposeEdit(ownerUri: string): Thenable<void> {
return this._runAction(ownerUri, (runner) => {
return runner.disposeEdit(ownerUri);
});
}
public revertCell(ownerUri: string, rowId: number, columnId: number): Thenable<data.EditRevertCellResult> {
return this._runAction(ownerUri, (runner) => {
return runner.revertCell(ownerUri, rowId, columnId);
});
}
public revertRow(ownerUri: string, rowId: number): Thenable<void> {
return this._runAction(ownerUri, (runner) => {
return runner.revertRow(ownerUri, rowId);
});
}
public getEditRows(rowData: data.EditSubsetParams): Thenable<data.EditSubsetResult> {
return this._runAction(rowData.ownerUri, (runner) => {
return runner.getEditRows(rowData);
});
}
}

View File

@@ -0,0 +1,109 @@
/*---------------------------------------------------------------------------------------------
* 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 { localize } from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import { EditorInput } from 'vs/workbench/common/editor';
/**
* Input for the QueryResultsEditor. This input helps with logic for the viewing and editing of
* data in the results grid.
*/
export class QueryResultsInput extends EditorInput {
// Tracks if the editor that holds this input should be visible (i.e. true if a query has been run)
private _visible: boolean;
// Tracks if the editor has holds this input has has bootstrapped angular yet
private _hasBootstrapped: boolean;
// Holds the HTML content for the editor when the editor discards this input and loads another
private _editorContainer: HTMLElement;
constructor(private _uri: string) {
super();
this._visible = false;
this._hasBootstrapped = false;
}
getTypeId(): string {
return QueryResultsInput.ID;
}
getName(): string {
return localize('extensionsInputName', 'Extension');
}
matches(other: any): boolean {
if (other instanceof QueryResultsInput) {
return (other._uri === this._uri);
}
return false;
}
resolve(refresh?: boolean): TPromise<any> {
return TPromise.as(null);
}
supportsSplitEditor(): boolean {
return false;
}
public setVisibleTrue(): void {
this._visible = true;
}
public setBootstrappedTrue(): void {
this._hasBootstrapped = true;
}
public dispose(): void {
this._disposeContainer();
super.dispose();
}
private _disposeContainer() {
if (!this._editorContainer) {
return;
}
let parentContainer = this._editorContainer.parentNode;
if (parentContainer) {
parentContainer.removeChild(this._editorContainer);
this._editorContainer = null;
}
}
//// Properties
static get ID() {
return 'workbench.query.queryResultsInput';
}
set container(container: HTMLElement) {
this._disposeContainer();
this._editorContainer = container;
}
get container(): HTMLElement {
return this._editorContainer;
}
get hasBootstrapped(): boolean {
return this._hasBootstrapped;
}
get visible(): boolean {
return this._visible;
}
get uri(): string {
return this._uri;
}
}

View File

@@ -0,0 +1,300 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as ConnectionConstants from 'sql/parts/connection/common/constants';
import * as Constants from 'sql/parts/query/common/constants';
import * as LocalizedConstants from 'sql/parts/query/common/localizedConstants';
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
import { SaveResultsRequestParams } from 'data';
import { IQueryManagementService } from 'sql/parts/query/common/queryManagement';
import { ISaveRequest, SaveFormat } from 'sql/parts/grid/common/interfaces';
import * as PathUtilities from 'sql/common/pathUtilities';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IMessageService, Severity } from 'vs/platform/message/common/message';
import { IOutputService, IOutputChannel, IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/parts/output/common/output';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows';
import { Registry } from 'vs/platform/registry/common/platform';
import URI from 'vs/base/common/uri';
import { IUntitledEditorService, UNTITLED_SCHEMA } from 'vs/workbench/services/untitled/common/untitledEditorService';
import * as paths from 'vs/base/common/paths';
import * as nls from 'vs/nls';
import * as pretty from 'pretty-data';
import { ISlickRange } from 'angular2-slickgrid';
import * as path from 'path';
/**
* Handles save results request from the context menu of slickGrid
*/
export class ResultSerializer {
public static tempFileCount: number = 1;
private static MAX_FILENAMES = 100;
private _uri: string;
private _filePath: string;
constructor(
@IInstantiationService private _instantiationService: IInstantiationService,
@IMessageService private _messageService: IMessageService,
@IOutputService private _outputService: IOutputService,
@IQueryManagementService private _queryManagementService: IQueryManagementService,
@IWorkspaceConfigurationService private _workspaceConfigurationService: IWorkspaceConfigurationService,
@IWorkbenchEditorService private _editorService: IWorkbenchEditorService,
@IWorkspaceContextService private _contextService: IWorkspaceContextService,
@IWindowsService private _windowsService: IWindowsService,
@IWindowService private _windowService: IWindowService,
@IUntitledEditorService private _untitledEditorService: IUntitledEditorService
) { }
/**
* Handle save request by getting filename from user and sending request to service
*/
public saveResults(uri: string, saveRequest: ISaveRequest): Thenable<void> {
const self = this;
this._uri = uri;
// prompt for filepath
let filePath = self.promptForFilepath(saveRequest);
if (filePath) {
return self.sendRequestToService(filePath, saveRequest.batchIndex, saveRequest.resultSetNumber, saveRequest.format, saveRequest.selection ? saveRequest.selection[0] : undefined);
}
return Promise.resolve(undefined);
}
/**
* Open a xml/json link - Opens the content in a new editor pane
*/
public openLink(content: string, columnName: string, linkType: string): void {
let fileMode: string = undefined;
let fileUri = this.getUntitledFileUri(columnName);
if (linkType === SaveFormat.XML) {
fileMode = SaveFormat.XML;
try {
content = pretty.pd.xml(content);
} catch (e) {
// If Xml fails to parse, fall back on original Xml content
}
} else if (linkType === SaveFormat.JSON) {
let jsonContent: string = undefined;
fileMode = SaveFormat.JSON;
try {
jsonContent = JSON.parse(content);
} catch (e) {
// If Json fails to parse, fall back on original Json content
}
if (jsonContent) {
// If Json content was valid and parsed, pretty print content to a string
content = JSON.stringify(jsonContent, undefined, 4);
}
}
this.openUntitledFile(fileMode, content, fileUri);
}
private getUntitledFileUri(columnName: string): URI {
let fileName = columnName;
let uri: URI = URI.from({ scheme: UNTITLED_SCHEMA, path: fileName });
// If the current filename is taken, try another up to a max number
if (this._untitledEditorService.exists(uri)) {
let i = 1;
while (i < ResultSerializer.MAX_FILENAMES
&& this._untitledEditorService.exists(uri)) {
fileName = [columnName, i.toString()].join('-');
uri = URI.from({ scheme: UNTITLED_SCHEMA, path: fileName });
i++;
}
if (this._untitledEditorService.exists(uri)) {
// If this fails, return undefined and let the system figure out the right name
uri = undefined;
}
}
return uri;
}
private ensureOutputChannelExists(): void {
Registry.as<IOutputChannelRegistry>(OutputExtensions.OutputChannels)
.registerChannel(ConnectionConstants.outputChannelName, ConnectionConstants.outputChannelName);
}
private get outputChannel(): IOutputChannel {
this.ensureOutputChannelExists();
return this._outputService.getChannel(ConnectionConstants.outputChannelName);
}
private get rootPath(): string {
return PathUtilities.getRootPath(this._contextService);
}
private logToOutputChannel(message: string): void {
this.outputChannel.append(message);
}
private promptForFilepath(saveRequest: ISaveRequest): string {
let filepathPlaceHolder = PathUtilities.resolveCurrentDirectory(this._uri, this.rootPath);
filepathPlaceHolder = path.join(filepathPlaceHolder, this.getResultsDefaultFilename(saveRequest));
let filePath: string = this._windowService.showSaveDialog({
title: nls.localize('saveAsFileTitle', 'Choose Results File'),
defaultPath: paths.normalize(filepathPlaceHolder, true)
});
return filePath;
}
private getResultsDefaultFilename(saveRequest: ISaveRequest): string {
let fileName = 'Results';
switch (saveRequest.format) {
case SaveFormat.CSV:
fileName = fileName + '.csv';
break;
case SaveFormat.JSON:
fileName = fileName + '.json';
break;
case SaveFormat.EXCEL:
fileName = fileName + '.xlsx';
break;
case SaveFormat.XML:
fileName = fileName + '.xml';
break;
default:
fileName = fileName + '.txt';
}
return fileName;
}
private getConfigForCsv(): SaveResultsRequestParams {
let saveResultsParams = <SaveResultsRequestParams>{ resultFormat: SaveFormat.CSV as string };
// get save results config from vscode config
let saveConfig = WorkbenchUtils.getSqlConfigSection(this._workspaceConfigurationService, Constants.configSaveAsCsv);
// if user entered config, set options
if (saveConfig) {
if (saveConfig.includeHeaders !== undefined) {
saveResultsParams.includeHeaders = saveConfig.includeHeaders;
}
}
return saveResultsParams;
}
private getConfigForJson(): SaveResultsRequestParams {
// JSON does not currently have special conditions
let saveResultsParams = <SaveResultsRequestParams>{ resultFormat: SaveFormat.JSON as string };
return saveResultsParams;
}
private getConfigForExcel(): SaveResultsRequestParams {
// get save results config from vscode config
// Note: we are currently using the configSaveAsCsv setting since it has the option mssql.saveAsCsv.includeHeaders
// and we want to have just 1 setting that lists this.
let config = this.getConfigForCsv();
config.resultFormat = SaveFormat.EXCEL;
return config;
}
private getParameters(filePath: string, batchIndex: number, resultSetNo: number, format: string, selection: ISlickRange): SaveResultsRequestParams {
let saveResultsParams: SaveResultsRequestParams;
if (!path.isAbsolute(filePath)) {
this._filePath = PathUtilities.resolveFilePath(this._uri, filePath, this.rootPath);
} else {
this._filePath = filePath;
}
if (format === SaveFormat.CSV) {
saveResultsParams = this.getConfigForCsv();
} else if (format === SaveFormat.JSON) {
saveResultsParams = this.getConfigForJson();
} else if (format === SaveFormat.EXCEL) {
saveResultsParams = this.getConfigForExcel();
}
saveResultsParams.filePath = this._filePath;
saveResultsParams.ownerUri = this._uri;
saveResultsParams.resultSetIndex = resultSetNo;
saveResultsParams.batchIndex = batchIndex;
if (this.isSelected(selection)) {
saveResultsParams.rowStartIndex = selection.fromRow;
saveResultsParams.rowEndIndex = selection.toRow;
saveResultsParams.columnStartIndex = selection.fromCell;
saveResultsParams.columnEndIndex = selection.toCell;
}
return saveResultsParams;
}
/**
* Check if a range of cells were selected.
*/
private isSelected(selection: ISlickRange): boolean {
return (selection && !((selection.fromCell === selection.toCell) && (selection.fromRow === selection.toRow)));
}
/**
* Send request to sql tools service to save a result set
*/
private sendRequestToService(filePath: string, batchIndex: number, resultSetNo: number, format: string, selection: ISlickRange): Thenable<void> {
let saveResultsParams = this.getParameters(filePath, batchIndex, resultSetNo, format, selection);
this.logToOutputChannel(LocalizedConstants.msgSaveStarted + this._filePath);
// send message to the sqlserverclient for converting results to the requested format and saving to filepath
return this._queryManagementService.saveResults(saveResultsParams).then(result => {
if (result.messages) {
this._messageService.show(Severity.Error, LocalizedConstants.msgSaveFailed + result.messages);
this.logToOutputChannel(LocalizedConstants.msgSaveFailed + result.messages);
} else {
this._messageService.show(Severity.Info, LocalizedConstants.msgSaveSucceeded + this._filePath);
this.logToOutputChannel(LocalizedConstants.msgSaveSucceeded + filePath);
this.openSavedFile(this._filePath, format);
}
// TODO telemetry for save results
// Telemetry.sendTelemetryEvent('SavedResults', { 'type': format });
}, error => {
this._messageService.show(Severity.Error, LocalizedConstants.msgSaveFailed + error);
this.logToOutputChannel(LocalizedConstants.msgSaveFailed + error);
});
}
/**
* Open the saved file in a new vscode editor pane
*/
private openSavedFile(filePath: string, format: string): void {
if (format === SaveFormat.EXCEL) {
// This will not open in VSCode as it's treated as binary. Use the native file opener instead
// Note: must use filePath here, URI does not open correctly
// TODO see if there is an alternative opener that includes error handling
let fileUri = URI.from({ scheme: PathUtilities.FILE_SCHEMA, path: filePath });
this._windowsService.openExternal(fileUri.toString());
} else {
let uri = URI.file(filePath);
this._editorService.openEditor({ resource: uri }).then((result) => {
}, (error: any) => {
this._messageService.show(Severity.Error, error);
});
}
}
/**
* Open the saved file in a new vscode editor pane
*/
private openUntitledFile(fileMode: string, contents: string, fileUri: URI = undefined): void {
const input = this._untitledEditorService.createOrGet(fileUri, fileMode, contents);
this._editorService.openEditor(input, { pinned: true })
.then(
(success) => {
},
(error: any) => {
this._messageService.show(Severity.Error, error);
}
);
}
}