Skip to content

Commit c1da623

Browse files
committed
Add creating extension packs when opening the editor
1 parent 3664803 commit c1da623

File tree

3 files changed

+269
-20
lines changed

3 files changed

+269
-20
lines changed

extensions/ql-vscode/src/data-extensions-editor/extension-pack-picker.ts

Lines changed: 122 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,38 @@
1-
import { relative, resolve, sep } from "path";
2-
import { pathExists, readFile } from "fs-extra";
3-
import { load as loadYaml } from "js-yaml";
1+
import { join, relative, resolve, sep } from "path";
2+
import { outputFile, pathExists, readFile } from "fs-extra";
3+
import { dump as dumpYaml, load as loadYaml } from "js-yaml";
44
import { minimatch } from "minimatch";
55
import { CancellationToken, window } from "vscode";
66
import { CodeQLCliServer } from "../cli";
7-
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from "../helpers";
7+
import {
8+
getOnDiskWorkspaceFolders,
9+
getOnDiskWorkspaceFoldersObjects,
10+
showAndLogErrorMessage,
11+
} from "../helpers";
812
import { ProgressCallback } from "../progress";
913
import { DatabaseItem } from "../local-databases";
1014
import { getQlPackPath, QLPACK_FILENAMES } from "../pure/ql";
1115

1216
const maxStep = 3;
1317

