Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions extensions/ql-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,10 @@
"command": "codeQL.runQueryContextEditor",
"title": "CodeQL: Run Query on Selected Database"
},
{
"command": "codeQL.goToFile",
"title": "CodeQL: Go to File in Selected Database"
},
{
"command": "codeQL.runWarmOverlayBaseCacheForQuery",
"title": "CodeQL: Warm Overlay-Base Cache for Query"
Expand Down Expand Up @@ -1874,6 +1878,9 @@
"command": "codeQL.gotoQLContextEditor",
"when": "false"
},
{
"command": "codeQL.goToFile"
},
{
"command": "codeQL.trimCache"
},
Expand Down
3 changes: 3 additions & 0 deletions extensions/ql-vscode/src/common/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,9 @@ export type LocalDatabasesCommands = {
// Internal commands
"codeQLDatabases.removeOrphanedDatabases": () => Promise<void>;
"codeQL.getCurrentDatabase": () => Promise<string | undefined>;

// Source archive file search
"codeQL.goToFile": () => Promise<void>;
};

// Commands tied to variant analysis
Expand Down
14 changes: 14 additions & 0 deletions extensions/ql-vscode/src/databases/local-databases-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import type { QueryRunner } from "../query-server";
import type { App } from "../common/app";
import { redactableError } from "../common/errors";
import type { LocalDatabasesCommands } from "../common/commands";
import { searchSourceArchiveFiles } from "./source-archive-file-search";
import {
createMultiSelectionCommand,
createSingleSelectionCommand,
Expand Down Expand Up @@ -317,9 +318,22 @@ export class DatabaseUI extends DisposableObject {
),
"codeQLDatabases.removeOrphanedDatabases":
this.handleRemoveOrphanedDatabases.bind(this),
"codeQL.goToFile": this.handleGoToFile.bind(this),
};
}

private async handleGoToFile(): Promise<void> {
const currentDb = this.databaseManager.currentDatabaseItem;
if (!currentDb) {
void showAndLogErrorMessage(
this.app.logger,
"No CodeQL database selected. Please select a database first.",
);
return;
}
await searchSourceArchiveFiles(currentDb);
}
Comment thread
hvitved marked this conversation as resolved.

private async handleMakeCurrentDatabase(
databaseItem: DatabaseItem,
): Promise<void> {
Expand Down
109 changes: 109 additions & 0 deletions extensions/ql-vscode/src/databases/source-archive-file-search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type { QuickPickItem, Uri } from "vscode";
import { FileType, window, workspace } from "vscode";
import type { DatabaseItem } from "./local-databases";
import {
encodeSourceArchiveUri,
decodeSourceArchiveUri,
} from "../common/vscode/archive-filesystem-provider";

interface SourceArchiveFileQuickPickItem extends QuickPickItem {
uri: Uri;
}

/**
* Recursively collects all file URIs from a source archive directory.
*/
async function collectFiles(
dirUri: Uri,
sourceArchiveZipPath: string,
prefix: string,
Comment thread
hvitved marked this conversation as resolved.
): Promise<SourceArchiveFileQuickPickItem[]> {
const entries = await workspace.fs.readDirectory(dirUri);
const items: SourceArchiveFileQuickPickItem[] = [];
Comment thread
hvitved marked this conversation as resolved.
Outdated

for (const [name, type] of entries) {
const childPath = prefix ? `${prefix}/${name}` : name;
const childUri = encodeSourceArchiveUri({
sourceArchiveZipPath,
pathWithinSourceArchive: `${decodeSourceArchiveUri(dirUri).pathWithinSourceArchive}/${name}`,
});

Comment thread
hvitved marked this conversation as resolved.
if (type === FileType.File) {
items.push({
label: name,
description: prefix,
uri: childUri,
});
} else if (type === FileType.Directory) {
const subItems = await collectFiles(
childUri,
sourceArchiveZipPath,
childPath,
);
items.push(...subItems);
Comment thread
hvitved marked this conversation as resolved.
Outdated
}
}

return items;
}

/**
* Shows a Quick Pick to search for and open a file from the source archive
* of the given database.
*/
export async function searchSourceArchiveFiles(
databaseItem: DatabaseItem,
): Promise<void> {
let explorerUri: Uri;
try {
explorerUri = databaseItem.getSourceArchiveExplorerUri();
} catch (e) {
void window.showErrorMessage(e instanceof Error ? e.message : String(e));
return;
}
const sourceArchiveZipPath =
decodeSourceArchiveUri(explorerUri).sourceArchiveZipPath;

const quickPick = window.createQuickPick<SourceArchiveFileQuickPickItem>();
quickPick.placeholder = "Go to File in Selected Database...";
quickPick.matchOnDescription = true;
quickPick.busy = true;
quickPick.show();

try {
const items = await collectFiles(explorerUri, sourceArchiveZipPath, "");
// Sort items by file name, then by path
Comment on lines +64 to +72
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is correct, although it only matters in extremely niche situations.

The returned promise is ultimately only used by the command handlers, which doesn't matter for UI-triggered commands, but it can matter if the command is invoked programmatically through executeCommand.

The fix should be straightforward though, just move the Promise creation up before the show() call and store the promise in a variable.

items.sort((a, b) => {
const nameCmp = a.label.localeCompare(b.label);
if (nameCmp !== 0) {
return nameCmp;
}
return (a.description ?? "").localeCompare(b.description ?? "");
});
quickPick.items = items;
quickPick.busy = false;
} catch (e) {
quickPick.dispose();
void window.showErrorMessage(
`Failed to read source archive: ${e instanceof Error ? e.message : String(e)}`,
);
return;
}

return new Promise<void>((resolve) => {
quickPick.onDidAccept(async () => {
const selected = quickPick.selectedItems[0];
quickPick.dispose();
if (selected) {
const doc = await workspace.openTextDocument(selected.uri);
await window.showTextDocument(doc);
}
resolve();
Comment thread
hvitved marked this conversation as resolved.
Outdated
});

quickPick.onDidHide(() => {
quickPick.dispose();
resolve();
});
});
}
Loading