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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
\ 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 @@
+
\ 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);