Fix sort order and selection

This commit fixes two related issues with the
history view.

1. Sort order was changing after a query item completed. The fix is a
   change in how we fire off the `onDidChangeTreeData` event. When the
   event is fired with a single item, that item is pushed to the top of
   the list. I'm not exactly sure why this wasn't happening before, but
   I suspect it was because we were refreshing the list at the same time
   as we were inserting the new item.

   The solution here is to always refresh the entire list, instead of
   single items. This is fine since re building the list is a trivial
   operation. See the `refreshTreeView()` method.

   With this change, the sort order is now stable.

2. Originally reported here: #1093
   The problem is that the internal treeView selection was not being
   updated when a new item was being added. Due to some oddities with
   the way selection works in the tree view (ie- the visible selection
   does not always match the internal selection).

   The solution is to use the current item from the `treeDataProvider`
   in `determineSelection`.

Also, this change makes the sorting more precise and fixes some typos.
This commit is contained in:
Andrew Eisenberg
2022-01-27 14:58:28 -08:00
parent dc1bace4c6
commit 6dbb1a27b9
12 changed files with 64 additions and 56 deletions

View File

@@ -95,7 +95,7 @@ export class CompareInterfaceManager extends DisposableObject {
currentResultSetName: currentResultSetName, currentResultSetName: currentResultSetName,
rows, rows,
message, message,
datebaseUri: to.initialInfo.databaseInfo.databaseUri, databaseUri: to.initialInfo.databaseInfo.databaseUri,
}); });
} }
} }

View File

@@ -17,7 +17,7 @@ const emptyComparison: SetComparisonsMessage = {
columns: [], columns: [],
commonResultSetNames: [], commonResultSetNames: [],
currentResultSetName: '', currentResultSetName: '',
datebaseUri: '', databaseUri: '',
message: 'Empty comparison' message: 'Empty comparison'
}; };

View File

