Merge pull request #2287 from github/robertbrignull/selection

Introduce helpers for commands that operate on selections
This commit is contained in:
Robert
2023-04-27 11:38:22 +01:00
committed by GitHub
6 changed files with 301 additions and 448 deletions

View File

@@ -0,0 +1,51 @@
import { showAndLogErrorMessage } from "../helpers";
import {
ExplorerSelectionCommandFunction,
TreeViewContextMultiSelectionCommandFunction,
TreeViewContextSingleSelectionCommandFunction,
} from "./commands";
// A hack to match types that are not an array, which is useful to help avoid
// misusing createSingleSelectionCommand, e.g. where T accidentally gets instantiated
// as DatabaseItem[] instead of DatabaseItem.
type NotArray = object & { length?: never };
// A way to get the type system to help assert that one type is a supertype of another.
type CreateSupertypeOf<Super, Sub extends Super> = Sub;
// This asserts that SelectionCommand is assignable to all of the different types of
// SelectionCommand defined in commands.ts. The intention is the output from the helpers
// in this file can be used with any of the select command types and can handle any of
// the inputs.
type SelectionCommand<T extends NotArray> = CreateSupertypeOf<
TreeViewContextMultiSelectionCommandFunction<T> &
TreeViewContextSingleSelectionCommandFunction<T> &
ExplorerSelectionCommandFunction<T>,
(singleItem: T, multiSelect?: T[] | undefined) => Promise<void>
>;
export function createSingleSelectionCommand<T extends NotArray>(
f: (argument: T) => Promise<void>,
itemName: string,
): SelectionCommand<T> {
return async (singleItem, multiSelect) => {
if (multiSelect === undefined || multiSelect.length === 1) {
return f(singleItem);
} else {
void showAndLogErrorMessage(`Please select a single ${itemName}.`);
return;
}
};
}
export function createMultiSelectionCommand<T extends NotArray>(
f: (argument: T[]) => Promise<void>,
): SelectionCommand<T> {
return async (singleItem, multiSelect) => {
if (multiSelect !== undefined && multiSelect.length > 0) {
return f(multiSelect);
} else {
return f([singleItem]);
}
};
}

View File

