Skip to content

Commit 29d0483

Browse files
Merge pull request #2287 from github/robertbrignull/selection
Introduce helpers for commands that operate on selections
2 parents 988df04 + 392c59e commit 29d0483

File tree

6 files changed

+301
-448
lines changed

6 files changed

+301
-448
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { showAndLogErrorMessage } from "../helpers";
2+
import {
3+
ExplorerSelectionCommandFunction,
4+
TreeViewContextMultiSelectionCommandFunction,
5+
TreeViewContextSingleSelectionCommandFunction,
6+
} from "./commands";
7+
8+
// A hack to match types that are not an array, which is useful to help avoid
9+
// misusing createSingleSelectionCommand, e.g. where T accidentally gets instantiated
10+
// as DatabaseItem[] instead of DatabaseItem.
11+
type NotArray = object & { length?: never };
12+
13+
// A way to get the type system to help assert that one type is a supertype of another.
14+
type CreateSupertypeOf<Super, Sub extends Super> = Sub;
15+
16+
// This asserts that SelectionCommand is assignable to all of the different types of
17+
// SelectionCommand defined in commands.ts. The intention is the output from the helpers
18+
// in this file can be used with any of the select command types and can handle any of
19+
// the inputs.
20+
type SelectionCommand<T extends NotArray> = CreateSupertypeOf<
21+
TreeViewContextMultiSelectionCommandFunction<T> &
22+
TreeViewContextSingleSelectionCommandFunction<T> &
23+
ExplorerSelectionCommandFunction<T>,
24+
(singleItem: T, multiSelect?: T[] | undefined) => Promise<void>
25+
>;
26+
27+
export function createSingleSelectionCommand<T extends NotArray>(
28+
f: (argument: T) => Promise<void>,
29+
itemName: string,
30+
): SelectionCommand<T> {
31+
return async (singleItem, multiSelect) => {
32+
if (multiSelect === undefined || multiSelect.length === 1) {
33+
return f(singleItem);
34+
} else {
35+
void showAndLogErrorMessage(`Please select a single ${itemName}.`);
36+
return;
37+
}
38+
};
39+
}
40+
41+
export function createMultiSelectionCommand<T extends NotArray>(
42+
f: (argument: T[]) => Promise<void>,
43+
): SelectionCommand<T> {
44+
return async (singleItem, multiSelect) => {
45+
if (multiSelect !== undefined && multiSelect.length > 0) {
46+
return f(multiSelect);
47+
} else {
48+
return f([singleItem]);
49+
}
50+
};
51+
}

extensions/ql-vscode/src/databases/local-databases-ui.ts

Lines changed: 67 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ import { isCanary } from "../config";
4646
import { App } from "../common/app";
4747
import { redactableError } from "../pure/errors";
4848
import { LocalDatabasesCommands } from "../common/commands";
49+
import {
50+
createMultiSelectionCommand,
51+
createSingleSelectionCommand,
52+
} from "../common/selection-commands";
4953

