Skip to content

Commit fd570ab

Browse files
Merge branch 'main' into robertbrignull/no-nested-functions
2 parents e81d585 + dc55ef9 commit fd570ab

23 files changed

Lines changed: 544 additions & 100 deletions

File tree

extensions/ql-vscode/package-lock.json

Lines changed: 19 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/ql-vscode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1535,7 +1535,7 @@
15351535
"ts-node": "^10.7.0",
15361536
"ts-protoc-gen": "^0.9.0",
15371537
"typescript": "^4.5.5",
1538-
"webpack": "^5.62.2",
1538+
"webpack": "^5.76.0",
15391539
"webpack-cli": "^4.6.0"
15401540
},
15411541
"lint-staged": {

extensions/ql-vscode/src/cli.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1284,11 +1284,25 @@ export class CodeQLCliServer implements Disposable {
12841284
);
12851285
}
12861286

1287-
async packInstall(dir: string, forceUpdate = false) {
1287+
async packInstall(
1288+
dir: string,
1289+
{ forceUpdate = false, workspaceFolders = [] as string[] } = {},
1290+
) {
12881291
const args = [dir];
12891292
if (forceUpdate) {
12901293
args.push("--mode", "update");
12911294
}
1295+
if (workspaceFolders?.length > 0) {
1296+
if (await this.cliConstraints.supportsAdditionalPacksInstall()) {
1297+
args.push(
1298+
// Allow prerelease packs from the ql submodule.
1299+
"--allow-prerelease",
1300+
// Allow the use of --additional-packs argument without issueing a warning
1301+
"--no-strict-mode",
1302+
...this.getAdditionalPacksArg(workspaceFolders),
1303+
);
1304+
}
1305+
}
12921306
return this.runJsonCodeQlCliCommandWithAuthentication(
12931307
["pack", "install"],
12941308
args,
@@ -1692,6 +1706,13 @@ export class CliVersionConstraint {
16921706
*/
16931707
public static CLI_VERSION_WITH_QLPACKS_KIND = new SemVer("2.12.3");
16941708

1709+
/**
1710+
* CLI version that supports the `--additional-packs` option for the `pack install` command.
1711+
*/
1712+
public static CLI_VERSION_WITH_ADDITIONAL_PACKS_INSTALL = new SemVer(
1713+
"2.12.4",
1714+
);
1715+
16951716
constructor(private readonly cli: CodeQLCliServer) {
16961717
/**/
16971718
}
@@ -1755,4 +1776,10 @@ export class CliVersionConstraint {
17551776
CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND,
17561777
);
17571778
}
1779+
1780+
async supportsAdditionalPacksInstall() {
1781+
return this.isVersionAtLeast(
1782+
CliVersionConstraint.CLI_VERSION_WITH_ADDITIONAL_PACKS_INSTALL,
1783+
);
1784+
}
17581785
}

extensions/ql-vscode/src/common/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Disposable } from "../pure/disposable-object";
33
import { AppEventEmitter } from "./events";
44
import { Logger } from "./logging";
55
import { Memento } from "./memento";
6+
import { AppCommandManager } from "./commands";
67

78
export interface App {
89
createEventEmitter<T>(): AppEventEmitter<T>;
@@ -15,6 +16,7 @@ export interface App {
1516
readonly workspaceStoragePath?: string;
1617
readonly workspaceState: Memento;
1718
readonly credentials: Credentials;
19+
readonly commands: AppCommandManager;
1820
}
1921

2022
export enum AppMode {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { CommandManager } from "../packages/commands";
2+
3+
/**
4+
* Contains type definitions for all commands used by the extension.
5+
*
6+
* To add a new command first define its type here, then provide
7+
* the implementation in the corresponding `getCommands` function.
8+
*/
9+
10+
// Base commands not tied directly to a module like e.g. variant analysis.
11+
export type BaseCommands = {
12+
"codeQL.openDocumentation": () => Promise<void>;
13+
};
14+
15+
// Commands tied to variant analysis
16+
export type VariantAnalysisCommands = {
17+
"codeQL.openVariantAnalysisLogs": (
18+
variantAnalysisId: number,
19+
) => Promise<void>;
20+
};
21+
22+
export type AllCommands = BaseCommands & VariantAnalysisCommands;
23+
24+
export type AppCommandManager = CommandManager<AllCommands>;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { commands } from "vscode";
2+
import { commandRunner } from "../../commandRunner";
3+
import { CommandFunction, CommandManager } from "../../packages/commands";
4+
5+
/**
6+
* Create a command manager for VSCode, wrapping the commandRunner
7+
* and vscode.executeCommand.
8+
*/
9+
export function createVSCodeCommandManager<
10+
Commands extends Record<string, CommandFunction>,
11+
>(): CommandManager<Commands> {
12+
return new CommandManager(commandRunner, wrapExecuteCommand);
13+
}
14+
15+
/**
16+
* wrapExecuteCommand wraps commands.executeCommand to satisfy that the
17+
* type is a Promise. Type script does not seem to be smart enough
18+
* to figure out that `ReturnType<Commands[CommandName]>` is actually
19+
* a Promise, so we need to add a second layer of wrapping and unwrapping
20+
* (The `Promise<Awaited<` part) to get the right types.
21+
*/
22+
async function wrapExecuteCommand<
23+
Commands extends Record<string, CommandFunction>,
24+
CommandName extends keyof Commands & string = keyof Commands & string,
25+
>(
26+
commandName: CommandName,
27+
...args: Parameters<Commands[CommandName]>
28+
): Promise<Awaited<ReturnType<Commands[CommandName]>>> {
29+
return await commands.executeCommand<
30+
Awaited<ReturnType<Commands[CommandName]>>
31+
>(commandName, ...args);
32+
}

extensions/ql-vscode/src/common/vscode/vscode-app.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@ import { AppEventEmitter } from "../events";
66
import { extLogger, Logger } from "../logging";
77
import { Memento } from "../memento";
88
import { VSCodeAppEventEmitter } from "./events";
9+
import { AppCommandManager } from "../commands";
10+
import { createVSCodeCommandManager } from "./commands";
911

1012
export class ExtensionApp implements App {
1113
public readonly credentials: VSCodeCredentials;
14+
public readonly commands: AppCommandManager;
1215

1316
public constructor(
1417
public readonly extensionContext: vscode.ExtensionContext,
1518
) {
1619
this.credentials = new VSCodeCredentials();
20+
this.commands = createVSCodeCommandManager();
21+
extensionContext.subscriptions.push(this.commands);
1722
}
1823

1924
public get extensionPath(): string {

extensions/ql-vscode/src/extension.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ import { DbModule } from "./databases/db-module";
137137
import { redactableError } from "./pure/errors";
138138
import { QueryHistoryDirs } from "./query-history/query-history-dirs";
139139
import { DirResult } from "tmp";
140+
import { AllCommands, BaseCommands } from "./common/commands";
140141

141142
/**
142143
* extension.ts
@@ -168,6 +169,17 @@ let isInstallingOrUpdatingDistribution = false;
168169
const extensionId = "GitHub.vscode-codeql";
169170
const extension = extensions.getExtension(extensionId);
170171

172+
/**
173+
* Return all commands that are not tied to the more specific managers.
174+
*/
175+
function getCommands(): BaseCommands {
176+
return {
177+
"codeQL.openDocumentation": async () => {
178+
await env.openExternal(Uri.parse("https://codeql.github.com/docs/"));
179+
},
180+
};
181+
}
182+
171183
/**
172184
* If the user tries to execute vscode commands after extension activation is failed, give
173185
* a sensible error message.
@@ -1113,14 +1125,14 @@ async function activateWithInstalledDistribution(
11131125
),
11141126
);
11151127

1116-
ctx.subscriptions.push(
1117-
commandRunner(
1118-
"codeQL.openVariantAnalysisLogs",
1119-
async (variantAnalysisId: number) => {
1120-
await variantAnalysisManager.openVariantAnalysisLogs(variantAnalysisId);
1121-
},
1122-
),
1123-
);
1128+
const allCommands: AllCommands = {
1129+
...getCommands(),
1130+
...variantAnalysisManager.getCommands(),
1131+
};
1132+
1133+
for (const [commandName, command] of Object.entries(allCommands)) {
1134+
app.commands.register(commandName as keyof AllCommands, command);
1135+
}
11241136

11251137
ctx.subscriptions.push(
11261138
commandRunner(
@@ -1343,12 +1355,6 @@ async function activateWithInstalledDistribution(
13431355
),
13441356
);
13451357

1346-
ctx.subscriptions.push(
1347-
commandRunner("codeQL.openDocumentation", async () =>
1348-
env.openExternal(Uri.parse("https://codeql.github.com/docs/")),
1349-
),
1350-
);
1351-
13521358
ctx.subscriptions.push(
13531359
commandRunner("codeQL.copyVersion", async () => {
13541360
const text = `CodeQL extension version: ${
Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,70 @@
1-
export class CommandManager {}
1+
/**
2+
* Contains a generic implementation of typed commands.
3+
*
4+
* This allows different parts of the extension to register commands with a certain type,
5+
* and then allow other parts to call those commands in a well-typed manner.
6+
*/
7+
8+
import { Disposable } from "./Disposable";
9+
10+
/**
11+
* A command function is a completely untyped command.
12+
*/
13+
export type CommandFunction = (...args: any[]) => Promise<unknown>;
14+
15+
/**
16+
* The command manager basically takes a single input, the type
17+
* of all the known commands. The second parameter is provided by
18+
* default (and should not be needed by the caller) it is a
19+
* technicality to allow the type system to look up commands.
20+
*/
21+
export class CommandManager<
22+
Commands extends Record<string, CommandFunction>,
23+
CommandName extends keyof Commands & string = keyof Commands & string,
24+
> implements Disposable
25+
{
26+
// TODO: should this be a map?
27+
// TODO: handle multiple command names
28+
private commands: Disposable[] = [];
29+
30+
constructor(
31+
private readonly commandRegister: <T extends CommandName>(
32+
commandName: T,
33+
fn: Commands[T],
34+
) => Disposable,
35+
private readonly commandExecute: <T extends CommandName>(
36+
commandName: T,
37+
...args: Parameters<Commands[T]>
38+
) => Promise<Awaited<ReturnType<Commands[T]>>>,
39+
) {}
40+
41+
/**
42+
* Register a command with the specified name and implementation.
43+
*/
44+
register<T extends CommandName>(
45+
commandName: T,
46+
definition: Commands[T],
47+
): void {
48+
this.commands.push(this.commandRegister(commandName, definition));
49+
}
50+
51+
/**
52+
* Execute a command with the specified name and the provided arguments.
53+
*/
54+
execute<T extends CommandName>(
55+
commandName: T,
56+
...args: Parameters<Commands[T]>
57+
): Promise<Awaited<ReturnType<Commands[T]>>> {
58+
return this.commandExecute(commandName, ...args);
59+
}
60+
61+
/**
62+
* Dispose the manager, disposing all the registered commands.
63+
*/
64+
dispose(): void {
65+
this.commands.forEach((cmd) => {
66+
cmd.dispose();
67+
});
68+
this.commands = [];
69+
}
70+
}

0 commit comments

Comments
 (0)