@@ -46,6 +46,10 @@ import { isCanary } from "../config";
import { App } from "../common/app";
import { redactableError } from "../pure/errors";
import { LocalDatabasesCommands } from "../common/commands";
import {
createMultiSelectionCommand,
createSingleSelectionCommand,
} from "../common/selection-commands";
enum SortOrder {
NameAsc = "NameAsc",
@@ -240,11 +244,22 @@ export class DatabaseUI extends DisposableObject {
this.handleMakeCurrentDatabase.bind(this),
"codeQLDatabases.sortByName": this.handleSortByName.bind(this),
"codeQLDatabases.sortByDateAdded": this.handleSortByDateAdded.bind(this),
"codeQLDatabases.removeDatabase": this.handleRemoveDatabase.bind(this),
"codeQLDatabases.upgradeDatabase": this.handleUpgradeDatabase.bind(this),
"codeQLDatabases.renameDatabase": this.handleRenameDatabase.bind(this),
"codeQLDatabases.openDatabaseFolder": this.handleOpenFolder.bind(this),
"codeQLDatabases.addDatabaseSource": this.handleAddSource.bind(this),
"codeQLDatabases.removeDatabase": createMultiSelectionCommand(
this.handleRemoveDatabase.bind(this),
),
"codeQLDatabases.upgradeDatabase": createMultiSelectionCommand(
this.handleUpgradeDatabase.bind(this),
),
"codeQLDatabases.renameDatabase": createSingleSelectionCommand(
this.handleRenameDatabase.bind(this),
"database",
),
"codeQLDatabases.openDatabaseFolder": createMultiSelectionCommand(
this.handleOpenFolder.bind(this),
),
"codeQLDatabases.addDatabaseSource": createMultiSelectionCommand(
this.handleAddSource.bind(this),
),
"codeQLDatabases.removeOrphanedDatabases":
this.handleRemoveOrphanedDatabases.bind(this),
};
@@ -515,12 +530,11 @@ export class DatabaseUI extends DisposableObject {
private async handleUpgradeCurrentDatabase(): Promise<void> {
return withProgress(
async (progress, token) => {
await this.handleUpgradeDatabaseInternal(
progress,
token,
this.databaseManager.currentDatabaseItem,
[],
);
if (this.databaseManager.currentDatabaseItem !== undefined) {
await this.handleUpgradeDatabasesInternal(progress, token, [
this.databaseManager.currentDatabaseItem,
]);
}
},
{
title: "Upgrading current database",
@@ -530,16 +544,14 @@ export class DatabaseUI extends DisposableObject {
}
private async handleUpgradeDatabase(
databaseItem: DatabaseItem | undefined,
multiSelect: DatabaseItem[] | undefined,
databaseItems: DatabaseItem[],
): Promise<void> {
return withProgress(
async (progress, token) => {
return await this.handleUpgradeDatabaseInternal(
return await this.handleUpgradeDatabasesInternal(
progress,
token,
databaseItem,
multiSelect,
databaseItems,
);
},
{
@@ -549,46 +561,37 @@ export class DatabaseUI extends DisposableObject {
);
}
private async handleUpgradeDatabaseInternal(
private async handleUpgradeDatabasesInternal(
progress: ProgressCallback,
token: CancellationToken,
databaseItem: DatabaseItem | undefined,
multiSelect: DatabaseItem[] | undefined,
databaseItems: DatabaseItem[],
): Promise<void> {
if (multiSelect?.length) {
await Promise.all(
multiSelect.map((dbItem) =>
this.handleUpgradeDatabaseInternal(progress, token, dbItem, []),
),
);
}
if (this.queryServer === undefined) {
throw new Error(
"Received request to upgrade database, but there is no running query server.",
);
}
if (databaseItem === undefined) {
throw new Error(
"Received request to upgrade database, but no database was provided.",
);
}
if (databaseItem.contents === undefined) {
throw new Error(
"Received request to upgrade database, but database contents could not be found.",
);
}
if (databaseItem.contents.dbSchemeUri === undefined) {
throw new Error(
"Received request to upgrade database, but database has no schema.",
);
}
await Promise.all(
databaseItems.map(async (databaseItem) => {
if (this.queryServer === undefined) {
throw new Error(
"Received request to upgrade database, but there is no running query server.",
);
}
if (databaseItem.contents === undefined) {
throw new Error(
"Received request to upgrade database, but database contents could not be found.",
);
}
if (databaseItem.contents.dbSchemeUri === undefined) {
throw new Error(
"Received request to upgrade database, but database has no schema.",
);
}
// Search for upgrade scripts in any workspace folders available
// Search for upgrade scripts in any workspace folders available
await this.queryServer.upgradeDatabaseExplicit(
databaseItem,
progress,
token,
await this.queryServer.upgradeDatabaseExplicit(
databaseItem,
progress,
token,
);
}),
);
}
@@ -651,24 +654,15 @@ export class DatabaseUI extends DisposableObject {
}
private async handleRemoveDatabase(
databaseItem: DatabaseItem,
multiSelect: DatabaseItem[] | undefined,
databaseItems: DatabaseItem[],
): Promise<void> {
return withProgress(
async (progress, token) => {
if (multiSelect?.length) {
await Promise.all(
multiSelect.map((dbItem) =>
this.databaseManager.removeDatabaseItem(progress, token, dbItem),
),
);
} else {
await this.databaseManager.removeDatabaseItem(
progress,
token,
databaseItem,
);
}
await Promise.all(
databaseItems.map((dbItem) =>
this.databaseManager.removeDatabaseItem(progress, token, dbItem),
),
);
},
{
title: "Removing database",
@@ -679,10 +673,7 @@ export class DatabaseUI extends DisposableObject {
private async handleRenameDatabase(
databaseItem: DatabaseItem,
multiSelect: DatabaseItem[] | undefined,
): Promise<void> {
this.assertSingleDatabase(multiSelect);
const newName = await window.showInputBox({
prompt: "Choose new database name",
value: databaseItem.name,
@@ -693,17 +684,10 @@ export class DatabaseUI extends DisposableObject {
}
}
private async handleOpenFolder(
databaseItem: DatabaseItem,
multiSelect: DatabaseItem[] | undefined,
): Promise<void> {
if (multiSelect?.length) {
await Promise.all(
multiSelect.map((dbItem) => env.openExternal(dbItem.databaseUri)),
);
} else {
await env.openExternal(databaseItem.databaseUri);
}
private async handleOpenFolder(databaseItems: DatabaseItem[]): Promise<void> {
await Promise.all(
databaseItems.map((dbItem) => env.openExternal(dbItem.databaseUri)),
);
}
/**
@@ -711,16 +695,9 @@ export class DatabaseUI extends DisposableObject {
* When a database is first added in the "Databases" view, its source folder is added to the workspace.
* If the source folder is removed from the workspace for some reason, we want to be able to re-add it if need be.
*/
private async handleAddSource(
databaseItem: DatabaseItem,
multiSelect: DatabaseItem[] | undefined,
): Promise<void> {
if (multiSelect?.length) {
for (const dbItem of multiSelect) {
await this.databaseManager.addDatabaseSourceArchiveFolder(dbItem);
}
} else {
await this.databaseManager.addDatabaseSourceArchiveFolder(databaseItem);
private async handleAddSource(databaseItems: DatabaseItem[]): Promise<void> {
for (const dbItem of databaseItems) {
await this.databaseManager.addDatabaseSourceArchiveFolder(dbItem);
}
}
@@ -823,13 +800,4 @@ export class DatabaseUI extends DisposableObject {
}
return Uri.file(dbPath);
}
private assertSingleDatabase(
multiSelect: DatabaseItem[] = [],
message = "Please select a single database.",
) {
if (multiSelect.length > 1) {
throw new Error(message);
}
}
}

View File

@@ -43,6 +43,7 @@ import { App } from "../common/app";
import { DisposableObject } from "../pure/disposable-object";
import { SkeletonQueryWizard } from "../skeleton-query-wizard";
import { LocalQueryRun } from "./local-query-run";
import { createMultiSelectionCommand } from "../common/selection-commands";
interface DatabaseQuickPickItem extends QuickPickItem {
databaseItem: DatabaseItem;
@@ -89,7 +90,9 @@ export class LocalQueries extends DisposableObject {
this.runQueryOnMultipleDatabases.bind(this),
"codeQL.runQueryOnMultipleDatabasesContextEditor":
this.runQueryOnMultipleDatabases.bind(this),
"codeQL.runQueries": this.runQueries.bind(this),
"codeQL.runQueries": createMultiSelectionCommand(
this.runQueries.bind(this),
),
"codeQL.quickEval": this.quickEval.bind(this),
"codeQL.quickEvalContextEditor": this.quickEval.bind(this),
"codeQL.codeLensQuickEval": this.codeLensQuickEval.bind(this),
@@ -130,12 +133,12 @@ export class LocalQueries extends DisposableObject {
);
}
private async runQueries(_: unknown, multi: Uri[]): Promise<void> {
private async runQueries(fileURIs: Uri[]): Promise<void> {
await withProgress(
async (progress, token) => {
const maxQueryCount = MAX_QUERIES.getValue() as number;
const [files, dirFound] = await gatherQlFiles(
multi.map((uri) => uri.fsPath),
fileURIs.map((uri) => uri.fsPath),
);
if (files.length > maxQueryCount) {
throw new Error(

View File

@@ -56,6 +56,10 @@ import { QueryHistoryDirs } from "./query-history-dirs";
import { QueryHistoryCommands } from "../common/commands";
import { App } from "../common/app";
import { tryOpenExternalFile } from "../vscode-utils/external-files";
import {
createMultiSelectionCommand,
createSingleSelectionCommand,
} from "../common/selection-commands";
/**
* query-history-manager.ts
@@ -232,33 +236,78 @@ export class QueryHistoryManager extends DisposableObject {
"codeQLQueryHistory.sortByDate": this.handleSortByDate.bind(this),
"codeQLQueryHistory.sortByCount": this.handleSortByCount.bind(this),
"codeQLQueryHistory.openQueryContextMenu":
"codeQLQueryHistory.openQueryContextMenu": createSingleSelectionCommand(
this.handleOpenQuery.bind(this),
"query",
),
"codeQLQueryHistory.removeHistoryItemContextMenu":
this.handleRemoveHistoryItem.bind(this),
createMultiSelectionCommand(this.handleRemoveHistoryItem.bind(this)),
"codeQLQueryHistory.removeHistoryItemContextInline":
this.handleRemoveHistoryItem.bind(this),
"codeQLQueryHistory.renameItem": this.handleRenameItem.bind(this),
createMultiSelectionCommand(this.handleRemoveHistoryItem.bind(this)),
"codeQLQueryHistory.renameItem": createSingleSelectionCommand(
this.handleRenameItem.bind(this),
"query",
),
"codeQLQueryHistory.compareWith": this.handleCompareWith.bind(this),
"codeQLQueryHistory.showEvalLog": this.handleShowEvalLog.bind(this),
"codeQLQueryHistory.showEvalLogSummary":
"codeQLQueryHistory.showEvalLog": createSingleSelectionCommand(
this.handleShowEvalLog.bind(this),
"query",
),
"codeQLQueryHistory.showEvalLogSummary": createSingleSelectionCommand(
this.handleShowEvalLogSummary.bind(this),
"codeQLQueryHistory.showEvalLogViewer":
"query",
),
"codeQLQueryHistory.showEvalLogViewer": createSingleSelectionCommand(
this.handleShowEvalLogViewer.bind(this),
"codeQLQueryHistory.showQueryLog": this.handleShowQueryLog.bind(this),
"codeQLQueryHistory.showQueryText": this.handleShowQueryText.bind(this),
"codeQLQueryHistory.openQueryDirectory":
"query",
),
"codeQLQueryHistory.showQueryLog": createSingleSelectionCommand(
this.handleShowQueryLog.bind(this),
"query",
),
"codeQLQueryHistory.showQueryText": createSingleSelectionCommand(
this.handleShowQueryText.bind(this),
"query",
),
"codeQLQueryHistory.openQueryDirectory": createSingleSelectionCommand(
this.handleOpenQueryDirectory.bind(this),
"codeQLQueryHistory.cancel": this.handleCancel.bind(this),
"codeQLQueryHistory.exportResults": this.handleExportResults.bind(this),
"codeQLQueryHistory.viewCsvResults": this.handleViewCsvResults.bind(this),
"codeQLQueryHistory.viewCsvAlerts": this.handleViewCsvAlerts.bind(this),
"codeQLQueryHistory.viewSarifAlerts":
"query",
),
"codeQLQueryHistory.cancel": createMultiSelectionCommand(
this.handleCancel.bind(this),
),
"codeQLQueryHistory.exportResults": createSingleSelectionCommand(
this.handleExportResults.bind(this),
"query",
),
"codeQLQueryHistory.viewCsvResults": createSingleSelectionCommand(
this.handleViewCsvResults.bind(this),
"query",
),
"codeQLQueryHistory.viewCsvAlerts": createSingleSelectionCommand(
this.handleViewCsvAlerts.bind(this),
"query",
),
"codeQLQueryHistory.viewSarifAlerts": createSingleSelectionCommand(
this.handleViewSarifAlerts.bind(this),
"codeQLQueryHistory.viewDil": this.handleViewDil.bind(this),
"codeQLQueryHistory.itemClicked": this.handleItemClicked.bind(this),
"codeQLQueryHistory.openOnGithub": this.handleOpenOnGithub.bind(this),
"codeQLQueryHistory.copyRepoList": this.handleCopyRepoList.bind(this),
"query",
),
"codeQLQueryHistory.viewDil": createSingleSelectionCommand(
this.handleViewDil.bind(this),
"query",
),
"codeQLQueryHistory.itemClicked": createSingleSelectionCommand(
this.handleItemClicked.bind(this),
"query",
),
"codeQLQueryHistory.openOnGithub": createSingleSelectionCommand(
this.handleOpenOnGithub.bind(this),
"query",
),
"codeQLQueryHistory.copyRepoList": createSingleSelectionCommand(
this.handleCopyRepoList.bind(this),
"query",
),
"codeQL.exportSelectedVariantAnalysisResults":
this.exportSelectedVariantAnalysisResults.bind(this),
@@ -392,35 +441,26 @@ export class QueryHistoryManager extends DisposableObject {
);
}
async handleOpenQuery(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
): Promise<void> {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
if (singleItem.t === "variant-analysis") {
await this.variantAnalysisManager.openQueryFile(
singleItem.variantAnalysis.id,
);
async handleOpenQuery(item: QueryHistoryInfo): Promise<void> {
if (item.t === "variant-analysis") {
await this.variantAnalysisManager.openQueryFile(item.variantAnalysis.id);
return;
}
let queryPath: string;
switch (singleItem.t) {
switch (item.t) {
case "local":
queryPath = singleItem.initialInfo.queryPath;
queryPath = item.initialInfo.queryPath;
break;
default:
assertNever(singleItem);
assertNever(item);
}
const textDocument = await workspace.openTextDocument(Uri.file(queryPath));
const editor = await window.showTextDocument(textDocument, ViewColumn.One);
if (singleItem.t === "local") {
const queryText = singleItem.initialInfo.queryText;
if (queryText !== undefined && singleItem.initialInfo.isQuickQuery) {
if (item.t === "local") {
const queryText = item.initialInfo.queryText;
if (queryText !== undefined && item.initialInfo.isQuickQuery) {
await editor.edit((edit) =>
edit.replace(
textDocument.validateRange(
@@ -451,13 +491,9 @@ export class QueryHistoryManager extends DisposableObject {
);
}
async handleRemoveHistoryItem(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
multiSelect ||= [singleItem];
async handleRemoveHistoryItem(items: QueryHistoryInfo[]) {
await Promise.all(
multiSelect.map(async (item) => {
items.map(async (item) => {
if (item.t === "local") {
// Removing in progress local queries is not supported. They must be cancelled first.
if (item.status !== QueryStatus.InProgress) {
@@ -547,17 +583,10 @@ export class QueryHistoryManager extends DisposableObject {
}
}
async handleRenameItem(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
): Promise<void> {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
async handleRenameItem(item: QueryHistoryInfo): Promise<void> {
const response = await window.showInputBox({
placeHolder: `(use default: ${this.queryHistoryConfigListener.format})`,
value: singleItem.userSpecifiedLabel ?? "",
value: item.userSpecifiedLabel ?? "",
title: "Set query label",
prompt:
"Set the query history item label. See the description of the codeQL.queryHistory.format setting for more information.",
@@ -565,7 +594,7 @@ export class QueryHistoryManager extends DisposableObject {
// undefined response means the user cancelled the dialog; don't change anything
if (response !== undefined) {
// Interpret empty string response as 'go back to using default'
singleItem.userSpecifiedLabel = response === "" ? undefined : response;
item.userSpecifiedLabel = response === "" ? undefined : response;
await this.refreshTreeView();
}
}
@@ -607,53 +636,39 @@ export class QueryHistoryManager extends DisposableObject {
}
}
async handleItemClicked(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
this.treeDataProvider.setCurrentItem(singleItem);
async handleItemClicked(item: QueryHistoryInfo) {
this.treeDataProvider.setCurrentItem(item);
const now = new Date();
const prevItemClick = this.lastItemClick;
this.lastItemClick = { time: now, item: singleItem };
this.lastItemClick = { time: now, item };
if (
prevItemClick !== undefined &&
now.valueOf() - prevItemClick.time.valueOf() < DOUBLE_CLICK_TIME &&
singleItem === prevItemClick.item
item === prevItemClick.item
) {
// show original query file on double click
await this.handleOpenQuery(singleItem, [singleItem]);
await this.handleOpenQuery(item);
} else if (
singleItem.t === "variant-analysis" ||
singleItem.status === QueryStatus.Completed
item.t === "variant-analysis" ||
item.status === QueryStatus.Completed
) {
// show results on single click (if results view is available)
await this.openQueryResults(singleItem);
await this.openQueryResults(item);
}
}
async handleShowQueryLog(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
async handleShowQueryLog(item: QueryHistoryInfo) {
// Local queries only
if (!this.assertSingleQuery(multiSelect) || singleItem?.t !== "local") {
if (item?.t !== "local" || !item.completedQuery) {
return;
}
if (!singleItem.completedQuery) {
return;
}
if (singleItem.completedQuery.logFileLocation) {
if (item.completedQuery.logFileLocation) {
await tryOpenExternalFile(
this.app.commands,
singleItem.completedQuery.logFileLocation,
item.completedQuery.logFileLocation,
);
} else {
void showAndLogWarningMessage("No log file available");
@@ -678,31 +693,24 @@ export class QueryHistoryManager extends DisposableObject {
throw new Error("Unable to get query directory");
}
async handleOpenQueryDirectory(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
async handleOpenQueryDirectory(item: QueryHistoryInfo) {
let externalFilePath: string | undefined;
if (singleItem.t === "local") {
if (singleItem.completedQuery) {
if (item.t === "local") {
if (item.completedQuery) {
externalFilePath = join(
singleItem.completedQuery.query.querySaveDir,
item.completedQuery.query.querySaveDir,
"timestamp",
);
}
} else if (singleItem.t === "variant-analysis") {
} else if (item.t === "variant-analysis") {
externalFilePath = join(
this.variantAnalysisManager.getVariantAnalysisStorageLocation(
singleItem.variantAnalysis.id,
item.variantAnalysis.id,
),
"timestamp",
);
} else {
assertNever(singleItem);
assertNever(item);
}
if (externalFilePath) {
@@ -747,44 +755,30 @@ export class QueryHistoryManager extends DisposableObject {
);
}
async handleShowEvalLog(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
// Only applicable to an individual local query
if (!this.assertSingleQuery(multiSelect) || singleItem.t !== "local") {
async handleShowEvalLog(item: QueryHistoryInfo) {
if (item.t !== "local") {
return;
}
if (singleItem.evalLogLocation) {
await tryOpenExternalFile(this.app.commands, singleItem.evalLogLocation);
if (item.evalLogLocation) {
await tryOpenExternalFile(this.app.commands, item.evalLogLocation);
} else {
this.warnNoEvalLogs();
}
}
async handleShowEvalLogSummary(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
// Only applicable to an individual local query
if (!this.assertSingleQuery(multiSelect) || singleItem.t !== "local") {
async handleShowEvalLogSummary(item: QueryHistoryInfo) {
if (item.t !== "local") {
return;
}
if (singleItem.evalLogSummaryLocation) {
await tryOpenExternalFile(
this.app.commands,
singleItem.evalLogSummaryLocation,
);
if (item.evalLogSummaryLocation) {
await tryOpenExternalFile(this.app.commands, item.evalLogSummaryLocation);
return;
}
// Summary log file doesn't exist.
if (
singleItem.evalLogLocation &&
(await pathExists(singleItem.evalLogLocation))
) {
if (item.evalLogLocation && (await pathExists(item.evalLogLocation))) {
// If raw log does exist, then the summary log is still being generated.
this.warnInProgressEvalLogSummary();
} else {
@@ -792,17 +786,13 @@ export class QueryHistoryManager extends DisposableObject {
}
}
async handleShowEvalLogViewer(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
// Only applicable to an individual local query
if (!this.assertSingleQuery(multiSelect) || singleItem.t !== "local") {
async handleShowEvalLogViewer(item: QueryHistoryInfo) {
if (item.t !== "local") {
return;
}
// If the JSON summary file location wasn't saved, display error
if (singleItem.jsonEvalLogSummaryLocation === undefined) {
if (item.jsonEvalLogSummaryLocation === undefined) {
this.warnInProgressEvalLogViewer();
return;
}
@@ -810,27 +800,22 @@ export class QueryHistoryManager extends DisposableObject {
// TODO(angelapwen): Stream the file in.
try {
const evalLogData: EvalLogData[] = await parseViewerData(
singleItem.jsonEvalLogSummaryLocation,
item.jsonEvalLogSummaryLocation,
);
const evalLogTreeBuilder = new EvalLogTreeBuilder(
singleItem.getQueryName(),
item.getQueryName(),
evalLogData,
);
this.evalLogViewer.updateRoots(await evalLogTreeBuilder.getRoots());
} catch (e) {
throw new Error(
`Could not read evaluator log summary JSON file to generate viewer data at ${singleItem.jsonEvalLogSummaryLocation}.`,
`Could not read evaluator log summary JSON file to generate viewer data at ${item.jsonEvalLogSummaryLocation}.`,
);
}
}
async handleCancel(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
multiSelect ||= [singleItem];
const results = multiSelect.map(async (item) => {
async handleCancel(items: QueryHistoryInfo[]) {
const results = items.map(async (item) => {
if (item.status === QueryStatus.InProgress) {
if (item.t === "local") {
item.cancel();
@@ -847,51 +832,32 @@ export class QueryHistoryManager extends DisposableObject {
await Promise.all(results);
}
async handleShowQueryText(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] = [],
) {
if (!this.assertSingleQuery(multiSelect)) {
return;
}
if (singleItem.t === "variant-analysis") {
await this.variantAnalysisManager.openQueryText(
singleItem.variantAnalysis.id,
);
async handleShowQueryText(item: QueryHistoryInfo) {
if (item.t === "variant-analysis") {
await this.variantAnalysisManager.openQueryText(item.variantAnalysis.id);
return;
}
const params = new URLSearchParams({
isQuickEval: String(
!!(
singleItem.t === "local" && singleItem.initialInfo.quickEvalPosition
),
!!(item.t === "local" && item.initialInfo.quickEvalPosition),
),
queryText: encodeURIComponent(getQueryText(singleItem)),
queryText: encodeURIComponent(getQueryText(item)),
});
const queryId = getQueryId(singleItem);
const queryId = getQueryId(item);
const uri = Uri.parse(`codeql:${queryId}.ql?${params.toString()}`, true);
const doc = await workspace.openTextDocument(uri);
await window.showTextDocument(doc, { preview: false });
}
async handleViewSarifAlerts(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
// Local queries only
if (
!this.assertSingleQuery(multiSelect) ||
singleItem.t !== "local" ||
!singleItem.completedQuery
) {
async handleViewSarifAlerts(item: QueryHistoryInfo) {
if (item.t !== "local" || !item.completedQuery) {
return;
}
const query = singleItem.completedQuery.query;
const query = item.completedQuery.query;
const hasInterpretedResults = query.canHaveInterpretedResults();
if (hasInterpretedResults) {
await tryOpenExternalFile(
@@ -899,26 +865,18 @@ export class QueryHistoryManager extends DisposableObject {
query.resultsPaths.interpretedResultsPath,
);
} else {
const label = this.labelProvider.getLabel(singleItem);
const label = this.labelProvider.getLabel(item);
void showAndLogInformationMessage(
`Query ${label} has no interpreted results.`,
);
}
}
async handleViewCsvResults(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
// Local queries only
if (
!this.assertSingleQuery(multiSelect) ||
singleItem.t !== "local" ||
!singleItem.completedQuery
) {
async handleViewCsvResults(item: QueryHistoryInfo) {
if (item.t !== "local" || !item.completedQuery) {
return;
}
const query = singleItem.completedQuery.query;
const query = item.completedQuery.query;
if (await query.hasCsv()) {
void tryOpenExternalFile(this.app.commands, query.csvPath);
return;
@@ -928,59 +886,37 @@ export class QueryHistoryManager extends DisposableObject {
}
}
async handleViewCsvAlerts(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
// Local queries only
if (
!this.assertSingleQuery(multiSelect) ||
singleItem.t !== "local" ||
!singleItem.completedQuery
) {
async handleViewCsvAlerts(item: QueryHistoryInfo) {
if (item.t !== "local" || !item.completedQuery) {
return;
}
await tryOpenExternalFile(
this.app.commands,
await singleItem.completedQuery.query.ensureCsvAlerts(
await item.completedQuery.query.ensureCsvAlerts(
this.qs.cliServer,
this.dbm,
),
);
}
async handleViewDil(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
// Local queries only
if (
!this.assertSingleQuery(multiSelect) ||
singleItem.t !== "local" ||
!singleItem.completedQuery
) {
async handleViewDil(item: QueryHistoryInfo) {
if (item.t !== "local" || !item.completedQuery) {
return;
}
await tryOpenExternalFile(
this.app.commands,
await singleItem.completedQuery.query.ensureDilPath(this.qs.cliServer),
await item.completedQuery.query.ensureDilPath(this.qs.cliServer),
);
}
async handleOpenOnGithub(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
if (
!this.assertSingleQuery(multiSelect) ||
singleItem.t !== "variant-analysis"
) {
async handleOpenOnGithub(item: QueryHistoryInfo) {
if (item.t !== "variant-analysis") {
return;
}
const actionsWorkflowRunUrl = getActionsWorkflowRunUrl(singleItem);
const actionsWorkflowRunUrl = getActionsWorkflowRunUrl(item);
await this.app.commands.execute(
"vscode.open",
@@ -988,39 +924,23 @@ export class QueryHistoryManager extends DisposableObject {
);
}
async handleCopyRepoList(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
) {
// Variant analyses only
if (
!this.assertSingleQuery(multiSelect) ||
singleItem.t !== "variant-analysis"
) {
async handleCopyRepoList(item: QueryHistoryInfo) {
if (item.t !== "variant-analysis") {
return;
}
await this.app.commands.execute(
"codeQL.copyVariantAnalysisRepoList",
singleItem.variantAnalysis.id,
item.variantAnalysis.id,
);
}
async handleExportResults(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[] | undefined,
): Promise<void> {
// Variant analysis only
if (
!this.assertSingleQuery(multiSelect) ||
singleItem.t !== "variant-analysis"
) {
async handleExportResults(item: QueryHistoryInfo): Promise<void> {
if (item.t !== "variant-analysis") {
return;
}
await this.variantAnalysisManager.exportResults(
singleItem.variantAnalysis.id,
);
await this.variantAnalysisManager.exportResults(item.variantAnalysis.id);
}
/**
@@ -1123,17 +1043,6 @@ export class QueryHistoryManager extends DisposableObject {
return choice?.query;
}
private assertSingleQuery(
multiSelect: QueryHistoryInfo[] = [],
message = "Please select a single query.",
) {
if (multiSelect.length > 1) {
void showAndLogErrorMessage(message);
return false;
}
return true;
}
/**
* Updates the compare with source query. This ensures that all compare command invocations
* when exactly 2 queries are selected always have the proper _from_ query. Always use

View File

@@ -155,9 +155,7 @@ describe("QueryHistoryManager", () => {
it("should show results", async () => {
queryHistoryManager = await createMockQueryHistory(allHistory);
const itemClicked = localQueryHistory[0];
await queryHistoryManager.handleItemClicked(itemClicked, [
itemClicked,
]);
await queryHistoryManager.handleItemClicked(itemClicked);
expect(
localQueriesResultsViewStub.showResults,
@@ -175,9 +173,7 @@ describe("QueryHistoryManager", () => {
it("should do nothing", async () => {
queryHistoryManager = await createMockQueryHistory(allHistory);
const itemClicked = localQueryHistory[2];
await queryHistoryManager.handleItemClicked(itemClicked, [
itemClicked,
]);
await queryHistoryManager.handleItemClicked(itemClicked);
expect(
localQueriesResultsViewStub.showResults,
@@ -191,9 +187,7 @@ describe("QueryHistoryManager", () => {
it("should show results", async () => {
queryHistoryManager = await createMockQueryHistory(allHistory);
const itemClicked = variantAnalysisHistory[0];
await queryHistoryManager.handleItemClicked(itemClicked, [
itemClicked,
]);
await queryHistoryManager.handleItemClicked(itemClicked);
expect(variantAnalysisManagerStub.showView).toHaveBeenCalledTimes(
1,
@@ -211,9 +205,7 @@ describe("QueryHistoryManager", () => {
it("should show results", async () => {
queryHistoryManager = await createMockQueryHistory(allHistory);
const itemClicked = variantAnalysisHistory[1];
await queryHistoryManager.handleItemClicked(itemClicked, [
itemClicked,
]);
await queryHistoryManager.handleItemClicked(itemClicked);
expect(variantAnalysisManagerStub.showView).toHaveBeenCalledTimes(
1,
@@ -228,25 +220,6 @@ describe("QueryHistoryManager", () => {
});
});
});
describe("double click", () => {
it("should do nothing", async () => {
queryHistoryManager = await createMockQueryHistory(allHistory);
const itemClicked = allHistory[0];
const secondItemClicked = allHistory[1];
await queryHistoryManager.handleItemClicked(itemClicked, [
itemClicked,
secondItemClicked,
]);
expect(localQueriesResultsViewStub.showResults).not.toHaveBeenCalled();
expect(variantAnalysisManagerStub.showView).not.toBeCalled();
expect(
queryHistoryManager.treeDataProvider.getCurrent(),
).toBeUndefined();
});
});
});
describe("handleRemoveHistoryItem", () => {
@@ -278,9 +251,7 @@ describe("QueryHistoryManager", () => {
);
// remove an item
await queryHistoryManager.handleRemoveHistoryItem(toDelete, [
toDelete,
]);
await queryHistoryManager.handleRemoveHistoryItem([toDelete]);
});
it("should remove the item", () => {
@@ -320,9 +291,7 @@ describe("QueryHistoryManager", () => {
await queryHistoryManager.treeView.reveal(toDelete, {
select: true,
});
await queryHistoryManager.handleRemoveHistoryItem(toDelete, [
toDelete,
]);
await queryHistoryManager.handleRemoveHistoryItem([toDelete]);
});
it("should remove the item", () => {
@@ -401,9 +370,7 @@ describe("QueryHistoryManager", () => {
it("should remove the item", async () => {
// remove an item
await queryHistoryManager.handleRemoveHistoryItem(toDelete, [
toDelete,
]);
await queryHistoryManager.handleRemoveHistoryItem([toDelete]);
expect(
variantAnalysisManagerStub.removeVariantAnalysis,
@@ -415,9 +382,7 @@ describe("QueryHistoryManager", () => {
it("should not change the selection", async () => {
// remove an item
await queryHistoryManager.handleRemoveHistoryItem(toDelete, [
toDelete,
]);
await queryHistoryManager.handleRemoveHistoryItem([toDelete]);
expect(queryHistoryManager.treeDataProvider.getCurrent()).toEqual(
selected,
@@ -429,9 +394,7 @@ describe("QueryHistoryManager", () => {
it("should show a modal asking 'Are you sure?'", async () => {
// remove an item
await queryHistoryManager.handleRemoveHistoryItem(toDelete, [
toDelete,
]);
await queryHistoryManager.handleRemoveHistoryItem([toDelete]);
expect(showBinaryChoiceDialogSpy).toHaveBeenCalledWith(
"You are about to delete this query: a-query-name (javascript). Are you sure?",
@@ -440,9 +403,7 @@ describe("QueryHistoryManager", () => {
it("should show a toast notification with a link to GitHub Actions", async () => {
// remove an item
await queryHistoryManager.handleRemoveHistoryItem(toDelete, [
toDelete,
]);
await queryHistoryManager.handleRemoveHistoryItem([toDelete]);
expect(showInformationMessageWithActionSpy).toHaveBeenCalled();
});
@@ -454,9 +415,7 @@ describe("QueryHistoryManager", () => {
it("should not delete the item", async () => {
// remove an item
await queryHistoryManager.handleRemoveHistoryItem(toDelete, [
toDelete,
]);
await queryHistoryManager.handleRemoveHistoryItem([toDelete]);
expect(queryHistoryManager.treeDataProvider.allHistory).toContain(
toDelete,
@@ -465,9 +424,7 @@ describe("QueryHistoryManager", () => {
it("should not show a toast notification", async () => {
// remove an item
await queryHistoryManager.handleRemoveHistoryItem(toDelete, [
toDelete,
]);
await queryHistoryManager.handleRemoveHistoryItem([toDelete]);
expect(
showInformationMessageWithActionSpy,
@@ -493,9 +450,7 @@ describe("QueryHistoryManager", () => {
await queryHistoryManager.treeView.reveal(toDelete, {
select: true,
});
await queryHistoryManager.handleRemoveHistoryItem(toDelete, [
toDelete,
]);
await queryHistoryManager.handleRemoveHistoryItem([toDelete]);
});
it("should remove the item", () => {
@@ -555,9 +510,7 @@ describe("QueryHistoryManager", () => {
);
// remove an item
await queryHistoryManager.handleRemoveHistoryItem(toDelete, [
toDelete,
]);
await queryHistoryManager.handleRemoveHistoryItem([toDelete]);
});
it("should remove the item", () => {
@@ -600,9 +553,7 @@ describe("QueryHistoryManager", () => {
await queryHistoryManager.treeView.reveal(toDelete, {
select: true,
});
await queryHistoryManager.handleRemoveHistoryItem(toDelete, [
toDelete,
]);
await queryHistoryManager.handleRemoveHistoryItem([toDelete]);
});
it("should remove the item", () => {
@@ -640,7 +591,7 @@ describe("QueryHistoryManager", () => {
const inProgress1 = localQueryHistory[4];
const cancelSpy = jest.spyOn(inProgress1, "cancel");
await queryHistoryManager.handleCancel(inProgress1, [inProgress1]);
await queryHistoryManager.handleCancel([inProgress1]);
expect(cancelSpy).toBeCalledTimes(1);
});
@@ -654,10 +605,7 @@ describe("QueryHistoryManager", () => {
const cancelSpy1 = jest.spyOn(inProgress1, "cancel");
const cancelSpy2 = jest.spyOn(inProgress2, "cancel");
await queryHistoryManager.handleCancel(inProgress1, [
inProgress1,
inProgress2,
]);
await queryHistoryManager.handleCancel([inProgress1, inProgress2]);
expect(cancelSpy1).toBeCalled();
expect(cancelSpy2).toBeCalled();
});
@@ -668,7 +616,7 @@ describe("QueryHistoryManager", () => {
// cancelling the selected item
const inProgress1 = variantAnalysisHistory[1];
await queryHistoryManager.handleCancel(inProgress1, [inProgress1]);
await queryHistoryManager.handleCancel([inProgress1]);
expect(cancelVariantAnalysisSpy).toBeCalledWith(
inProgress1.variantAnalysis.id,
);
@@ -681,10 +629,7 @@ describe("QueryHistoryManager", () => {
const inProgress1 = variantAnalysisHistory[1];
const inProgress2 = variantAnalysisHistory[3];
await queryHistoryManager.handleCancel(inProgress1, [
inProgress1,
inProgress2,
]);
await queryHistoryManager.handleCancel([inProgress1, inProgress2]);
expect(cancelVariantAnalysisSpy).toBeCalledWith(
inProgress1.variantAnalysis.id,
);
@@ -702,7 +647,7 @@ describe("QueryHistoryManager", () => {
const completed = localQueryHistory[0];
const cancelSpy = jest.spyOn(completed, "cancel");
await queryHistoryManager.handleCancel(completed, [completed]);
await queryHistoryManager.handleCancel([completed]);
expect(cancelSpy).not.toBeCalledTimes(1);
});
@@ -716,7 +661,7 @@ describe("QueryHistoryManager", () => {
const cancelSpy = jest.spyOn(completed, "cancel");
const cancelSpy2 = jest.spyOn(failed, "cancel");
await queryHistoryManager.handleCancel(completed, [completed, failed]);
await queryHistoryManager.handleCancel([completed, failed]);
expect(cancelSpy).not.toBeCalledTimes(1);
expect(cancelSpy2).not.toBeCalledTimes(1);
});
@@ -727,9 +672,7 @@ describe("QueryHistoryManager", () => {
// cancelling the selected item
const completedVariantAnalysis = variantAnalysisHistory[0];
await queryHistoryManager.handleCancel(completedVariantAnalysis, [
completedVariantAnalysis,
]);
await queryHistoryManager.handleCancel([completedVariantAnalysis]);
expect(cancelVariantAnalysisSpy).not.toBeCalledWith(
completedVariantAnalysis.variantAnalysis,
);
@@ -742,7 +685,7 @@ describe("QueryHistoryManager", () => {
const completedVariantAnalysis = variantAnalysisHistory[0];
const failedVariantAnalysis = variantAnalysisHistory[2];
await queryHistoryManager.handleCancel(completedVariantAnalysis, [
await queryHistoryManager.handleCancel([
completedVariantAnalysis,
failedVariantAnalysis,
]);
@@ -761,7 +704,7 @@ describe("QueryHistoryManager", () => {
queryHistoryManager = await createMockQueryHistory(localQueryHistory);
const item = localQueryHistory[4];
await queryHistoryManager.handleCopyRepoList(item, [item]);
await queryHistoryManager.handleCopyRepoList(item);
expect(executeCommand).not.toBeCalled();
});
@@ -770,21 +713,12 @@ describe("QueryHistoryManager", () => {
queryHistoryManager = await createMockQueryHistory(allHistory);
const item = variantAnalysisHistory[1];
await queryHistoryManager.handleCopyRepoList(item, [item]);
await queryHistoryManager.handleCopyRepoList(item);
expect(executeCommand).toBeCalledWith(
"codeQL.copyVariantAnalysisRepoList",
item.variantAnalysis.id,
);
});
it("should not copy repo list for multiple variant analyses", async () => {
queryHistoryManager = await createMockQueryHistory(allHistory);
const item1 = variantAnalysisHistory[1];
const item2 = variantAnalysisHistory[3];
await queryHistoryManager.handleCopyRepoList(item1, [item1, item2]);
expect(executeCommand).not.toBeCalled();
});
});
describe("handleExportResults", () => {
@@ -792,7 +726,7 @@ describe("QueryHistoryManager", () => {
queryHistoryManager = await createMockQueryHistory(localQueryHistory);
const item = localQueryHistory[4];
await queryHistoryManager.handleExportResults(item, [item]);
await queryHistoryManager.handleExportResults(item);
expect(variantAnalysisManagerStub.exportResults).not.toBeCalled();
});
@@ -801,20 +735,11 @@ describe("QueryHistoryManager", () => {
queryHistoryManager = await createMockQueryHistory(allHistory);
const item = variantAnalysisHistory[1];
await queryHistoryManager.handleExportResults(item, [item]);
await queryHistoryManager.handleExportResults(item);
expect(variantAnalysisManagerStub.exportResults).toBeCalledWith(
item.variantAnalysis.id,
);
});
it("should not export results for multiple variant analyses", async () => {
queryHistoryManager = await createMockQueryHistory(allHistory);
const item1 = variantAnalysisHistory[1];
const item2 = variantAnalysisHistory[3];
await queryHistoryManager.handleExportResults(item1, [item1, item2]);
expect(variantAnalysisManagerStub.exportResults).not.toBeCalled();
});
});
describe("Local Queries", () => {

View File

@@ -132,10 +132,7 @@ describe("Variant Analyses and QueryHistoryManager", () => {
await qhm.readQueryHistory();
// Remove the first variant analysis
await qhm.handleRemoveHistoryItem(
qhm.treeDataProvider.allHistory[0],
undefined,
);
await qhm.handleRemoveHistoryItem([qhm.treeDataProvider.allHistory[0]]);
// Add it back to the history
qhm.addQuery(rawQueryHistory[0]);
@@ -152,7 +149,7 @@ describe("Variant Analyses and QueryHistoryManager", () => {
// Remove both queries
// Just for fun, let's do it in reverse order
await qhm.handleRemoveHistoryItem(undefined!, [
await qhm.handleRemoveHistoryItem([
qhm.treeDataProvider.allHistory[1],
qhm.treeDataProvider.allHistory[0],
]);
@@ -180,13 +177,13 @@ describe("Variant Analyses and QueryHistoryManager", () => {
it("should handle a click", async () => {
await qhm.readQueryHistory();
await qhm.handleItemClicked(qhm.treeDataProvider.allHistory[0], []);
await qhm.handleItemClicked(qhm.treeDataProvider.allHistory[0]);
expect(showViewStub).toBeCalledWith(rawQueryHistory[0].variantAnalysis.id);
});
it("should get the query text", async () => {
await qhm.readQueryHistory();
await qhm.handleShowQueryText(qhm.treeDataProvider.allHistory[0], []);
await qhm.handleShowQueryText(qhm.treeDataProvider.allHistory[0]);
expect(openQueryTextSpy).toHaveBeenCalledWith(
rawQueryHistory[0].variantAnalysis.id,