18+
const packNamePartRegex = /[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
19+
const packNameRegex = new RegExp(
20+
`^(?:(?<scope>${packNamePartRegex.source})/)?(?<name>${packNamePartRegex.source})$`,
21+
);
22+
const packNameLength = 128;
23+
1424
export async function pickExtensionPackModelFile(
1525
cliServer: Pick<CodeQLCliServer, "resolveQlpacks" | "resolveExtensions">,
1626
databaseItem: Pick<DatabaseItem, "name">,
1727
progress: ProgressCallback,
1828
token: CancellationToken,
1929
): Promise<string | undefined> {
20-
const extensionPackPath = await pickExtensionPack(cliServer, progress, token);
30+
const extensionPackPath = await pickExtensionPack(
31+
cliServer,
32+
databaseItem,
33+
progress,
34+
token,
35+
);
2136
if (!extensionPackPath) {
2237
return;
2338
}
@@ -38,6 +53,7 @@ export async function pickExtensionPackModelFile(
3853

3954
async function pickExtensionPack(
4055
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">,
56+
databaseItem: Pick<DatabaseItem, "name">,
4157
progress: ProgressCallback,
4258
token: CancellationToken,
4359
): Promise<string | undefined> {
@@ -50,10 +66,20 @@ async function pickExtensionPack(
5066
// Get all existing extension packs in the workspace
5167
const additionalPacks = getOnDiskWorkspaceFolders();
5268
const extensionPacks = await cliServer.resolveQlpacks(additionalPacks, true);
53-
const options = Object.keys(extensionPacks).map((pack) => ({
54-
label: pack,
55-
extensionPack: pack,
56-
}));
69+
70+
if (Object.keys(extensionPacks).length === 0) {
71+
return pickNewExtensionPack(databaseItem, token);
72+
}
73+
74+
const options: Array<{ label: string; extensionPack: string | null }> =
75+
Object.keys(extensionPacks).map((pack) => ({
76+
label: pack,
77+
extensionPack: pack,
78+
}));
79+
options.push({
80+
label: "Create new extension pack",
81+
extensionPack: null,
82+
});
5783

5884
progress({
5985
message: "Choosing extension pack...",
@@ -72,6 +98,10 @@ async function pickExtensionPack(
7298
return undefined;
7399
}
74100

101+
if (!extensionPackOption.extensionPack) {
102+
return pickNewExtensionPack(databaseItem, token);
103+
}
104+
75105
const extensionPackPaths = extensionPacks[extensionPackOption.extensionPack];
76106
if (extensionPackPaths.length !== 1) {
77107
void showAndLogErrorMessage(
@@ -153,6 +183,89 @@ async function pickModelFile(
153183
return pickNewModelFile(databaseItem, extensionPackPath, token);
154184
}
155185

186+
async function pickNewExtensionPack(
187+
databaseItem: Pick<DatabaseItem, "name">,
188+
token: CancellationToken,
189+
): Promise<string | undefined> {
190+
const workspaceFolders = getOnDiskWorkspaceFoldersObjects();
191+
const workspaceFolderOptions = workspaceFolders.map((folder) => ({
192+
label: folder.name,
193+
detail: folder.uri.fsPath,
194+
path: folder.uri.fsPath,
195+
}));
196+
197+
// We're not using window.showWorkspaceFolderPick because that also includes the database source folders while
198+
// we only want to include on-disk workspace folders.
199+
const workspaceFolder = await window.showQuickPick(workspaceFolderOptions, {
200+
title: "Select workspace folder to create extension pack in",
201+
});
202+
if (!workspaceFolder) {
203+
return undefined;
204+
}
205+
206+
const packName = await window.showInputBox(
207+
{
208+
title: "Create new extension pack",
209+
prompt: "Enter name of extension pack",
210+
placeHolder: `e.g. ${databaseItem.name}-extensions`,
211+
validateInput: async (value: string): Promise<string | undefined> => {
212+
if (!value) {
213+
return "Pack name must not be empty";
214+
}
215+
216+
if (value.length > packNameLength) {
217+
return `Pack name must be no longer than ${packNameLength} characters`;
218+
}
219+
220+
const matches = packNameRegex.exec(value);
221+
if (!matches?.groups) {
222+
return "Invalid package name: a pack name must contain only lowercase ASCII letters, ASCII digits, and hyphens";
223+
}
224+
225+
const packPath = join(workspaceFolder.path, matches.groups.name);
226+
if (await pathExists(packPath)) {
227+
return `A pack already exists at ${packPath}`;
228+
}
229+
230+
return undefined;
231+
},
232+
},
233+
token,
234+
);
235+
if (!packName) {
236+
return undefined;
237+
}
238+
239+
const matches = packNameRegex.exec(packName);
240+
if (!matches?.groups) {
241+
return;
242+
}
243+
244+
const name = matches.groups.name;
245+
const packPath = join(workspaceFolder.path, name);
246+
247+
if (await pathExists(packPath)) {
248+
return undefined;
249+
}
250+
251+
const packYamlPath = join(packPath, "codeql-pack.yml");
252+
253+
await outputFile(
254+
packYamlPath,
255+
dumpYaml({
256+
name,
257+
version: "0.0.0",
258+
library: true,
259+
extensionTargets: {
260+
"codeql/java-all": "*",
261+
},
262+
dataExtensions: ["models/**/*.yml"],
263+
}),
264+
);
265+
266+
return packPath;
267+
}
268+
156269
async function pickNewModelFile(
157270
databaseItem: Pick<DatabaseItem, "name">,
158271
extensionPackPath: string,

extensions/ql-vscode/src/helpers.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
window as Window,
1717
workspace,
1818
env,
19+
WorkspaceFolder,
1920
} from "vscode";
2021
import { CodeQLCliServer, QlpacksInfo } from "./cli";
2122
import { UserCancellationException } from "./progress";
@@ -249,16 +250,21 @@ export async function showInformationMessageWithAction(
249250
}
250251

251252
/** Gets all active workspace folders that are on the filesystem. */
252-
export function getOnDiskWorkspaceFolders() {
253+
export function getOnDiskWorkspaceFoldersObjects() {
253254
const workspaceFolders = workspace.workspaceFolders || [];
254-
const diskWorkspaceFolders: string[] = [];
255+
const diskWorkspaceFolders: WorkspaceFolder[] = [];
255256
for (const workspaceFolder of workspaceFolders) {
256257
if (workspaceFolder.uri.scheme === "file")
257-
diskWorkspaceFolders.push(workspaceFolder.uri.fsPath);
258+
diskWorkspaceFolders.push(workspaceFolder);
258259
}
259260
return diskWorkspaceFolders;
260261
}
261262

263+
/** Gets all active workspace folders that are on the filesystem. */
264+
export function getOnDiskWorkspaceFolders() {
265+
return getOnDiskWorkspaceFoldersObjects().map((folder) => folder.uri.fsPath);
266+
}
267+
262268
/** Check if folder is already present in workspace */
263269
export function isFolderAlreadyInWorkspace(folderName: string) {
264270
const workspaceFolders = workspace.workspaceFolders || [];

0 commit comments

Comments
 (0)