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, '%': '%', };