diff --git a/extensions/ql-vscode/CHANGELOG.md b/extensions/ql-vscode/CHANGELOG.md
index beedc5ee6..b9378fe28 100644
--- a/extensions/ql-vscode/CHANGELOG.md
+++ b/extensions/ql-vscode/CHANGELOG.md
@@ -6,6 +6,7 @@
- Respect the `codeQL.runningQueries.numberOfThreads` setting when creating SARIF files during result interpretation. [#771](https://github.com/github/vscode-codeql/pull/771)
- Allow using raw LGTM project slugs for fetching LGTM databases. [#769](https://github.com/github/vscode-codeql/pull/769)
- Better error messages when BQRS interpretation fails to produce SARIF. [#770](https://github.com/github/vscode-codeql/pull/770)
+- Implement sorting of the query history view by name, date, and results count. [#777](https://github.com/github/vscode-codeql/pull/777)
## 1.4.3 - 22 February 2021
diff --git a/extensions/ql-vscode/media/dark/sort-num.svg b/extensions/ql-vscode/media/dark/sort-num.svg
new file mode 100644
index 000000000..c5dfb149e
--- /dev/null
+++ b/extensions/ql-vscode/media/dark/sort-num.svg
@@ -0,0 +1,15 @@
+
+
+
diff --git a/extensions/ql-vscode/media/light/sort-num.svg b/extensions/ql-vscode/media/light/sort-num.svg
new file mode 100644
index 000000000..9007ec80d
--- /dev/null
+++ b/extensions/ql-vscode/media/light/sort-num.svg
@@ -0,0 +1,15 @@
+
+
+
diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json
index 76152abc1..8b6a783f3 100644
--- a/extensions/ql-vscode/package.json
+++ b/extensions/ql-vscode/package.json
@@ -179,8 +179,8 @@
},
"codeQL.queryHistory.format": {
"type": "string",
- "default": "[%t] %q on %d - %s",
- "description": "Default string for how to label query history items. %t is the time of the query, %q is the query name, %d is the database name, and %s is a status string."
+ "default": "%q on %d - %s, %r result count [%t]",
+ "description": "Default string for how to label query history items. %t is the time of the query, %q is the query name, %d is the database name, %r is the number of results, and %s is a status string."
},
"codeQL.runningTests.numberOfThreads": {
"scope": "window",
@@ -357,6 +357,30 @@
"dark": "media/dark/trash.svg"
}
},
+ {
+ "command": "codeQLQueryHistory.sortByName",
+ "title": "Sort by Name",
+ "icon": {
+ "light": "media/light/sort-alpha.svg",
+ "dark": "media/dark/sort-alpha.svg"
+ }
+ },
+ {
+ "command": "codeQLQueryHistory.sortByDate",
+ "title": "Sort by Query Date",
+ "icon": {
+ "light": "media/light/sort-date.svg",
+ "dark": "media/dark/sort-date.svg"
+ }
+ },
+ {
+ "command": "codeQLQueryHistory.sortByCount",
+ "title": "Sort by Results Count",
+ "icon": {
+ "light": "media/light/sort-num.svg",
+ "dark": "media/dark/sort-num.svg"
+ }
+ },
{
"command": "codeQLQueryHistory.showQueryLog",
"title": "Show Query Log"
@@ -461,6 +485,21 @@
"when": "view == codeQLQueryHistory",
"group": "navigation"
},
+ {
+ "command": "codeQLQueryHistory.sortByName",
+ "when": "view == codeQLQueryHistory",
+ "group": "navigation"
+ },
+ {
+ "command": "codeQLQueryHistory.sortByDate",
+ "when": "view == codeQLQueryHistory",
+ "group": "navigation"
+ },
+ {
+ "command": "codeQLQueryHistory.sortByCount",
+ "when": "view == codeQLQueryHistory",
+ "group": "navigation"
+ },
{
"command": "codeQLAstViewer.clear",
"when": "view == codeQLAstViewer",
@@ -666,6 +705,18 @@
"command": "codeQLQueryHistory.compareWith",
"when": "false"
},
+ {
+ "command": "codeQLQueryHistory.sortByName",
+ "when": "false"
+ },
+ {
+ "command": "codeQLQueryHistory.sortByDate",
+ "when": "false"
+ },
+ {
+ "command": "codeQLQueryHistory.sortByCount",
+ "when": "false"
+ },
{
"command": "codeQLAstViewer.gotoCode",
"when": "false"
diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts
index 1418d274e..09041070e 100644
--- a/extensions/ql-vscode/src/extension.ts
+++ b/extensions/ql-vscode/src/extension.ts
@@ -472,12 +472,11 @@ async function activateWithInstalledDistribution(
progress,
token
);
- const item = qhm.addQuery(info);
+ const item = qhm.buildCompletedQuery(info);
await showResultsForCompletedQuery(item, WebviewReveal.NotForced);
- // The call to showResults potentially creates SARIF file;
- // Update the tree item context value to allow viewing that
- // SARIF file from context menu.
- await qhm.refreshTreeView(item);
+ // Note we must update the query history view after showing results as the
+ // display and sorting might depend on the number of results
+ await qhm.addCompletedQuery(item);
}
}
diff --git a/extensions/ql-vscode/src/interface.ts b/extensions/ql-vscode/src/interface.ts
index 436cb07e5..a2cb86efb 100644
--- a/extensions/ql-vscode/src/interface.ts
+++ b/extensions/ql-vscode/src/interface.ts
@@ -400,6 +400,7 @@ export class InterfaceManager extends DisposableObject {
}
);
const resultSet = transformBqrsResultSet(schema, chunk);
+ results.setResultCount(interpretationPage?.numTotalResults || resultSet.schema.rows);
const parsedResultSets: ParsedResultSets = {
pageNumber: 0,
pageSize,
diff --git a/extensions/ql-vscode/src/query-history.ts b/extensions/ql-vscode/src/query-history.ts
index e2a88fafb..0be37aaa3 100644
--- a/extensions/ql-vscode/src/query-history.ts
+++ b/extensions/ql-vscode/src/query-history.ts
@@ -1,6 +1,6 @@
import * as path from 'path';
import * as vscode from 'vscode';
-import { window as Window } from 'vscode';
+import { window as Window, env } from 'vscode';
import { CompletedQuery } from './query-results';
import { QueryHistoryConfig } from './config';
import { QueryWithResults } from './run-queries';
@@ -15,6 +15,7 @@ import { URLSearchParams } from 'url';
import { QueryServerClient } from './queryserver-client';
import { DisposableObject } from './pure/disposable-object';
import { commandRunner } from './commandRunner';
+import { assertNever } from './pure/helpers-pure';
/**
* query-history.ts
@@ -58,10 +59,21 @@ const SHOW_QUERY_TEXT_QUICK_EVAL_MSG = `\
*/
const FAILED_QUERY_HISTORY_ITEM_ICON = 'media/red-x.svg';
+enum SortOrder {
+ NameAsc = 'NameAsc',
+ NameDesc = 'NameDesc',
+ DateAsc = 'DateAsc',
+ DateDesc = 'DateDesc',
+ CountAsc = 'CountAsc',
+ CountDesc = 'CountDesc',
+}
+
/**
* Tree data provider for the query history view.
*/
export class HistoryTreeDataProvider extends DisposableObject {
+ private _sortOrder = SortOrder.DateAsc;
+
private _onDidChangeTreeData = super.push(new vscode.EventEmitter());
readonly onDidChangeTreeData: vscode.Event = this
@@ -111,7 +123,24 @@ export class HistoryTreeDataProvider extends DisposableObject {
getChildren(
element?: CompletedQuery
): vscode.ProviderResult {
- return element ? [] : this.history;
+ return element ? [] : this.history.sort((q1, q2) => {
+ switch (this.sortOrder) {
+ case SortOrder.NameAsc:
+ return q1.toString().localeCompare(q2.toString(), env.language);
+ case SortOrder.NameDesc:
+ return q2.toString().localeCompare(q1.toString(), env.language);
+ case SortOrder.DateAsc:
+ return q1.date.getTime() - q2.date.getTime();
+ case SortOrder.DateDesc:
+ return q2.date.getTime() - q1.date.getTime();
+ case SortOrder.CountAsc:
+ return q1.resultCount - q2.resultCount;
+ case SortOrder.CountDesc:
+ return q2.resultCount - q1.resultCount;
+ default:
+ assertNever(this.sortOrder);
+ }
+ });
}
getParent(_element: CompletedQuery): vscode.ProviderResult {
@@ -157,6 +186,15 @@ export class HistoryTreeDataProvider extends DisposableObject {
find(queryId: number): CompletedQuery | undefined {
return this.allHistory.find((query) => query.query.queryID === queryId);
}
+
+ public get sortOrder() {
+ return this._sortOrder;
+ }
+
+ public set sortOrder(newSortOrder: SortOrder) {
+ this._sortOrder = newSortOrder;
+ this._onDidChangeTreeData.fire();
+ }
}
/**
@@ -224,6 +262,24 @@ export class QueryHistoryManager extends DisposableObject {
this.handleRemoveHistoryItem.bind(this)
)
);
+ this.push(
+ commandRunner(
+ 'codeQLQueryHistory.sortByName',
+ this.handleSortByName.bind(this)
+ )
+ );
+ this.push(
+ commandRunner(
+ 'codeQLQueryHistory.sortByDate',
+ this.handleSortByDate.bind(this)
+ )
+ );
+ this.push(
+ commandRunner(
+ 'codeQLQueryHistory.sortByCount',
+ this.handleSortByCount.bind(this)
+ )
+ );
this.push(
commandRunner(
'codeQLQueryHistory.setLabel',
@@ -345,6 +401,30 @@ export class QueryHistoryManager extends DisposableObject {
}
}
+ async handleSortByName() {
+ if (this.treeDataProvider.sortOrder === SortOrder.NameAsc) {
+ this.treeDataProvider.sortOrder = SortOrder.NameDesc;
+ } else {
+ this.treeDataProvider.sortOrder = SortOrder.NameAsc;
+ }
+ }
+
+ async handleSortByDate() {
+ if (this.treeDataProvider.sortOrder === SortOrder.DateAsc) {
+ this.treeDataProvider.sortOrder = SortOrder.DateDesc;
+ } else {
+ this.treeDataProvider.sortOrder = SortOrder.DateAsc;
+ }
+ }
+
+ async handleSortByCount() {
+ if (this.treeDataProvider.sortOrder === SortOrder.CountAsc) {
+ this.treeDataProvider.sortOrder = SortOrder.CountDesc;
+ } else {
+ this.treeDataProvider.sortOrder = SortOrder.CountAsc;
+ }
+ }
+
async handleSetLabel(
singleItem: CompletedQuery,
multiSelect: CompletedQuery[]
@@ -362,7 +442,12 @@ export class QueryHistoryManager extends DisposableObject {
if (response !== undefined) {
// Interpret empty string response as 'go back to using default'
singleItem.options.label = response === '' ? undefined : response;
- this.treeDataProvider.refresh(singleItem);
+ if (this.treeDataProvider.sortOrder === SortOrder.NameAsc ||
+ this.treeDataProvider.sortOrder === SortOrder.NameDesc) {
+ this.treeDataProvider.refresh();
+ } else {
+ this.treeDataProvider.refresh(singleItem);
+ }
}
}
@@ -511,11 +596,14 @@ export class QueryHistoryManager extends DisposableObject {
}
}
- addQuery(info: QueryWithResults): CompletedQuery {
+ buildCompletedQuery(info: QueryWithResults): CompletedQuery {
const item = new CompletedQuery(info, this.queryHistoryConfigListener);
+ return item;
+ }
+
+ addCompletedQuery(item: CompletedQuery) {
this.treeDataProvider.pushQuery(item);
this.updateTreeViewSelectionIfVisible();
- return item;
}
find(queryId: number): CompletedQuery | undefined {
diff --git a/extensions/ql-vscode/src/query-results.ts b/extensions/ql-vscode/src/query-results.ts
index 1e6e1dd83..8e2666225 100644
--- a/extensions/ql-vscode/src/query-results.ts
+++ b/extensions/ql-vscode/src/query-results.ts
@@ -11,12 +11,14 @@ import { QueryHistoryConfig } from './config';
import { QueryHistoryItemOptions } from './query-history';
export class CompletedQuery implements QueryWithResults {
+ readonly date: Date;
readonly time: string;
readonly query: QueryInfo;
readonly result: messages.EvaluationResult;
readonly database: DatabaseInfo;
readonly logFileLocation?: string;
options: QueryHistoryItemOptions;
+ resultCount: number;
dispose: () => void;
/**
@@ -44,8 +46,14 @@ export class CompletedQuery implements QueryWithResults {
this.options = evaluation.options;
this.dispose = evaluation.dispose;
- this.time = new Date().toLocaleString(env.language);
+ this.date = new Date();
+ this.time = this.date.toLocaleString(env.language);
this.sortedResultsInfo = new Map();
+ this.resultCount = 0;
+ }
+
+ setResultCount(value: number) {
+ this.resultCount = value;
}
get databaseName(): string {
@@ -80,11 +88,12 @@ export class CompletedQuery implements QueryWithResults {
}
interpolate(template: string): string {
- const { databaseName, queryName, time, statusString } = this;
+ const { databaseName, queryName, time, resultCount, statusString } = this;
const replacements: { [k: string]: string } = {
t: time,
q: queryName,
d: databaseName,
+ r: resultCount.toString(),
s: statusString,
'%': '%',
};