Skip to content

Commit 2053b5d

Browse files
committed
Add configurable model filename to data extension editor
This adds a pickable model filename from an existing extension pack to the data extensions editor. This allows the user to edit one of their existing data extensions. This does not yet add the ability to create new extension packs and/or new model files. This uses the `codeql resolve extensions` command to get the list of available model files. This should be available in all CLI versions which the data extensions editor supports.
1 parent ae08a1b commit 2053b5d

File tree

4 files changed

+183
-51
lines changed

4 files changed

+183
-51
lines changed

extensions/ql-vscode/src/cli.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,20 @@ export type MlModelInfo = {
107107
/** The expected output of `codeql resolve ml-models`. */
108108
export type MlModelsInfo = { models: MlModelInfo[] };
109109

110+
export type DataExtensionInfo = {
111+
predicate: string;
112+
file: string;
113+
index: number;
114+
};
115+
116+
/** The expected output of `codeql resolve extensions`. */
117+
export type ExtensionsInfo = {
118+
models: MlModelInfo[];
119+
data: {
120+
[filename: string]: DataExtensionInfo[];
121+
};
122+
};
123+
110124
/**
111125
* The expected output of `codeql resolve qlref`.
112126
*/
@@ -1192,6 +1206,29 @@ export class CodeQLCliServer implements Disposable {
11921206
);
11931207
}
11941208

