Skip to content

Commit 061e792

Browse files
committed
Add new command: 'Go to File in Selected Database'
1 parent 205baa1 commit 061e792

4 files changed

Lines changed: 133 additions & 0 deletions

File tree

extensions/ql-vscode/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,10 @@
528528
"command": "codeQL.runQueryContextEditor",
529529
"title": "CodeQL: Run Query on Selected Database"
530530
},
531+
{
532+
"command": "codeQL.goToFile",
533+
"title": "CodeQL: Go to File in Selected Database"
534+
},
531535
{
532536
"command": "codeQL.runWarmOverlayBaseCacheForQuery",
533537
"title": "CodeQL: Warm Overlay-Base Cache for Query"
@@ -1874,6 +1878,9 @@
18741878
"command": "codeQL.gotoQLContextEditor",
18751879
"when": "false"
18761880
},
1881+
{
1882+
"command": "codeQL.goToFile"
1883+
},
18771884
{
18781885
"command": "codeQL.trimCache"
18791886
},

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,9 @@ export type LocalDatabasesCommands = {
260260
// Internal commands
261261
"codeQLDatabases.removeOrphanedDatabases": () => Promise<void>;
262262
"codeQL.getCurrentDatabase": () => Promise<string | undefined>;
263+
264+
// Source archive file search
265+
"codeQL.goToFile": () => Promise<void>;
263266
};
264267

265268
// Commands tied to variant analysis

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import type { QueryRunner } from "../query-server";
4545
import type { App } from "../common/app";
4646
import { redactableError } from "../common/errors";
4747
import type { LocalDatabasesCommands } from "../common/commands";
48+
import { searchSourceArchiveFiles } from "./source-archive-file-search";
4849
import {
4950
createMultiSelectionCommand,
5051
createSingleSelectionCommand,
@@ -317,9 +318,22 @@ export class DatabaseUI extends DisposableObject {
317318
),
318319
"codeQLDatabases.removeOrphanedDatabases":
319320
this.handleRemoveOrphanedDatabases.bind(this),
321+
"codeQL.goToFile": this.handleGoToFile.bind(this),
320322
};
321323
}
322324

325+
private async handleGoToFile(): Promise<void> {
326+
const currentDb = this.databaseManager.currentDatabaseItem;
327+
if (!currentDb) {
328+
void showAndLogErrorMessage(
329+
this.app.logger,
330+
"No CodeQL database selected. Please select a database first.",
331+
);
332+
return;
333+
}
334+
await searchSourceArchiveFiles(currentDb);
335+
}
336+
323337
private async handleMakeCurrentDatabase(
324338
databaseItem: DatabaseItem,
325339
): Promise<void> {
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import type { QuickPickItem, Uri } from "vscode";
2+
import { FileType, window, workspace } from "vscode";
3+
import type { DatabaseItem } from "./local-databases";
4+
import {
5+
encodeSourceArchiveUri,
6+
decodeSourceArchiveUri,
7+
} from "../common/vscode/archive-filesystem-provider";
8+
9+
interface SourceArchiveFileQuickPickItem extends QuickPickItem {
10+
uri: Uri;
11+
}
12+
13+
/**
14+
* Recursively collects all file URIs from a source archive directory.
15+
*/
16+
async function collectFiles(
17+
dirUri: Uri,
18+
sourceArchiveZipPath: string,
19+
prefix: string,
20+
): Promise<SourceArchiveFileQuickPickItem[]> {
21+
const entries = await workspace.fs.readDirectory(dirUri);
22+
const items: SourceArchiveFileQuickPickItem[] = [];
23+
24+
for (const [name, type] of entries) {
25+
const childPath = prefix ? `${prefix}/${name}` : name;
26+
const childUri = encodeSourceArchiveUri({
27+
sourceArchiveZipPath,
28+
pathWithinSourceArchive: `${decodeSourceArchiveUri(dirUri).pathWithinSourceArchive}/${name}`,
29+
});
30+
31+
if (type === FileType.File) {
32+
items.push({
33+
label: name,
34+
description: prefix,
35+
uri: childUri,
36+
});
37+
} else if (type === FileType.Directory) {
38+
const subItems = await collectFiles(
39+
childUri,
40+
sourceArchiveZipPath,
41+
childPath,
42+
);
43+
items.push(...subItems);
44+
}
45+
}
46+
47+
return items;
48+
}
49+
50+
/**
51+
* Shows a Quick Pick to search for and open a file from the source archive
52+
* of the given database.
53+
*/
54+
export async function searchSourceArchiveFiles(
55+
databaseItem: DatabaseItem,
56+
): Promise<void> {
57+
let explorerUri: Uri;
58+
try {
59+
explorerUri = databaseItem.getSourceArchiveExplorerUri();
60+
} catch (e) {
61+
void window.showErrorMessage(e instanceof Error ? e.message : String(e));
62+
return;
63+
}
64+
const sourceArchiveZipPath =
65+
decodeSourceArchiveUri(explorerUri).sourceArchiveZipPath;
66+
67+
const quickPick = window.createQuickPick<SourceArchiveFileQuickPickItem>();
68+
quickPick.placeholder = "Go to File in Selected Database...";
69+
quickPick.matchOnDescription = true;
70+
quickPick.busy = true;
71+
quickPick.show();
72+
73+
try {
74+
const items = await collectFiles(explorerUri, sourceArchiveZipPath, "");
75+
// Sort items by file name, then by path
76+
items.sort((a, b) => {
77+
const nameCmp = a.label.localeCompare(b.label);
78+
if (nameCmp !== 0) {
79+
return nameCmp;
80+
}
81+
return (a.description ?? "").localeCompare(b.description ?? "");
82+
});
83+
quickPick.items = items;
84+
quickPick.busy = false;
85+
} catch (e) {
86+
quickPick.dispose();
87+
void window.showErrorMessage(
88+
`Failed to read source archive: ${e instanceof Error ? e.message : String(e)}`,
89+
);
90+
return;
91+
}
92+
93+
return new Promise<void>((resolve) => {
94+
quickPick.onDidAccept(async () => {
95+
const selected = quickPick.selectedItems[0];
96+
quickPick.dispose();
97+
if (selected) {
98+
const doc = await workspace.openTextDocument(selected.uri);
99+
await window.showTextDocument(doc);
100+
}
101+
resolve();
102+
});
103+
104+
quickPick.onDidHide(() => {
105+
quickPick.dispose();
106+
resolve();
107+
});
108+
});
109+
}

0 commit comments

Comments
 (0)