Skip to content

Commit dc55ef9

Browse files
authored
Merge pull request #2157 from github/starcke/commands-registration
Commands registration
2 parents 3d9f55f + 5303ec6 commit dc55ef9

14 files changed

Lines changed: 304 additions & 21 deletions

File tree

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
@@ -136,6 +136,7 @@ import { RepositoriesFilterSortStateWithIds } from "./pure/variant-analysis-filt
136136
import { DbModule } from "./databases/db-module";
137137
import { redactableError } from "./pure/errors";
138138
import { QueryHistoryDirs } from "./query-history/query-history-dirs";
139+
import { AllCommands, BaseCommands } from "./common/commands";
139140

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

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

1194-
ctx.subscriptions.push(
1195-
commandRunner(
1196-
"codeQL.openVariantAnalysisLogs",
1197-
async (variantAnalysisId: number) => {
1198-
await variantAnalysisManager.openVariantAnalysisLogs(variantAnalysisId);
1199-
},
1200-
),
1201-
);
1206+
const allCommands: AllCommands = {
1207+
...getCommands(),
1208+
...variantAnalysisManager.getCommands(),
1209+
};
1210+
1211+
for (const [commandName, command] of Object.entries(allCommands)) {
1212+
app.commands.register(commandName as keyof AllCommands, command);
1213+
}
12021214

12031215
ctx.subscriptions.push(
12041216
commandRunner(
@@ -1410,12 +1422,6 @@ async function activateWithInstalledDistribution(
14101422
),
14111423
);
14121424

1413-
ctx.subscriptions.push(
1414-
commandRunner("codeQL.openDocumentation", async () =>
1415-
env.openExternal(Uri.parse("https://codeql.github.com/docs/")),
1416-
),
1417-
);
1418-
14191425
ctx.subscriptions.push(
14201426
commandRunner("codeQL.copyVersion", async () => {
14211427
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+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* This interface mirrors the vscode.Disaposable class, so that
3+
* the command manager does not depend on vscode directly.
4+
*/
5+
export interface Disposable {
6+
dispose(): void;
7+
}

extensions/ql-vscode/src/variant-analysis/variant-analysis-manager.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import { URLSearchParams } from "url";
6262
import { DbManager } from "../databases/db-manager";
6363
import { App } from "../common/app";
6464
import { redactableError } from "../pure/errors";
65+
import { AppCommandManager, VariantAnalysisCommands } from "../common/commands";
6566

6667
export class VariantAnalysisManager
6768
extends DisposableObject
@@ -123,6 +124,18 @@ export class VariantAnalysisManager
123124
);
124125
}
125126

127+
getCommands(): VariantAnalysisCommands {
128+
return {
129+
"codeQL.openVariantAnalysisLogs": async (variantAnalysisId: number) => {
130+
await this.openVariantAnalysisLogs(variantAnalysisId);
131+
},
132+
};
133+
}
134+
135+
get commandManager(): AppCommandManager {
136+
return this.app.commands;
137+
}
138+
126139
public async runVariantAnalysis(
127140
uri: Uri | undefined,
128141
progress: ProgressCallback,

extensions/ql-vscode/src/variant-analysis/variant-analysis-view-manager.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
VariantAnalysis,
33
VariantAnalysisScannedRepositoryState,
44
} from "./shared/variant-analysis";
5+
import { AppCommandManager } from "../common/commands";
56

67
export interface VariantAnalysisViewInterface {
78
variantAnalysisId: number;
@@ -11,6 +12,8 @@ export interface VariantAnalysisViewInterface {
1112
export interface VariantAnalysisViewManager<
1213
T extends VariantAnalysisViewInterface,
1314
> {
15+
commandManager: AppCommandManager;
16+
1417
registerView(view: T): void;
1518
unregisterView(view: T): void;
1619
getView(variantAnalysisId: number): T | undefined;

extensions/ql-vscode/src/variant-analysis/variant-analysis-view.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export class VariantAnalysisView
145145
);
146146
break;
147147
case "openLogs":
148-
await commands.executeCommand(
148+
await this.manager.commandManager.execute(
149149
"codeQL.openVariantAnalysisLogs",
150150
this.variantAnalysisId,
151151
);

0 commit comments

Comments
 (0)