diff --git a/gulpfile.js b/gulpfile.js index 3c588144e..248296208 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -36,13 +36,21 @@ gulp.task('ext:lint', () => { }); // Copy icons for OE -gulp.task('ext:copy-assets', (done) => { +gulp.task('ext:copy-OE-assets', (done) => { return gulp.src([ config.paths.project.root + '/src/objectExplorer/objectTypes/*' ]) .pipe(gulp.dest('out/src/objectExplorer/objectTypes')); }); +// Copy icons for Query History +gulp.task('ext:copy-queryHistory-assets', (done) => { + return gulp.src([ + config.paths.project.root + '/src/queryHistory/icons/*' + ]) + .pipe(gulp.dest('out/src/queryHistory/icons')); +}); + // Compile source gulp.task('ext:compile-src', (done) => { return gulp.src([ @@ -208,7 +216,7 @@ gulp.task('ext:compile-tests', (done) => { }); -gulp.task('ext:compile', gulp.series('ext:compile-src', 'ext:compile-tests', 'ext:copy-assets')); +gulp.task('ext:compile', gulp.series('ext:compile-src', 'ext:compile-tests', 'ext:copy-OE-assets', 'ext:copy-queryHistory-assets')); gulp.task('ext:copy-tests', () => { return gulp.src(config.paths.project.root + '/test/resources/**/*') diff --git a/localization/xliff/enu/constants/localizedConstants.enu.xlf b/localization/xliff/enu/constants/localizedConstants.enu.xlf index 8a811da63..e85b32d2a 100644 --- a/localization/xliff/enu/constants/localizedConstants.enu.xlf +++ b/localization/xliff/enu/constants/localizedConstants.enu.xlf @@ -251,9 +251,24 @@ Firewall rule successfully created. + + Choose Query History listing + + + Choose An Action + + + Open Query History Listing + + + Run Query History Listing + Invalid IP Address + + No Queries Available + Retry diff --git a/package.json b/package.json index 63945271f..49a092c29 100644 --- a/package.json +++ b/package.json @@ -151,6 +151,10 @@ { "id": "objectExplorer", "name": "%extension.connections%" + }, + { + "id": "queryHistory", + "name": "%extension.queryHistory%" } ] }, @@ -204,6 +208,24 @@ "when": "view == objectExplorer", "title": "%mssql.addObjectExplorer%", "group": "navigation" + }, + { + "command": "mssql.startQueryHistoryCapture", + "when": "view == queryHistory && isQueryHistoryEnabled == false", + "title": "%mssql.startQueryHistoryCapture%", + "group": "navigation" + }, + { + "command": "mssql.pauseQueryHistoryCapture", + "when": "view == queryHistory && isQueryHistoryEnabled == true", + "title": "%mssql.pauseQueryHistoryCapture%", + "group": "navigation" + }, + { + "command": "mssql.clearAllQueryHistory", + "when": "view == queryHistory", + "title": "%mssql.clearAllQueryHistory%", + "group": "secondary" } ], "view/item/context": [ @@ -251,6 +273,21 @@ "command": "mssql.scriptAlter", "when": "view == objectExplorer && viewItem =~ /^(AggregateFunction|PartitionFunction|ScalarValuedFunction|StoredProcedure|TableValuedFunction|View)$/", "group": "MS_SQL@4" + }, + { + "command": "mssql.openQueryHistory", + "when": "view == queryHistory && viewItem == queryHistoryNode", + "group": "MS_SQL@1" + }, + { + "command": "mssql.runQueryHistory", + "when": "view == queryHistory && viewItem == queryHistoryNode", + "group": "MS_SQL@2" + }, + { + "command": "mssql.deleteQueryHistory", + "when": "view == queryHistory && viewItem == queryHistoryNode", + "group": "MS_SQL@3" } ], "commandPalette": [ @@ -293,6 +330,14 @@ { "command": "mssql.toggleSqlCmd", "when": "editorFocus && editorLangId == 'sql'" + }, + { + "command": "mssql.startQueryHistoryCapture", + "when": "isQueryHistoryEnabled == false" + }, + { + "command": "mssql.pauseQueryHistoryCapture", + "when": "isQueryHistoryEnabled == true" } ] }, @@ -418,6 +463,49 @@ "command": "mssql.scriptAlter", "title": "%mssql.scriptAlter%", "group": "MS SQL" + }, + { + "command": "mssql.openQueryHistory", + "title": "%mssql.openQueryHistory%", + "group": "MS SQL" + }, + { + "command": "mssql.runQueryHistory", + "title": "%mssql.runQueryHistory%", + "group": "MS SQL" + }, + { + "command": "mssql.deleteQueryHistory", + "title": "%mssql.deleteQueryHistory%", + "group": "MS SQL" + }, + { + "command": "mssql.clearAllQueryHistory", + "title": "%mssql.clearAllQueryHistory%", + "group": "MS SQL" + }, + { + "command": "mssql.startQueryHistoryCapture", + "title": "%mssql.startQueryHistoryCapture%", + "group": "MS SQL", + "icon": { + "light": "images/start.svg", + "dark": "images/start_inverse.svg" + } + }, + { + "command": "mssql.pauseQueryHistoryCapture", + "title": "%mssql.pauseQueryHistoryCapture%", + "group": "MS SQL", + "icon": { + "light": "images/stop.svg", + "dark": "images/stop_inverse.svg" + } + }, + { + "command": "mssql.commandPaletteQueryHistory", + "title": "%mssql.commandPaletteQueryHistory%", + "group": "MS SQL" } ], "keybindings": [ @@ -826,6 +914,18 @@ "default": false, "description": "%mssql.persistQueryResultTabs%", "scope": "window" + }, + "mssql.queryHistoryLimit": { + "type": "number", + "default": 20, + "description": "%mssql.queryHistoryLimit%", + "scope": "window" + }, + "mssql.enableQueryHistoryCapture": { + "type": "boolean", + "default": true, + "description": "%mssql.enableQueryHistoryCapture%", + "scope": "window" } } } diff --git a/package.nls.json b/package.nls.json index f33f5b9c7..8705adfac 100644 --- a/package.nls.json +++ b/package.nls.json @@ -8,9 +8,18 @@ "mssql.scriptDelete":"Script as Drop", "mssql.scriptExecute":"Script as Execute", "mssql.scriptAlter":"Script as Alter", +"mssql.openQueryHistory":"Open Query", +"mssql.runQueryHistory":"Run Query", +"mssql.deleteQueryHistory":"Delete", +"mssql.clearAllQueryHistory":"Clear All History", +"mssql.enableQueryHistoryCapture":"Enable Query History Capture", +"mssql.startQueryHistoryCapture":"Start Query History Capture", +"mssql.pauseQueryHistoryCapture":"Pause Query History Capture", +"mssql.commandPaletteQueryHistory":"Open Query History in Command Palette", "mssql.removeObjectExplorerNode":"Remove", "mssql.refreshObjectExplorerNode":"Refresh", "extension.connections":"Connections", +"extension.queryHistory":"Query History", "mssql.connect":"Connect", "mssql.disconnect":"Disconnect", "mssql.manageProfiles":"Manage Connection Profiles", @@ -80,5 +89,6 @@ "mssql.intelliSense.enableSuggestions":"Should IntelliSense suggestions be enabled", "mssql.intelliSense.enableQuickInfo":"Should IntelliSense quick info be enabled", "mssql.intelliSense.lowerCaseSuggestions":"Should IntelliSense suggestions be lowercase", -"mssql.persistQueryResultTabs":"Should query result selections and scroll positions be saved when switching tabs (may impact performance)" +"mssql.persistQueryResultTabs":"Should query result selections and scroll positions be saved when switching tabs (may impact performance)", +"mssql.queryHistoryLimit":"Number of query history entries to show in the Query History view" } diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 837e10b5a..88b658722 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -9,6 +9,8 @@ export const extensionName = 'mssql'; export const extensionConfigSectionName = 'mssql'; export const mssqlProviderName = 'MSSQL'; export const noneProviderName = 'None'; +export const objectExplorerId = 'objectExplorer'; +export const queryHistory = 'queryHistory'; export const connectionApplicationName = 'vscode-mssql'; export const outputChannelName = 'MSSQL'; export const connectionConfigFilename = 'settings.json'; @@ -24,6 +26,14 @@ export const cmdChooseDatabase = 'mssql.chooseDatabase'; export const cmdChooseLanguageFlavor = 'mssql.chooseLanguageFlavor'; export const cmdShowReleaseNotes = 'mssql.showReleaseNotes'; export const cmdShowGettingStarted = 'mssql.showGettingStarted'; +export const cmdRefreshQueryHistory = 'mssql.refreshQueryHistory'; +export const cmdClearAllQueryHistory = 'mssql.clearAllQueryHistory'; +export const cmdDeleteQueryHistory = 'mssql.deleteQueryHistory'; +export const cmdOpenQueryHistory = 'mssql.openQueryHistory'; +export const cmdRunQueryHistory = 'mssql.runQueryHistory'; +export const cmdStartQueryHistory = 'mssql.startQueryHistoryCapture'; +export const cmdPauseQueryHistory = 'mssql.pauseQueryHistoryCapture'; +export const cmdCommandPaletteQueryHistory = 'mssql.commandPaletteQueryHistory'; export const cmdNewQuery = 'mssql.newQuery'; export const cmdManageConnectionProfiles = 'mssql.manageProfiles'; export const cmdRebuildIntelliSenseCache = 'mssql.rebuildIntelliSenseCache'; @@ -107,6 +117,9 @@ export const sqlToolsServiceDownloadUrlConfigKey = 'downloadUrl'; export const extConfigResultFontFamily = 'resultsFontFamily'; export const configApplyLocalization = 'applyLocalization'; export const configPersistQueryResultTabs = 'persistQueryResultTabs'; +export const configQueryHistoryLimit = 'queryHistoryLimit'; +export const configEnableQueryHistoryCapture = 'enableQueryHistoryCapture'; +export const isQueryHistoryEnabled = 'isQueryHistoryEnabled'; // ToolsService Constants export const serviceInstallingTo = 'Installing SQL tools service to'; diff --git a/src/controllers/mainController.ts b/src/controllers/mainController.ts index 2243c95a3..d22df5e75 100644 --- a/src/controllers/mainController.ts +++ b/src/controllers/mainController.ts @@ -30,6 +30,8 @@ import { Deferred } from '../protocol'; import { ConnectTreeNode } from '../objectExplorer/connectTreeNode'; import { ObjectExplorerUtils } from '../objectExplorer/objectExplorerUtils'; import { ScriptOperation } from '../models/contracts/scripting/scriptingRequest'; +import { QueryHistoryProvider } from '../queryHistory/queryHistoryProvider'; +import { QueryHistoryNode } from '../queryHistory/queryHistoryNode'; /** * The main controller class that initializes the extension @@ -49,6 +51,7 @@ export default class MainController implements vscode.Disposable { private _lastOpenedTimer: Utils.Timer; private _untitledSqlDocumentService: UntitledSqlDocumentService; private _objectExplorerProvider: ObjectExplorerProvider; + private _queryHistoryProvider: QueryHistoryProvider; private _scriptingService: ScriptingService; /** @@ -139,116 +142,9 @@ export default class MainController implements vscode.Disposable { this.registerCommand(Constants.cmdToggleSqlCmd); this._event.on(Constants.cmdToggleSqlCmd, async () => { await self.onToggleSqlCmd(); }); - // Register the object explorer tree provider - this._objectExplorerProvider = new ObjectExplorerProvider(this._connectionMgr); - this._context.subscriptions.push( - vscode.window.registerTreeDataProvider('objectExplorer', this._objectExplorerProvider) - ); + this.initializeObjectExplorer(); - // Add Object Explorer Node - this.registerCommand(Constants.cmdAddObjectExplorer); - this._event.on(Constants.cmdAddObjectExplorer, async () => { - if (!self._objectExplorerProvider.objectExplorerExists) { - self._objectExplorerProvider.objectExplorerExists = true; - } - await self.createObjectExplorerSession(); - }); - - // Object Explorer New Query - this._context.subscriptions.push( - vscode.commands.registerCommand( - Constants.cmdObjectExplorerNewQuery, async (treeNodeInfo: TreeNodeInfo) => { - const connectionCredentials = Object.assign({}, treeNodeInfo.connectionCredentials); - const databaseName = ObjectExplorerUtils.getDatabaseName(treeNodeInfo); - if (databaseName !== connectionCredentials.database && - databaseName !== LocalizedConstants.defaultDatabaseLabel) { - connectionCredentials.database = databaseName; - } else if (databaseName === LocalizedConstants.defaultDatabaseLabel) { - connectionCredentials.database = ''; - } - treeNodeInfo.connectionCredentials = connectionCredentials; - await self.onNewQuery(treeNodeInfo); - })); - - // Remove Object Explorer Node - this._context.subscriptions.push( - vscode.commands.registerCommand( - Constants.cmdRemoveObjectExplorerNode, async (treeNodeInfo: TreeNodeInfo) => { - await this._objectExplorerProvider.removeObjectExplorerNode(treeNodeInfo); - let profile = treeNodeInfo.connectionCredentials; - await this._connectionMgr.connectionStore.removeProfile(profile, false); - return this._objectExplorerProvider.refresh(undefined); - })); - - // Refresh Object Explorer Node - this.registerCommand(Constants.cmdRefreshObjectExplorerNode); - this._event.on(Constants.cmdRefreshObjectExplorerNode, () => { - return this._objectExplorerProvider.refreshNode(this._objectExplorerProvider.currentNode); - }); - - // Sign In into Object Explorer Node - this._context.subscriptions.push( - vscode.commands.registerCommand( - Constants.cmdObjectExplorerNodeSignIn, async (node: AccountSignInTreeNode) => { - let profile = node.parentNode.connectionCredentials; - profile = await self.connectionManager.connectionUI.promptForRetryCreateProfile(profile); - if (profile) { - node.parentNode.connectionCredentials = profile; - self._objectExplorerProvider.updateNode(node.parentNode); - self._objectExplorerProvider.signInNodeServer(node.parentNode); - return self._objectExplorerProvider.refresh(undefined); - } - })); - - // Connect to Object Explorer Node - this._context.subscriptions.push( - vscode.commands.registerCommand( - Constants.cmdConnectObjectExplorerNode, async (node: ConnectTreeNode) => { - self._objectExplorerProvider.currentNode = node.parentNode; - await self.createObjectExplorerSession(node.parentNode.connectionCredentials); - })); - - // Disconnect Object Explorer Node - this._context.subscriptions.push( - vscode.commands.registerCommand( - Constants.cmdDisconnectObjectExplorerNode, async (node: TreeNodeInfo) => { - await this._objectExplorerProvider.removeObjectExplorerNode(node, true); - return this._objectExplorerProvider.refresh(undefined); - })); - - // Initiate the scripting service - this._scriptingService = new ScriptingService(this._connectionMgr); - - // Script as Select - this._context.subscriptions.push( - vscode.commands.registerCommand( - Constants.cmdScriptSelect, async (node: TreeNodeInfo) => { - this.scriptNode(node, ScriptOperation.Select, true); - })); - - // Script as Create - this._context.subscriptions.push( - vscode.commands.registerCommand( - Constants.cmdScriptCreate, async (node: TreeNodeInfo) => - this.scriptNode(node, ScriptOperation.Create))); - - // Script as Drop - this._context.subscriptions.push( - vscode.commands.registerCommand( - Constants.cmdScriptDelete, async (node: TreeNodeInfo) => - this.scriptNode(node, ScriptOperation.Delete))); - - // Script as Execute - this._context.subscriptions.push( - vscode.commands.registerCommand( - Constants.cmdScriptExecute, async (node: TreeNodeInfo) => - this.scriptNode(node, ScriptOperation.Execute))); - - // Script as Alter - this._context.subscriptions.push( - vscode.commands.registerCommand( - Constants.cmdScriptAlter, async (node: TreeNodeInfo) => - this.scriptNode(node, ScriptOperation.Alter))); + this.initializeQueryHistory(); // Add handlers for VS Code generated commands this._vscodeWrapper.onDidCloseTextDocument(async (params) => await this.onDidCloseTextDocument(params)); @@ -374,6 +270,196 @@ export default class MainController implements vscode.Disposable { } } + /** + * Initializes the Object Explorer commands + */ + private initializeObjectExplorer(): void { + const self = this; + // Register the object explorer tree provider + this._objectExplorerProvider = new ObjectExplorerProvider(this._connectionMgr); + this._context.subscriptions.push( + vscode.window.registerTreeDataProvider('objectExplorer', this._objectExplorerProvider) + ); + + // Add Object Explorer Node + this.registerCommand(Constants.cmdAddObjectExplorer); + this._event.on(Constants.cmdAddObjectExplorer, async () => { + if (!self._objectExplorerProvider.objectExplorerExists) { + self._objectExplorerProvider.objectExplorerExists = true; + } + await self.createObjectExplorerSession(); + }); + + // Object Explorer New Query + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdObjectExplorerNewQuery, async (treeNodeInfo: TreeNodeInfo) => { + const connectionCredentials = Object.assign({}, treeNodeInfo.connectionCredentials); + const databaseName = ObjectExplorerUtils.getDatabaseName(treeNodeInfo); + if (databaseName !== connectionCredentials.database && + databaseName !== LocalizedConstants.defaultDatabaseLabel) { + connectionCredentials.database = databaseName; + } else if (databaseName === LocalizedConstants.defaultDatabaseLabel) { + connectionCredentials.database = ''; + } + treeNodeInfo.connectionCredentials = connectionCredentials; + await self.onNewQuery(treeNodeInfo); + })); + + // Remove Object Explorer Node + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdRemoveObjectExplorerNode, async (treeNodeInfo: TreeNodeInfo) => { + await this._objectExplorerProvider.removeObjectExplorerNode(treeNodeInfo); + let profile = treeNodeInfo.connectionCredentials; + await this._connectionMgr.connectionStore.removeProfile(profile, false); + return this._objectExplorerProvider.refresh(undefined); + })); + + // Refresh Object Explorer Node + this.registerCommand(Constants.cmdRefreshObjectExplorerNode); + this._event.on(Constants.cmdRefreshObjectExplorerNode, () => { + return this._objectExplorerProvider.refreshNode(this._objectExplorerProvider.currentNode); + }); + + // Sign In into Object Explorer Node + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdObjectExplorerNodeSignIn, async (node: AccountSignInTreeNode) => { + let profile = node.parentNode.connectionCredentials; + profile = await self.connectionManager.connectionUI.promptForRetryCreateProfile(profile); + if (profile) { + node.parentNode.connectionCredentials = profile; + self._objectExplorerProvider.updateNode(node.parentNode); + self._objectExplorerProvider.signInNodeServer(node.parentNode); + return self._objectExplorerProvider.refresh(undefined); + } + })); + + // Connect to Object Explorer Node + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdConnectObjectExplorerNode, async (node: ConnectTreeNode) => { + self._objectExplorerProvider.currentNode = node.parentNode; + await self.createObjectExplorerSession(node.parentNode.connectionCredentials); + })); + + // Disconnect Object Explorer Node + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdDisconnectObjectExplorerNode, async (node: TreeNodeInfo) => { + await this._objectExplorerProvider.removeObjectExplorerNode(node, true); + return this._objectExplorerProvider.refresh(undefined); + })); + + // Initiate the scripting service + this._scriptingService = new ScriptingService(this._connectionMgr); + + // Script as Select + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdScriptSelect, async (node: TreeNodeInfo) => { + this.scriptNode(node, ScriptOperation.Select, true); + })); + + // Script as Create + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdScriptCreate, async (node: TreeNodeInfo) => + this.scriptNode(node, ScriptOperation.Create))); + + // Script as Drop + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdScriptDelete, async (node: TreeNodeInfo) => + this.scriptNode(node, ScriptOperation.Delete))); + + // Script as Execute + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdScriptExecute, async (node: TreeNodeInfo) => + this.scriptNode(node, ScriptOperation.Execute))); + + // Script as Alter + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdScriptAlter, async (node: TreeNodeInfo) => + this.scriptNode(node, ScriptOperation.Alter))); + } + + /** + * Initializes the Query History commands + */ + private initializeQueryHistory(): void { + // Register the query history tree provider + this._queryHistoryProvider = new QueryHistoryProvider(this._connectionMgr, this._outputContentProvider, + this._vscodeWrapper, this._untitledSqlDocumentService, this._statusview, this._prompter); + + this._context.subscriptions.push( + vscode.window.registerTreeDataProvider('queryHistory', this._queryHistoryProvider) + ); + + // Command to refresh Query History + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdRefreshQueryHistory, (ownerUri: string, hasError: boolean) => { + const config = this._vscodeWrapper.getConfiguration(Constants.extensionConfigSectionName); + let queryHistoryEnabled = config.get(Constants.configEnableQueryHistoryCapture); + if (queryHistoryEnabled) { + const timeStamp = new Date(); + this._queryHistoryProvider.refresh(ownerUri, timeStamp, hasError); + } + })); + + // Command to enable clear all entries in Query History + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdClearAllQueryHistory, () => { + this._queryHistoryProvider.clearAll(); + })); + + // Command to enable delete an entry in Query History + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdDeleteQueryHistory, (node: QueryHistoryNode) => { + this._queryHistoryProvider.deleteQueryHistoryEntry(node); + })); + + // Command to enable open a query in Query History + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdOpenQueryHistory, async (node: QueryHistoryNode) => { + await this._queryHistoryProvider.openQueryHistoryEntry(node); + })); + + // Command to enable run a query in Query History + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdRunQueryHistory, async (node: QueryHistoryNode) => { + await this._queryHistoryProvider.openQueryHistoryEntry(node, true); + })); + + // Command to start the query history capture + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdStartQueryHistory, async (node: QueryHistoryNode) => { + await this._queryHistoryProvider.startQueryHistoryCapture(); + })); + + // Command to pause the query history capture + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdPauseQueryHistory, async (node: QueryHistoryNode) => { + await this._queryHistoryProvider.pauseQueryHistoryCapture(); + })); + + // Command to open the query history experience in the command palette + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdCommandPaletteQueryHistory, () => { + this._queryHistoryProvider.showQueryHistoryCommandPalette(); + })); + } /** * Handles the command to enable SQLCMD mode diff --git a/src/controllers/queryRunner.ts b/src/controllers/queryRunner.ts index 45b5c5878..fa783c3a6 100644 --- a/src/controllers/queryRunner.ts +++ b/src/controllers/queryRunner.ts @@ -35,6 +35,7 @@ export interface IResultSet { columns: string[]; totalNumberOfRows: number; } + /* * Query Runner class which handles running a query, reports the results to the content manager, * and handles getting more rows from the service layer and disposing when the content is closed. @@ -49,6 +50,7 @@ export default class QueryRunner { private _isSqlCmd: boolean = false; public eventEmitter: EventEmitter = new EventEmitter(); private _uriToQueryPromiseMap = new Map>(); + private _uriToQueryStringMap = new Map(); // CONSTRUCTOR ///////////////////////////////////////////////////////// @@ -160,6 +162,18 @@ export default class QueryRunner { querySelection: selection }; + const doc = await this._vscodeWrapper.openTextDocument(this._vscodeWrapper.parseUri(this._ownerUri)); + let queryString = doc.getText(); + if (selection) { + let range = this._vscodeWrapper.range( + this._vscodeWrapper.position(selection.startLine, selection.startColumn), + this._vscodeWrapper.position(selection.endLine, selection.endColumn)); + queryString = doc.getText(range); + } + + // Set the query string for the uri + this._uriToQueryStringMap.set(this._ownerUri, queryString); + // Send the request to execute the query if (promise) { this._uriToQueryPromiseMap.set(this._ownerUri, promise); @@ -216,7 +230,9 @@ export default class QueryRunner { this._uriToQueryPromiseMap.delete(result.ownerUri); } this._statusView.executedQuery(result.ownerUri); - this.eventEmitter.emit('complete', Utils.parseNumAsTimeString(this._totalElapsedMilliseconds)); + let hasError = false; + hasError = this._batchSets.some(batch => batch.hasError === true); + this.eventEmitter.emit('complete', Utils.parseNumAsTimeString(this._totalElapsedMilliseconds), hasError); } public handleBatchStart(result: QueryExecuteBatchNotificationParams): void { @@ -576,6 +592,13 @@ export default class QueryRunner { } } + public getQueryString(uri: string): string { + if (this._uriToQueryStringMap.has(uri)) { + return this._uriToQueryStringMap.get(uri); + } + return undefined; + } + public resetHasCompleted(): void { this._hasCompleted = false; } diff --git a/src/controllers/vscodeWrapper.ts b/src/controllers/vscodeWrapper.ts index fc35fd4c3..8269e72f1 100644 --- a/src/controllers/vscodeWrapper.ts +++ b/src/controllers/vscodeWrapper.ts @@ -345,6 +345,13 @@ export default class VscodeWrapper { return this.getConfiguration(extensionName).update(resource, value, vscode.ConfigurationTarget.Global); } + /** + * Set a context for contributing command actions + */ + public async setContext(contextSection: string, value: any): Promise { + await this.executeCommand('setContext', contextSection, value); + } + /* * Called when there's a change in the extensions */ diff --git a/src/models/sqlOutputContentProvider.ts b/src/models/sqlOutputContentProvider.ts index 9274652d8..92695794a 100644 --- a/src/models/sqlOutputContentProvider.ts +++ b/src/models/sqlOutputContentProvider.ts @@ -214,7 +214,8 @@ export class SqlOutputContentProvider { queryRunner.eventEmitter.on('message', (message) => { this._panels.get(uri).proxy.sendEvent('message', message); }); - queryRunner.eventEmitter.on('complete', (totalMilliseconds) => { + queryRunner.eventEmitter.on('complete', (totalMilliseconds, hasError) => { + this._vscodeWrapper.executeCommand(Constants.cmdRefreshQueryHistory, uri, hasError); this._panels.get(uri).proxy.sendEvent('complete', totalMilliseconds); }); this._queryResultsMap.set(uri, new QueryRunnerState(queryRunner)); diff --git a/src/queryHistory/icons/status_error.svg b/src/queryHistory/icons/status_error.svg new file mode 100644 index 000000000..309574a48 --- /dev/null +++ b/src/queryHistory/icons/status_error.svg @@ -0,0 +1 @@ +globalerror_red \ No newline at end of file diff --git a/src/queryHistory/icons/status_success.svg b/src/queryHistory/icons/status_success.svg new file mode 100644 index 000000000..776e1fd90 --- /dev/null +++ b/src/queryHistory/icons/status_success.svg @@ -0,0 +1 @@ +success_16x16 \ No newline at end of file diff --git a/src/queryHistory/queryHistoryNode.ts b/src/queryHistory/queryHistoryNode.ts new file mode 100644 index 000000000..836c32015 --- /dev/null +++ b/src/queryHistory/queryHistoryNode.ts @@ -0,0 +1,79 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as LocalizedConstants from '../constants/localizedConstants'; +import { queryHistory } from '../constants/constants'; + +/** + * Empty Node shown when no queries are available + */ +export class EmptyHistoryNode extends vscode.TreeItem { + + private static readonly contextValue = 'emptyHistoryNode'; + + constructor() { + super(LocalizedConstants.msgNoQueriesAvailable, vscode.TreeItemCollapsibleState.None); + this.contextValue = EmptyHistoryNode.contextValue; + } +} + +/** + * Query history node + */ +export class QueryHistoryNode extends vscode.TreeItem { + + private static readonly contextValue = 'queryHistoryNode'; + private readonly iconsPath: string = path.join(__dirname, 'icons'); + private readonly successIcon: string = path.join(this.iconsPath, 'status_success.svg'); + private readonly failureIcon: string = path.join(this.iconsPath, 'status_error.svg'); + private _ownerUri: string; + private _timeStamp: Date; + private _isSuccess: boolean; + private _queryString: string; + private _connectionLabel: string; + + constructor( + label: string, + tooltip: string, + queryString: string, + ownerUri: string, + timeStamp: Date, + connectionLabel: string, + isSuccess: boolean + ) { + super(label, vscode.TreeItemCollapsibleState.None); + this._queryString = queryString; + this._ownerUri = ownerUri; + this._timeStamp = timeStamp; + this._isSuccess = isSuccess; + this._connectionLabel = connectionLabel; + this.iconPath = this._isSuccess ? this.successIcon : this.failureIcon; + this.tooltip = tooltip; + this.contextValue = QueryHistoryNode.contextValue; + } + + /** Getters */ + public get historyNodeLabel(): string { + return this.label; + } + + public get ownerUri(): string { + return this._ownerUri; + } + + public get timeStamp(): Date { + return this._timeStamp; + } + + public get queryString(): string { + return this._queryString; + } + + public get connectionLabel(): string { + return this._connectionLabel; + } +} diff --git a/src/queryHistory/queryHistoryProvider.ts b/src/queryHistory/queryHistoryProvider.ts new file mode 100644 index 000000000..8ad14b59f --- /dev/null +++ b/src/queryHistory/queryHistoryProvider.ts @@ -0,0 +1,207 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import * as vscode from 'vscode'; +import * as path from 'path'; +import ConnectionManager from '../controllers/connectionManager'; +import { SqlOutputContentProvider } from '../models/sqlOutputContentProvider'; +import { QueryHistoryNode, EmptyHistoryNode } from './queryHistoryNode'; +import VscodeWrapper from '../controllers/vscodeWrapper'; +import Constants = require('../constants/constants'); +import UntitledSqlDocumentService from '../controllers/untitledSqlDocumentService'; +import { Deferred } from '../protocol'; +import StatusView from '../views/statusView'; +import { IConnectionProfile } from '../models/interfaces'; +import { IPrompter } from '../prompts/question'; +import { QueryHistoryUI, QueryHistoryAction } from '../views/queryHistoryUI'; + +export class QueryHistoryProvider implements vscode.TreeDataProvider { + + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + private _queryHistoryNodes: vscode.TreeItem[] = [new EmptyHistoryNode()] + private _queryHistoryLimit: number; + private _queryHistoryEnabled: boolean; + private _queryHistoryUI: QueryHistoryUI + + constructor( + private _connectionManager: ConnectionManager, + private _outputContentProvider: SqlOutputContentProvider, + private _vscodeWrapper: VscodeWrapper, + private _untitledSqlDocumentService: UntitledSqlDocumentService, + private _statusView: StatusView, + private _prompter: IPrompter + ) { + const config = this._vscodeWrapper.getConfiguration(Constants.extensionConfigSectionName); + this._queryHistoryLimit = config.get(Constants.configQueryHistoryLimit); + this._queryHistoryEnabled = config.get(Constants.configEnableQueryHistoryCapture); + this._vscodeWrapper.setContext(Constants.isQueryHistoryEnabled, this._queryHistoryEnabled); + this._queryHistoryUI = new QueryHistoryUI(this._prompter, this._vscodeWrapper); + } + + clearAll(): void { + this._queryHistoryNodes = [new EmptyHistoryNode()]; + this._onDidChangeTreeData.fire(); + } + + refresh(ownerUri: string, timeStamp: Date, hasError): void { + const timeStampString = timeStamp.toLocaleString(); + const historyNodeLabel = this.createHistoryNodeLabel(ownerUri, timeStampString); + const tooltip = this.createHistoryNodeTooltip(ownerUri, timeStampString); + const queryString = this.getQueryString(ownerUri); + const connectionLabel = this.getConnectionLabel(ownerUri); + const node = new QueryHistoryNode(historyNodeLabel, tooltip, queryString, + ownerUri, timeStamp, connectionLabel, !hasError); + if (this._queryHistoryNodes.length === 1) { + if (this._queryHistoryNodes[0] instanceof EmptyHistoryNode) { + this._queryHistoryNodes = []; + } + } + this._queryHistoryNodes.push(node); + // Push out the first listing if it crosses limit to maintain + // an LRU order + if (this._queryHistoryNodes.length > this._queryHistoryLimit) { + this._queryHistoryNodes.shift(); + } + // return the query history sorted by timestamp + this._queryHistoryNodes.sort((a, b) => { + return (b as QueryHistoryNode).timeStamp.getTime()- + (a as QueryHistoryNode).timeStamp.getTime(); + }); + this._onDidChangeTreeData.fire(); + } + + getTreeItem(node: QueryHistoryNode): any { + return node; + } + + async getChildren(element?: any): Promise { + if (this._queryHistoryNodes.length == 0) { + this._queryHistoryNodes.push(new EmptyHistoryNode()); + } + return this._queryHistoryNodes; + } + + /** + * + */ + public async showQueryHistoryCommandPalette(): Promise { + const options = this._queryHistoryNodes.map(node => this._queryHistoryUI.convertToQuickPickItem(node)); + let queryHistoryQuickPickItem = await this._queryHistoryUI.showQueryHistoryCommandPalette(options); + this.openQueryHistoryEntry(queryHistoryQuickPickItem.node, queryHistoryQuickPickItem.action === + QueryHistoryAction.RunQueryHistoryAction); + } + + /** + * Starts the history capture by changing the setting + * and changes context for menu actions + */ + public async startQueryHistoryCapture(): Promise { + await this._vscodeWrapper.setConfiguration(Constants.extensionConfigSectionName, + Constants.configEnableQueryHistoryCapture, true); + await this._vscodeWrapper.setContext(Constants.isQueryHistoryEnabled, true); + } + + /** + * Pauses the history capture by changing the setting + * and changes context for menu actions + */ + public async pauseQueryHistoryCapture(): Promise { + await this._vscodeWrapper.setConfiguration(Constants.extensionConfigSectionName, + Constants.configEnableQueryHistoryCapture, false); + await this._vscodeWrapper.setContext(Constants.isQueryHistoryEnabled, false); + } + + /** + * Opens a query history listing in a new query window + */ + public async openQueryHistoryEntry(node: QueryHistoryNode, isExecute: boolean = false): Promise { + const editor = await this._untitledSqlDocumentService.newQuery(node.queryString); + let uri = editor.document.uri.toString(true); + let title = path.basename(editor.document.fileName); + const queryUriPromise = new Deferred(); + let credentials = this._connectionManager.getConnectionInfo(node.ownerUri).credentials; + await this._connectionManager.connect(uri, credentials, queryUriPromise); + await queryUriPromise; + this._statusView.languageFlavorChanged(uri, Constants.mssqlProviderName); + this._statusView.sqlCmdModeChanged(uri, false); + if (isExecute) { + const queryPromise = new Deferred(); + await this._outputContentProvider.runQuery(this._statusView, uri, undefined, title, queryPromise); + await queryPromise; + await this._connectionManager.connectionStore.removeRecentlyUsed(credentials); + } + } + + /** + * Deletes a query history entry for a URI + */ + public deleteQueryHistoryEntry(node: QueryHistoryNode): void { + let index = this._queryHistoryNodes.findIndex(n => { + let historyNode = n as QueryHistoryNode; + return historyNode === node; + }); + this._queryHistoryNodes.splice(index, 1); + this._onDidChangeTreeData.fire(); + } + + /** + * Getters + */ + public get queryHistoryNodes(): vscode.TreeItem[] { + return this._queryHistoryNodes; + } + + /** + * Limits the size of a string with ellipses in the middle + */ + public static limitStringSize(input: string, forCommandPalette: boolean = false): string { + if (!forCommandPalette) { + if (input.length > 25) { + return `${input.substr(0, 10)}...${input.substr(input.length-10, input.length)}` + } + } else { + if (input.length > 45) { + return `${input.substr(0, 20)}...${input.substr(input.length-20, input.length)}` + } + } + return input; + } + + /** + * Creates the node label for a query history node + */ + private createHistoryNodeLabel(ownerUri: string, timeStamp: string) { + const queryString = QueryHistoryProvider.limitStringSize(this.getQueryString(ownerUri)); + const connectionLabel = QueryHistoryProvider.limitStringSize(this.getConnectionLabel(ownerUri)); + return `${queryString}, ${connectionLabel}, ${timeStamp}`; + } + + /** + * Gets the selected text for the corresponding query history listing + */ + private getQueryString(ownerUri: string): string { + const queryRunner = this._outputContentProvider.getQueryRunner(ownerUri); + return queryRunner.getQueryString(ownerUri); + } + + /** + * Creates a connection label based on credentials + */ + private getConnectionLabel(ownerUri: string): string { + let connInfo = this._connectionManager.getConnectionInfo(ownerUri); + return `(${connInfo.credentials.server}|${connInfo.credentials.database})`; + } + + /** + * Creates a detailed tool tip when a node is hovered + */ + private createHistoryNodeTooltip(ownerUri: string, timeStamp: string): string { + const queryString = this.getQueryString(ownerUri); + const connectionLabel = this.getConnectionLabel(ownerUri); + return `${queryString}\n${connectionLabel}\n${timeStamp}`; + } +} diff --git a/src/views/queryHistoryUI.ts b/src/views/queryHistoryUI.ts new file mode 100644 index 000000000..54bec21d8 --- /dev/null +++ b/src/views/queryHistoryUI.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import VscodeWrapper from "../controllers/vscodeWrapper"; +import { IPrompter, IQuestion, QuestionTypes } from "../prompts/question"; +import { QueryHistoryProvider } from '../queryHistory/queryHistoryProvider'; +import { QueryHistoryNode } from '../queryHistory/queryHistoryNode'; +import * as LocalizedConstants from '../constants/localizedConstants'; + + +export enum QueryHistoryAction { + OpenQueryHistoryAction = 1, + RunQueryHistoryAction = 2 +}; + +export interface QueryHistoryQuickPickItem extends vscode.QuickPickItem { + node: QueryHistoryNode; + action: any; +}; + +export class QueryHistoryUI { + + constructor( + private _prompter: IPrompter, + private _vscodeWrapper: VscodeWrapper + ) {} + + public convertToQuickPickItem(node: vscode.TreeItem): QueryHistoryQuickPickItem { + let historyNode = node as QueryHistoryNode; + let quickPickItem: QueryHistoryQuickPickItem = { + label: QueryHistoryProvider.limitStringSize(historyNode.queryString, true), + detail: `${historyNode.connectionLabel}, ${historyNode.timeStamp.toLocaleString()}`, + node: historyNode, + action: undefined, + picked: false + }; + return quickPickItem; + } + + private showQueryHistoryActions(node: QueryHistoryNode): Promise { + let options = [{ label: LocalizedConstants.msgOpenQueryHistoryListing }, + { label: LocalizedConstants.msgRunQueryHistoryListing }]; + let question: IQuestion = { + type: QuestionTypes.expand, + name: 'question', + message: LocalizedConstants.msgChooseQueryHistoryAction, + choices: options + }; + return this._prompter.promptSingle(question).then((answer: vscode.QuickPickItem) => { + if (answer) { + return answer.label; + } + return undefined; + }); + } + + /** + * Shows the Query History List on the command palette + */ + public showQueryHistoryCommandPalette(options: vscode.QuickPickItem[]): Promise { + let question: IQuestion = { + type: QuestionTypes.expand, + name: 'question', + message: LocalizedConstants.msgChooseQueryHistoryListing, + choices: options + }; + return this._prompter.promptSingle(question).then((answer: QueryHistoryQuickPickItem) => { + if (answer) { + return this.showQueryHistoryActions(answer.node).then((actionAnswer: string) => { + if (actionAnswer === LocalizedConstants.msgOpenQueryHistoryListing) { + answer.action = QueryHistoryAction.OpenQueryHistoryAction; + } else if ( actionAnswer === LocalizedConstants.msgRunQueryHistoryListing) { + answer.action = QueryHistoryAction.RunQueryHistoryAction; + } + return answer; + }) + } + return undefined; + }); + } + +} \ No newline at end of file diff --git a/test/queryRunner.test.ts b/test/queryRunner.test.ts index c113fccbc..5a5dd51a9 100644 --- a/test/queryRunner.test.ts +++ b/test/queryRunner.test.ts @@ -72,6 +72,10 @@ suite('Query Runner tests', () => { // ... Mock up the view and VSCode wrapper to handle requests to update view testStatusView.setup(x => x.executingQuery(TypeMoq.It.isAnyString())); testVscodeWrapper.setup( x => x.logToOutputChannel(TypeMoq.It.isAnyString())); + let testDoc: vscode.TextDocument = { + getText() {} + } as any; + testVscodeWrapper.setup( x => x.openTextDocument(TypeMoq.It.isAny())).returns(() => Promise.resolve(testDoc)); // ... Mock up a event emitter to accept a start event (only) let mockEventEmitter = TypeMoq.Mock.ofType(EventEmitter, TypeMoq.MockBehavior.Loose); @@ -122,6 +126,10 @@ suite('Query Runner tests', () => { // ... Setup the vs code wrapper to handle output logging and error messages testVscodeWrapper.setup(x => x.logToOutputChannel(TypeMoq.It.isAnyString())); testVscodeWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAnyString())); + let testDoc: vscode.TextDocument = { + getText() {} + } as any; + testVscodeWrapper.setup( x => x.openTextDocument(TypeMoq.It.isAny())).returns(() => Promise.resolve(testDoc)); // ... Setup the event emitter to handle nothing let testEventEmitter = TypeMoq.Mock.ofType(EventEmitter, TypeMoq.MockBehavior.Strict); @@ -386,7 +394,7 @@ suite('Query Runner tests', () => { // Setup: // ... Create a mock for an event emitter that handles complete notifications let mockEventEmitter = TypeMoq.Mock.ofType(EventEmitter, TypeMoq.MockBehavior.Strict); - mockEventEmitter.setup(x => x.emit('complete', TypeMoq.It.isAnyString())); + mockEventEmitter.setup(x => x.emit('complete', TypeMoq.It.isAnyString(), TypeMoq.It.isAny())); // ... Setup the VS Code view handlers testStatusView.setup(x => x.executedQuery(TypeMoq.It.isAny())); @@ -426,7 +434,7 @@ suite('Query Runner tests', () => { testStatusView.verify(x => x.executedQuery(standardUri), TypeMoq.Times.once()); // ... The event emitter should have gotten a complete event - mockEventEmitter.verify(x => x.emit('complete', TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + mockEventEmitter.verify(x => x.emit('complete', TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once()); // ... The state of the query runner has been updated assert.equal(queryRunner.batchSets.length, 1);