1209+
/**
1210+
* Gets information about available extensions
1211+
* @param suite The suite to resolve.
1212+
* @param additionalPacks A list of directories to search for qlpacks.
1213+
* @returns An object containing the list of models and extensions
1214+
*/
1215+
async resolveExtensions(
1216+
suite: string,
1217+
additionalPacks: string[],
1218+
): Promise<ExtensionsInfo> {
1219+
const args = this.getAdditionalPacksArg(additionalPacks);
1220+
args.push(suite);
1221+
1222+
return this.runJsonCodeQlCliCommand<ExtensionsInfo>(
1223+
["resolve", "extensions"],
1224+
args,
1225+
"Resolving extensions",
1226+
{
1227+
addFormat: false,
1228+
},
1229+
);
1230+
}
1231+
11951232
/**
11961233
* Gets information about the available languages.
11971234
* @returns A dictionary mapping language name to the directory it comes from

extensions/ql-vscode/src/data-extensions-editor/data-extensions-editor-module.ts

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { ensureDir } from "fs-extra";
88
import { join } from "path";
99
import { App } from "../common/app";
1010
import { showAndLogErrorMessage } from "../helpers";
11+
import { withProgress } from "../progress";
12+
import { pickExtensionPack, pickModelFile } from "./extension-packs";
1113

1214
export class DataExtensionsEditorModule {
1315
private readonly queryStorageDir: string;
@@ -49,31 +51,55 @@ export class DataExtensionsEditorModule {
4951

5052
public getCommands(): DataExtensionsEditorCommands {
5153
return {
52-
"codeQL.openDataExtensionsEditor": async () => {
53-
const db = this.databaseManager.currentDatabaseItem;
54-
if (!db) {
55-
void showAndLogErrorMessage("No database selected");
56-
return;
57-
}
54+
"codeQL.openDataExtensionsEditor": async () =>
55+
withProgress(
56+
async (progress) => {
57+
const db = this.databaseManager.currentDatabaseItem;
58+
if (!db) {
59+
void showAndLogErrorMessage("No database selected");
60+
return;
61+
}
5862

59-
if (!(await this.cliServer.cliConstraints.supportsQlpacksKind())) {
60-
void showAndLogErrorMessage(
61-
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND.format()} or later.`,
62-
);
63-
return;
64-
}
63+
if (!(await this.cliServer.cliConstraints.supportsQlpacksKind())) {
64+
void showAndLogErrorMessage(
65+
`This feature requires CodeQL CLI version ${CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND.format()} or later.`,
66+
);
67+
return;
68+
}
6569

66-
const view = new DataExtensionsEditorView(
67-
this.ctx,
68-
this.app,
69-
this.databaseManager,
70-
this.cliServer,
71-
this.queryRunner,
72-
this.queryStorageDir,
73-
db,
74-
);
75-
await view.openView();
76-
},
70+
const extensionPackPath = await pickExtensionPack(
71+
this.cliServer,
72+
progress,
73+
);
74+
if (!extensionPackPath) {
75+
return;
76+
}
77+
78+
const modelFile = await pickModelFile(
79+
this.cliServer,
80+
progress,
81+
extensionPackPath,
82+
);
83+
if (!modelFile) {
84+
return;
85+
}
86+
87+
const view = new DataExtensionsEditorView(
88+
this.ctx,
89+
this.app,
90+
this.databaseManager,
91+
this.cliServer,
92+
this.queryRunner,
93+
this.queryStorageDir,
94+
db,
95+
modelFile,
96+
);
97+
await view.openView();
98+
},
99+
{
100+
title: "Opening Data Extensions Editor",
101+
},
102+
),
77103
};
78104
}
79105

extensions/ql-vscode/src/data-extensions-editor/data-extensions-editor-view.ts

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {
22
CancellationTokenSource,
33
ExtensionContext,
4-
Uri,
54
ViewColumn,
65
window,
76
workspace,
@@ -61,6 +60,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
6160
private readonly queryRunner: QueryRunner,
6261
private readonly queryStorageDir: string,
6362
private readonly databaseItem: DatabaseItem,
63+
private readonly modelFilename: string,
6464
) {
6565
super(ctx);
6666
}
@@ -148,29 +148,19 @@ export class DataExtensionsEditorView extends AbstractWebview<
148148
externalApiUsages: ExternalApiUsage[],
149149
modeledMethods: Record<string, ModeledMethod>,
150150
): Promise<void> {
151-
const modelFilename = this.calculateModelFilename();
152-
if (!modelFilename) {
153-
return;
154-
}
155-
156151
const yaml = createDataExtensionYaml(externalApiUsages, modeledMethods);
157152

158-
await writeFile(modelFilename, yaml);
153+
await writeFile(this.modelFilename, yaml);
159154

160-
void extLogger.log(`Saved data extension YAML to ${modelFilename}`);
155+
void extLogger.log(`Saved data extension YAML to ${this.modelFilename}`);
161156
}
162157

163158
protected async loadExistingModeledMethods(): Promise<void> {
164-
const modelFilename = this.calculateModelFilename();
165-
if (!modelFilename) {
166-
return;
167-
}
168-
169159
try {
170-
const yaml = await readFile(modelFilename, "utf8");
160+
const yaml = await readFile(this.modelFilename, "utf8");
171161

172162
const data = loadYaml(yaml, {
173-
filename: modelFilename,
163+
filename: this.modelFilename,
174164
});
175165

176166
const existingModeledMethods = loadDataExtensionYaml(data);
@@ -365,17 +355,4 @@ export class DataExtensionsEditorView extends AbstractWebview<
365355
message: "",
366356
});
367357
}
368-
369-
private calculateModelFilename(): string | undefined {
370-
const workspaceFolder = getQlSubmoduleFolder();
371-
if (!workspaceFolder) {
372-
return;
373-
}
374-
375-
return Uri.joinPath(
376-
workspaceFolder.uri,
377-
"java/ql/lib/ext",
378-
`${this.databaseItem.name.replaceAll("/", ".")}.model.yml`,
379-
).fsPath;
380-
}
381358
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { relative } from "path";
2+
import { window } from "vscode";
3+
import { CodeQLCliServer } from "../cli";
4+
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from "../helpers";
5+
import { ProgressCallback } from "../progress";
6+
7+
export async function pickExtensionPack(
8+
cliServer: CodeQLCliServer,
9+
progress: ProgressCallback,
10+
): Promise<string | undefined> {
11+
progress({
12+
message: "Resolving extension packs...",
13+
step: 1,
14+
maxStep: 3,
15+
});
16+
17+
// Get all existing extension packs in the workspace
18+
const additionalPacks = getOnDiskWorkspaceFolders();
19+
const extensionPacks = await cliServer.resolveQlpacks(additionalPacks, true);
20+
const options = Object.keys(extensionPacks).map((pack) => ({
21+
label: pack,
22+
extensionPack: pack,
23+
}));
24+
25+
progress({
26+
message: "Choosing extension pack...",
27+
step: 2,
28+
maxStep: 3,
29+
});
30+
31+
const extensionPackOption = await window.showQuickPick(options, {
32+
title: "Select extension pack to use",
33+
});
34+
if (!extensionPackOption) {
35+
return undefined;
36+
}
37+
38+
const extensionPackPaths = extensionPacks[extensionPackOption.extensionPack];
39+
if (extensionPackPaths.length !== 1) {
40+
void showAndLogErrorMessage(
41+
`Extension pack ${extensionPackOption.extensionPack} could not be resolved to a single location`,
42+
);
43+
return undefined;
44+
}
45+
46+
return extensionPackPaths[0];
47+
}
48+
49+
export async function pickModelFile(
50+
cliServer: CodeQLCliServer,
51+
progress: ProgressCallback,
52+
extensionPackPath: string,
53+
): Promise<string | undefined> {
54+
// Find the existing model files in the extension pack
55+
const additionalPacks = getOnDiskWorkspaceFolders();
56+
const extensions = await cliServer.resolveExtensions(
57+
extensionPackPath,
58+
additionalPacks,
59+
);
60+
61+
const modelFiles = new Set<string>();
62+
63+
if (extensionPackPath in extensions.data) {
64+
for (const extension of extensions.data[extensionPackPath]) {
65+
modelFiles.add(extension.file);
66+
}
67+
}
68+
69+
const fileOptions: Array<{ label: string; file: string }> = [];
70+
for (const file of modelFiles) {
71+
fileOptions.push({
72+
label: relative(extensionPackPath, file),
73+
file,
74+
});
75+
}
76+
77+
progress({
78+
message: "Choosing model file...",
79+
step: 3,
80+
maxStep: 3,
81+
});
82+
83+
const fileOption = await window.showQuickPick(fileOptions, {
84+
title: "Select model file to use",
85+
});
86+
87+
if (!fileOption) {
88+
return;
89+
}
90+
91+
return fileOption.file;
92+
}

0 commit comments

Comments
 (0)