5054
enum SortOrder {
5155
NameAsc = "NameAsc",
@@ -240,11 +244,22 @@ export class DatabaseUI extends DisposableObject {
240244
this.handleMakeCurrentDatabase.bind(this),
241245
"codeQLDatabases.sortByName": this.handleSortByName.bind(this),
242246
"codeQLDatabases.sortByDateAdded": this.handleSortByDateAdded.bind(this),
243-
"codeQLDatabases.removeDatabase": this.handleRemoveDatabase.bind(this),
244-
"codeQLDatabases.upgradeDatabase": this.handleUpgradeDatabase.bind(this),
245-
"codeQLDatabases.renameDatabase": this.handleRenameDatabase.bind(this),
246-
"codeQLDatabases.openDatabaseFolder": this.handleOpenFolder.bind(this),
247-
"codeQLDatabases.addDatabaseSource": this.handleAddSource.bind(this),
247+
"codeQLDatabases.removeDatabase": createMultiSelectionCommand(
248+
this.handleRemoveDatabase.bind(this),
249+
),
250+
"codeQLDatabases.upgradeDatabase": createMultiSelectionCommand(
251+
this.handleUpgradeDatabase.bind(this),
252+
),
253+
"codeQLDatabases.renameDatabase": createSingleSelectionCommand(
254+
this.handleRenameDatabase.bind(this),
255+
"database",
256+
),
257+
"codeQLDatabases.openDatabaseFolder": createMultiSelectionCommand(
258+
this.handleOpenFolder.bind(this),
259+
),
260+
"codeQLDatabases.addDatabaseSource": createMultiSelectionCommand(
261+
this.handleAddSource.bind(this),
262+
),
248263
"codeQLDatabases.removeOrphanedDatabases":
249264
this.handleRemoveOrphanedDatabases.bind(this),
250265
};
@@ -515,12 +530,11 @@ export class DatabaseUI extends DisposableObject {
515530
private async handleUpgradeCurrentDatabase(): Promise<void> {
516531
return withProgress(
517532
async (progress, token) => {
518-
await this.handleUpgradeDatabaseInternal(
519-
progress,
520-
token,
521-
this.databaseManager.currentDatabaseItem,
522-
[],
523-
);
533+
if (this.databaseManager.currentDatabaseItem !== undefined) {
534+
await this.handleUpgradeDatabasesInternal(progress, token, [
535+
this.databaseManager.currentDatabaseItem,
536+
]);
537+
}
524538
},
525539
{
526540
title: "Upgrading current database",
@@ -530,16 +544,14 @@ export class DatabaseUI extends DisposableObject {
530544
}
531545

532546
private async handleUpgradeDatabase(
533-
databaseItem: DatabaseItem | undefined,
534-
multiSelect: DatabaseItem[] | undefined,
547+
databaseItems: DatabaseItem[],
535548
): Promise<void> {
536549
return withProgress(
537550
async (progress, token) => {
538-
return await this.handleUpgradeDatabaseInternal(
551+
return await this.handleUpgradeDatabasesInternal(
539552
progress,
540553
token,
541-
databaseItem,
542-
multiSelect,
554+
databaseItems,
543555
);
544556
},
545557
{
@@ -549,46 +561,37 @@ export class DatabaseUI extends DisposableObject {
549561
);
550562
}
551563

552-
private async handleUpgradeDatabaseInternal(
564+
private async handleUpgradeDatabasesInternal(
553565
progress: ProgressCallback,
554566
token: CancellationToken,
555-
databaseItem: DatabaseItem | undefined,
556-
multiSelect: DatabaseItem[] | undefined,
567+
databaseItems: DatabaseItem[],
557568
): Promise<void> {
558-
if (multiSelect?.length) {
559-
await Promise.all(
560-
multiSelect.map((dbItem) =>
561-
this.handleUpgradeDatabaseInternal(progress, token, dbItem, []),
562-
),
563-
);
564-
}
565-
if (this.queryServer === undefined) {
566-
throw new Error(
567-
"Received request to upgrade database, but there is no running query server.",
568-
);
569-
}
570-
if (databaseItem === undefined) {
571-
throw new Error(
572-
"Received request to upgrade database, but no database was provided.",
573-
);
574-
}
575-
if (databaseItem.contents === undefined) {
576-
throw new Error(
577-
"Received request to upgrade database, but database contents could not be found.",
578-
);
579-
}
580-
if (databaseItem.contents.dbSchemeUri === undefined) {
581-
throw new Error(
582-
"Received request to upgrade database, but database has no schema.",
583-
);
584-
}
569+
await Promise.all(
570+
databaseItems.map(async (databaseItem) => {
571+
if (this.queryServer === undefined) {
572+
throw new Error(
573+
"Received request to upgrade database, but there is no running query server.",
574+
);
575+
}
576+
if (databaseItem.contents === undefined) {
577+
throw new Error(
578+
"Received request to upgrade database, but database contents could not be found.",
579+
);
580+
}
581+
if (databaseItem.contents.dbSchemeUri === undefined) {
582+
throw new Error(
583+
"Received request to upgrade database, but database has no schema.",
584+
);
585+
}
585586

586-
// Search for upgrade scripts in any workspace folders available
587+
// Search for upgrade scripts in any workspace folders available
587588

588-
await this.queryServer.upgradeDatabaseExplicit(
589-
databaseItem,
590-
progress,
591-
token,
589+
await this.queryServer.upgradeDatabaseExplicit(
590+
databaseItem,
591+
progress,
592+
token,
593+
);
594+
}),
592595
);
593596
}
594597

@@ -651,24 +654,15 @@ export class DatabaseUI extends DisposableObject {
651654
}
652655

653656
private async handleRemoveDatabase(
654-
databaseItem: DatabaseItem,
655-
multiSelect: DatabaseItem[] | undefined,
657+
databaseItems: DatabaseItem[],
656658
): Promise<void> {
657659
return withProgress(
658660
async (progress, token) => {
659-
if (multiSelect?.length) {
660-
await Promise.all(
661-
multiSelect.map((dbItem) =>
662-
this.databaseManager.removeDatabaseItem(progress, token, dbItem),
663-
),
664-
);
665-
} else {
666-
await this.databaseManager.removeDatabaseItem(
667-
progress,
668-
token,
669-
databaseItem,
670-
);
671-
}
661+
await Promise.all(
662+
databaseItems.map((dbItem) =>
663+
this.databaseManager.removeDatabaseItem(progress, token, dbItem),
664+
),
665+
);
672666
},
673667
{
674668
title: "Removing database",
@@ -679,10 +673,7 @@ export class DatabaseUI extends DisposableObject {
679673

680674
private async handleRenameDatabase(
681675
databaseItem: DatabaseItem,
682-
multiSelect: DatabaseItem[] | undefined,
683676
): Promise<void> {
684-
this.assertSingleDatabase(multiSelect);
685-
686677
const newName = await window.showInputBox({
687678
prompt: "Choose new database name",
688679
value: databaseItem.name,
@@ -693,34 +684,20 @@ export class DatabaseUI extends DisposableObject {
693684
}
694685
}
695686

696-
private async handleOpenFolder(
697-
databaseItem: DatabaseItem,
698-
multiSelect: DatabaseItem[] | undefined,
699-
): Promise<void> {
700-
if (multiSelect?.length) {
701-
await Promise.all(
702-
multiSelect.map((dbItem) => env.openExternal(dbItem.databaseUri)),
703-
);
704-
} else {
705-
await env.openExternal(databaseItem.databaseUri);
706-
}
687+
private async handleOpenFolder(databaseItems: DatabaseItem[]): Promise<void> {
688+
await Promise.all(
689+
databaseItems.map((dbItem) => env.openExternal(dbItem.databaseUri)),
690+
);
707691
}
708692

709693
/**
710694
* Adds the source folder of a CodeQL database to the workspace.
711695
* When a database is first added in the "Databases" view, its source folder is added to the workspace.
712696
* If the source folder is removed from the workspace for some reason, we want to be able to re-add it if need be.
713697
*/
714-
private async handleAddSource(
715-
databaseItem: DatabaseItem,
716-
multiSelect: DatabaseItem[] | undefined,
717-
): Promise<void> {
718-
if (multiSelect?.length) {
719-
for (const dbItem of multiSelect) {
720-
await this.databaseManager.addDatabaseSourceArchiveFolder(dbItem);
721-
}
722-
} else {
723-
await this.databaseManager.addDatabaseSourceArchiveFolder(databaseItem);
698+
private async handleAddSource(databaseItems: DatabaseItem[]): Promise<void> {
699+
for (const dbItem of databaseItems) {
700+
await this.databaseManager.addDatabaseSourceArchiveFolder(dbItem);
724701
}
725702
}
726703

@@ -823,13 +800,4 @@ export class DatabaseUI extends DisposableObject {
823800
}
824801
return Uri.file(dbPath);
825802
}
826-
827-
private assertSingleDatabase(
828-
multiSelect: DatabaseItem[] = [],
829-
message = "Please select a single database.",
830-
) {
831-
if (multiSelect.length > 1) {
832-
throw new Error(message);
833-
}
834-
}
835803
}

extensions/ql-vscode/src/local-queries/local-queries.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { App } from "../common/app";
4343
import { DisposableObject } from "../pure/disposable-object";
4444
import { SkeletonQueryWizard } from "../skeleton-query-wizard";
4545
import { LocalQueryRun } from "./local-query-run";
46+
import { createMultiSelectionCommand } from "../common/selection-commands";
4647

4748
interface DatabaseQuickPickItem extends QuickPickItem {
4849
databaseItem: DatabaseItem;
@@ -89,7 +90,9 @@ export class LocalQueries extends DisposableObject {
8990
this.runQueryOnMultipleDatabases.bind(this),
9091
"codeQL.runQueryOnMultipleDatabasesContextEditor":
9192
this.runQueryOnMultipleDatabases.bind(this),
92-
"codeQL.runQueries": this.runQueries.bind(this),
93+
"codeQL.runQueries": createMultiSelectionCommand(
94+
this.runQueries.bind(this),
95+
),
9396
"codeQL.quickEval": this.quickEval.bind(this),
9497
"codeQL.quickEvalContextEditor": this.quickEval.bind(this),
9598
"codeQL.codeLensQuickEval": this.codeLensQuickEval.bind(this),
@@ -130,12 +133,12 @@ export class LocalQueries extends DisposableObject {
130133
);
131134
}
132135

133-
private async runQueries(_: unknown, multi: Uri[]): Promise<void> {
136+
private async runQueries(fileURIs: Uri[]): Promise<void> {
134137
await withProgress(
135138
async (progress, token) => {
136139
const maxQueryCount = MAX_QUERIES.getValue() as number;
137140
const [files, dirFound] = await gatherQlFiles(
138-
multi.map((uri) => uri.fsPath),
141+
fileURIs.map((uri) => uri.fsPath),
139142
);
140143
if (files.length > maxQueryCount) {
141144
throw new Error(

0 commit comments

Comments
 (0)