@@ -76,7 +76,7 @@ export default function CompareTable(props: Props) {
schemaName={comparison.currentResultSetName} schemaName={comparison.currentResultSetName}
preventSort={true} preventSort={true}
/> />
{createRows(rows.from, comparison.datebaseUri)} {createRows(rows.from, comparison.databaseUri)}
</table> </table>
</td> </td>
<td> <td>
@@ -86,7 +86,7 @@ export default function CompareTable(props: Props) {
schemaName={comparison.currentResultSetName} schemaName={comparison.currentResultSetName}
preventSort={true} preventSort={true}
/> />
{createRows(rows.to, comparison.datebaseUri)} {createRows(rows.to, comparison.databaseUri)}
</table> </table>
</td> </td>
</tr> </tr>

View File

@@ -501,8 +501,6 @@ async function activateWithInstalledDistribution(
const initialInfo = await createInitialQueryInfo(selectedQuery, databaseInfo, quickEval, range); const initialInfo = await createInitialQueryInfo(selectedQuery, databaseInfo, quickEval, range);
const item = new FullQueryInfo(initialInfo, queryHistoryConfigurationListener); const item = new FullQueryInfo(initialInfo, queryHistoryConfigurationListener);
qhm.addQuery(item); qhm.addQuery(item);
await qhm.refreshTreeView(item);
try { try {
const completedQueryInfo = await compileAndRunQueryAgainstDatabase( const completedQueryInfo = await compileAndRunQueryAgainstDatabase(
cliServer, cliServer,
@@ -520,7 +518,7 @@ async function activateWithInstalledDistribution(
item.failureReason = e.message; item.failureReason = e.message;
throw e; throw e;
} finally { } finally {
await qhm.refreshTreeView(item); qhm.refreshTreeView();
} }
} }
} }

View File

@@ -33,7 +33,7 @@ import { Logger } from './logging';
import * as messages from './pure/messages'; import * as messages from './pure/messages';
import { commandRunner } from './commandRunner'; import { commandRunner } from './commandRunner';
import { CompletedQueryInfo, interpretResults } from './query-results'; import { CompletedQueryInfo, interpretResults } from './query-results';
import { QueryEvaluatonInfo, tmpDir } from './run-queries'; import { QueryEvaluationInfo, tmpDir } from './run-queries';
import { parseSarifLocation, parseSarifPlainTextMessage } from './pure/sarif-utils'; import { parseSarifLocation, parseSarifPlainTextMessage } from './pure/sarif-utils';
import { import {
WebviewReveal, WebviewReveal,
@@ -644,7 +644,7 @@ export class InterfaceManager extends DisposableObject {
} }
private async interpretResultsInfo( private async interpretResultsInfo(
query: QueryEvaluatonInfo, query: QueryEvaluationInfo,
sortState: InterpretedResultsSortState | undefined sortState: InterpretedResultsSortState | undefined
): Promise<Interpretation | undefined> { ): Promise<Interpretation | undefined> {
if ( if (

View File

@@ -316,7 +316,7 @@ export interface SetComparisonsMessage {
readonly currentResultSetName: string; readonly currentResultSetName: string;
readonly rows: QueryCompareResult | undefined; readonly rows: QueryCompareResult | undefined;
readonly message: string | undefined; readonly message: string | undefined;
readonly datebaseUri: string; readonly databaseUri: string;
} }
export enum DiffKind { export enum DiffKind {

View File

@@ -96,9 +96,6 @@ export class HistoryTreeDataProvider extends DisposableObject {
private localSuccessIconPath: string; private localSuccessIconPath: string;
/**
* When not undefined, must be reference-equal to an item in `this.databases`.
*/
private current: FullQueryInfo | undefined; private current: FullQueryInfo | undefined;
constructor(extensionPath: string) { constructor(extensionPath: string) {
@@ -152,8 +149,8 @@ export class HistoryTreeDataProvider extends DisposableObject {
element?: FullQueryInfo element?: FullQueryInfo
): ProviderResult<FullQueryInfo[]> { ): ProviderResult<FullQueryInfo[]> {
return element ? [] : this.history.sort((h1, h2) => { return element ? [] : this.history.sort((h1, h2) => {
const q1 = h1.completedQuery; const resultCount1 = h1.completedQuery?.resultCount ?? -1;
const q2 = h2.completedQuery; const resultCount2 = h2.completedQuery?.resultCount ?? -1;
switch (this.sortOrder) { switch (this.sortOrder) {
case SortOrder.NameAsc: case SortOrder.NameAsc:
@@ -165,9 +162,15 @@ export class HistoryTreeDataProvider extends DisposableObject {
case SortOrder.DateDesc: case SortOrder.DateDesc:
return h2.initialInfo.start.getTime() - h1.initialInfo.start.getTime(); return h2.initialInfo.start.getTime() - h1.initialInfo.start.getTime();
case SortOrder.CountAsc: case SortOrder.CountAsc:
return (!q1 || !q2) ? 0 : q1.resultCount - q2.resultCount; // If the result counts are equal, sort by name.
return resultCount1 - resultCount2 === 0
? h1.label.localeCompare(h2.label, env.language)
: resultCount1 - resultCount2;
case SortOrder.CountDesc: case SortOrder.CountDesc:
return (!q1 || !q2) ? 0 : q2.resultCount - q1.resultCount; // If the result counts are equal, sort by name.
return resultCount2 - resultCount1 === 0
? h2.label.localeCompare(h1.label, env.language)
: resultCount2 - resultCount1;
default: default:
assertNever(this.sortOrder); assertNever(this.sortOrder);
} }
@@ -183,8 +186,8 @@ export class HistoryTreeDataProvider extends DisposableObject {
} }
pushQuery(item: FullQueryInfo): void { pushQuery(item: FullQueryInfo): void {
this.current = item;
this.history.push(item); this.history.push(item);
this.setCurrentItem(item);
this.refresh(); this.refresh();
} }
@@ -193,16 +196,17 @@ export class HistoryTreeDataProvider extends DisposableObject {
} }
remove(item: FullQueryInfo) { remove(item: FullQueryInfo) {
if (this.current === item) { const isCurrent = this.current === item;
this.current = undefined; if (isCurrent) {
this.setCurrentItem();
} }
const index = this.history.findIndex((i) => i === item); const index = this.history.findIndex((i) => i === item);
if (index >= 0) { if (index >= 0) {
this.history.splice(index, 1); this.history.splice(index, 1);
if (this.current === undefined && this.history.length > 0) { if (isCurrent && this.history.length > 0) {
// Try to keep a current item, near the deleted item if there // Try to keep a current item, near the deleted item if there
// are any available. // are any available.
this.current = this.history[Math.min(index, this.history.length - 1)]; this.setCurrentItem(this.history[Math.min(index, this.history.length - 1)]);
} }
this.refresh(); this.refresh();
} }
@@ -212,8 +216,8 @@ export class HistoryTreeDataProvider extends DisposableObject {
return this.history; return this.history;
} }
refresh(item?: FullQueryInfo) { refresh() {
this._onDidChangeTreeData.fire(item); this._onDidChangeTreeData.fire(undefined);
} }
public get sortOrder() { public get sortOrder() {
@@ -267,11 +271,13 @@ export class QueryHistoryManager extends DisposableObject {
this.updateTreeViewSelectionIfVisible() this.updateTreeViewSelectionIfVisible()
) )
); );
// Don't allow the selection to become empty
this.push( this.push(
this.treeView.onDidChangeSelection(async (ev) => { this.treeView.onDidChangeSelection(async (ev) => {
if (ev.selection.length == 0) { if (ev.selection.length === 0) {
// Don't allow the selection to become empty
this.updateTreeViewSelectionIfVisible(); this.updateTreeViewSelectionIfVisible();
} else {
this.treeDataProvider.setCurrentItem(ev.selection[0]);
} }
this.updateCompareWith(ev.selection); this.updateCompareWith(ev.selection);
}) })
@@ -433,12 +439,15 @@ export class QueryHistoryManager extends DisposableObject {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect); const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
(finalMultiSelect || [finalSingleItem]).forEach((item) => { (finalMultiSelect || [finalSingleItem]).forEach((item) => {
this.treeDataProvider.remove(item); // TODO: Removing in progress queries is not supported yet
item.completedQuery?.dispose(); if (item.status !== QueryStatus.InProgress) {
this.treeDataProvider.remove(item);
item.completedQuery?.dispose();
}
}); });
const current = this.treeDataProvider.getCurrent(); const current = this.treeDataProvider.getCurrent();
if (current !== undefined) { if (current !== undefined) {
await this.treeView.reveal(current); await this.treeView.reveal(current, { select: true });
await this.invokeCallbackOn(current); await this.invokeCallbackOn(current);
} }
} }
@@ -484,12 +493,7 @@ export class QueryHistoryManager extends DisposableObject {
if (response !== undefined) { if (response !== undefined) {
// Interpret empty string response as 'go back to using default' // Interpret empty string response as 'go back to using default'
singleItem.initialInfo.userSpecifiedLabel = response === '' ? undefined : response; singleItem.initialInfo.userSpecifiedLabel = response === '' ? undefined : response;
if (this.treeDataProvider.sortOrder === SortOrder.NameAsc || this.treeDataProvider.refresh();
this.treeDataProvider.sortOrder === SortOrder.NameDesc) {
this.treeDataProvider.refresh();
} else {
this.treeDataProvider.refresh(singleItem);
}
} }
} }
@@ -685,7 +689,7 @@ export class QueryHistoryManager extends DisposableObject {
// We must fire the onDidChangeTreeData event to ensure the current element can be selected // We must fire the onDidChangeTreeData event to ensure the current element can be selected
// using `reveal` if the tree view was not visible when the current element was added. // using `reveal` if the tree view was not visible when the current element was added.
this.treeDataProvider.refresh(); this.treeDataProvider.refresh();
void this.treeView.reveal(current); void this.treeView.reveal(current, { select: true });
} }
} }
} }
@@ -826,13 +830,19 @@ the file in the file explorer and dragging it into the workspace.`
singleItem: FullQueryInfo, singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[] multiSelect: FullQueryInfo[]
): { finalSingleItem: FullQueryInfo; finalMultiSelect: FullQueryInfo[] } { ): { finalSingleItem: FullQueryInfo; finalMultiSelect: FullQueryInfo[] } {
if (singleItem === undefined && (multiSelect === undefined || multiSelect.length === 0 || multiSelect[0] === undefined)) { if (!singleItem && !multiSelect?.[0]) {
const selection = this.treeView.selection; const selection = this.treeView.selection;
if (selection) { const current = this.treeDataProvider.getCurrent();
if (selection?.length) {
return { return {
finalSingleItem: selection[0], finalSingleItem: selection[0],
finalMultiSelect: selection finalMultiSelect: selection
}; };
} else if (current) {
return {
finalSingleItem: current,
finalMultiSelect: [current]
};
} }
} }
return { return {
@@ -841,7 +851,7 @@ the file in the file explorer and dragging it into the workspace.`
}; };
} }
async refreshTreeView(item: FullQueryInfo): Promise<void> { refreshTreeView(): void {
this.treeDataProvider.refresh(item); this.treeDataProvider.refresh();
} }
} }

View File

@@ -1,6 +1,6 @@
import { env } from 'vscode'; import { env } from 'vscode';
import { QueryWithResults, tmpDir, QueryEvaluatonInfo } from './run-queries'; import { QueryWithResults, tmpDir, QueryEvaluationInfo } from './run-queries';
import * as messages from './pure/messages'; import * as messages from './pure/messages';
import * as cli from './cli'; import * as cli from './cli';
import * as sarif from 'sarif'; import * as sarif from 'sarif';
@@ -39,7 +39,7 @@ export enum QueryStatus {
} }
export class CompletedQueryInfo implements QueryWithResults { export class CompletedQueryInfo implements QueryWithResults {
readonly query: QueryEvaluatonInfo; readonly query: QueryEvaluationInfo;
readonly result: messages.EvaluationResult; readonly result: messages.EvaluationResult;
readonly logFileLocation?: string; readonly logFileLocation?: string;
resultCount: number; resultCount: number;

View File

@@ -53,7 +53,7 @@ export const tmpDirDisposal = {
* temporary files associated with it, such as the compiled query * temporary files associated with it, such as the compiled query
* output and results. * output and results.
*/ */
export class QueryEvaluatonInfo { export class QueryEvaluationInfo {
readonly compiledQueryPath: string; readonly compiledQueryPath: string;
readonly dilPath: string; readonly dilPath: string;
readonly csvPath: string; readonly csvPath: string;
@@ -266,7 +266,7 @@ export class QueryEvaluatonInfo {
export interface QueryWithResults { export interface QueryWithResults {
readonly query: QueryEvaluatonInfo; readonly query: QueryEvaluationInfo;
readonly result: messages.EvaluationResult; readonly result: messages.EvaluationResult;
readonly logFileLocation?: string; readonly logFileLocation?: string;
readonly dispose: () => void; readonly dispose: () => void;
@@ -351,7 +351,7 @@ async function getSelectedPosition(editor: TextEditor, range?: Range): Promise<m
async function checkDbschemeCompatibility( async function checkDbschemeCompatibility(
cliServer: cli.CodeQLCliServer, cliServer: cli.CodeQLCliServer,
qs: qsClient.QueryServerClient, qs: qsClient.QueryServerClient,
query: QueryEvaluatonInfo, query: QueryEvaluationInfo,
progress: ProgressCallback, progress: ProgressCallback,
token: CancellationToken, token: CancellationToken,
): Promise<void> { ): Promise<void> {
@@ -393,7 +393,7 @@ async function checkDbschemeCompatibility(
} }
} }
function reportNoUpgradePath(query: QueryEvaluatonInfo) { function reportNoUpgradePath(query: QueryEvaluationInfo) {
throw new Error(`Query ${query.program.queryPath} expects database scheme ${query.queryDbscheme}, but the current database has a different scheme, and no database upgrades are available. The current database scheme may be newer than the CodeQL query libraries in your workspace.\n\nPlease try using a newer version of the query libraries.`); throw new Error(`Query ${query.program.queryPath} expects database scheme ${query.queryDbscheme}, but the current database has a different scheme, and no database upgrades are available. The current database scheme may be newer than the CodeQL query libraries in your workspace.\n\nPlease try using a newer version of the query libraries.`);
} }
@@ -403,7 +403,7 @@ function reportNoUpgradePath(query: QueryEvaluatonInfo) {
async function compileNonDestructiveUpgrade( async function compileNonDestructiveUpgrade(
qs: qsClient.QueryServerClient, qs: qsClient.QueryServerClient,
upgradeTemp: tmp.DirectoryResult, upgradeTemp: tmp.DirectoryResult,
query: QueryEvaluatonInfo, query: QueryEvaluationInfo,
progress: ProgressCallback, progress: ProgressCallback,
token: CancellationToken, token: CancellationToken,
): Promise<string> { ): Promise<string> {
@@ -614,7 +614,7 @@ export async function compileAndRunQueryAgainstDatabase(
} }
} }
const query = new QueryEvaluatonInfo(initialInfo.id, qlProgram, db, packConfig.dbscheme, initialInfo.quickEvalPosition, metadata, templates); const query = new QueryEvaluationInfo(initialInfo.id, qlProgram, db, packConfig.dbscheme, initialInfo.quickEvalPosition, metadata, templates);
const upgradeDir = await tmp.dir({ dir: upgradesTmpDir.name, unsafeCleanup: true }); const upgradeDir = await tmp.dir({ dir: upgradesTmpDir.name, unsafeCleanup: true });
try { try {
@@ -716,7 +716,7 @@ const compilationFailedErrorTail = ' compilation failed. Please make sure there
' and choose CodeQL Query Server from the dropdown.'; ' and choose CodeQL Query Server from the dropdown.';
function createSyntheticResult( function createSyntheticResult(
query: QueryEvaluatonInfo, query: QueryEvaluationInfo,
message: string, message: string,
resultType: number resultType: number
): QueryWithResults { ): QueryWithResults {

View File

@@ -6,7 +6,7 @@ import * as sinon from 'sinon';
import * as chaiAsPromised from 'chai-as-promised'; import * as chaiAsPromised from 'chai-as-promised';
import { logger } from '../../logging'; import { logger } from '../../logging';
import { QueryHistoryManager, HistoryTreeDataProvider } from '../../query-history'; import { QueryHistoryManager, HistoryTreeDataProvider } from '../../query-history';
import { QueryEvaluatonInfo, QueryWithResults } from '../../run-queries'; import { QueryEvaluationInfo, QueryWithResults } from '../../run-queries';
import { QueryHistoryConfigListener } from '../../config'; import { QueryHistoryConfigListener } from '../../config';
import * as messages from '../../pure/messages'; import * as messages from '../../pure/messages';
import { QueryServerClient } from '../../queryserver-client'; import { QueryServerClient } from '../../queryserver-client';
@@ -379,7 +379,7 @@ describe('query-history', () => {
return { return {
query: { query: {
hasInterpretedResults: () => Promise.resolve(hasInterpretedResults) hasInterpretedResults: () => Promise.resolve(hasInterpretedResults)
} as QueryEvaluatonInfo, } as QueryEvaluationInfo,
result: { result: {
resultType: didRunSuccessfully resultType: didRunSuccessfully
? messages.QueryResultType.SUCCESS ? messages.QueryResultType.SUCCESS

View File

@@ -6,7 +6,7 @@ import 'sinon-chai';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import * as chaiAsPromised from 'chai-as-promised'; import * as chaiAsPromised from 'chai-as-promised';
import { FullQueryInfo, InitialQueryInfo, interpretResults } from '../../query-results'; import { FullQueryInfo, InitialQueryInfo, interpretResults } from '../../query-results';
import { QueryEvaluatonInfo, QueryWithResults, tmpDir } from '../../run-queries'; import { QueryEvaluationInfo, QueryWithResults, tmpDir } from '../../run-queries';
import { QueryHistoryConfig } from '../../config'; import { QueryHistoryConfig } from '../../config';
import { EvaluationResult, QueryResultType } from '../../pure/messages'; import { EvaluationResult, QueryResultType } from '../../pure/messages';
import { SortDirection, SortedResultSetInfo } from '../../pure/interface-types'; import { SortDirection, SortedResultSetInfo } from '../../pure/interface-types';
@@ -248,7 +248,7 @@ describe('query-results', () => {
resultsPath: '/a/b/c', resultsPath: '/a/b/c',
interpretedResultsPath: '/d/e/f' interpretedResultsPath: '/d/e/f'
} }
} as QueryEvaluatonInfo, } as QueryEvaluationInfo,
result: { result: {
evaluationTime: 12340, evaluationTime: 12340,
resultType: didRunSuccessfully resultType: didRunSuccessfully

View File

@@ -5,7 +5,7 @@ import 'sinon-chai';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import * as chaiAsPromised from 'chai-as-promised'; import * as chaiAsPromised from 'chai-as-promised';
import { QueryEvaluatonInfo } from '../../run-queries'; import { QueryEvaluationInfo } from '../../run-queries';
import { QlProgram, Severity, compileQuery } from '../../pure/messages'; import { QlProgram, Severity, compileQuery } from '../../pure/messages';
import { DatabaseItem } from '../../databases'; import { DatabaseItem } from '../../databases';
@@ -13,7 +13,7 @@ chai.use(chaiAsPromised);
const expect = chai.expect; const expect = chai.expect;
describe('run-queries', () => { describe('run-queries', () => {
it('should create a QueryEvaluatonInfo', () => { it('should create a QueryEvaluationInfo', () => {
const info = createMockQueryInfo(); const info = createMockQueryInfo();
const queryID = info.queryID; const queryID = info.queryID;
@@ -86,7 +86,7 @@ describe('run-queries', () => {
let queryNum = 0; let queryNum = 0;
function createMockQueryInfo() { function createMockQueryInfo() {
return new QueryEvaluatonInfo( return new QueryEvaluationInfo(
queryNum++, queryNum++,
'my-program' as unknown as QlProgram, 'my-program' as unknown as QlProgram,
{ {