Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions extensions/ql-vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- Remove support for CodeQL CLI versions older than 2.22.4. [#4344](https://github.com/github/vscode-codeql/pull/4344)
- Added support for selection-based result filtering via a checkbox in the result viewer. When enabled, only results from the currently-viewed file are shown. Additionally, if the editor selection is non-empty, only results within the selection range are shown. [#4362](https://github.com/github/vscode-codeql/pull/4362)
- Added a new "CodeQL: Go to File in Selected Database" command that allows you to open a file from the source archive of the currently selected database. [#4390](https://github.com/github/vscode-codeql/pull/4390)

## 1.17.7 - 5 December 2025

Expand Down
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
113 changes: 113 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,113 @@
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.
items: SourceArchiveFileQuickPickItem[] = [],
): Promise<SourceArchiveFileQuickPickItem[]> {
const entries = await workspace.fs.readDirectory(dirUri);

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) {
await collectFiles(childUri, sourceArchiveZipPath, childPath, items);
}
}

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 filesPromise = collectFiles(explorerUri, sourceArchiveZipPath, "");
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.

It looks like we are walking the entire source archive each time the user invokes the go-to-file command. Is that the case? If so, have you considered moving the tree walk outside the hot path?


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 filesPromise;
// 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();
try {
if (selected) {
const doc = await workspace.openTextDocument(selected.uri);
await window.showTextDocument(doc);
}
} catch (e) {
void window.showErrorMessage(
`Failed to open source archive file: ${e instanceof Error ? e.message : String(e)}`,
);
} finally {
resolve();
}
});

quickPick.onDidHide(() => {
quickPick.dispose();
resolve();
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Uri } from "vscode";
import { DatabaseUI } from "../../../../src/databases/local-databases-ui";
import { testDisposeHandler } from "../../test-dispose-handler";
import { createMockApp } from "../../../__mocks__/appMock";
import { mockedObject } from "../../utils/mocking.helpers";
import type { DatabaseFetcher } from "../../../../src/databases/database-fetcher";
import type { DatabaseItem } from "../../../../src/databases/local-databases";
import { searchSourceArchiveFiles } from "../../../../src/databases/source-archive-file-search";

jest.mock("../../../../src/databases/source-archive-file-search");
const mockedSearchSourceArchiveFiles = jest.mocked(searchSourceArchiveFiles);

describe("handleGoToFile", () => {
const app = createMockApp({});
const storageDir = "/tmp/test-storage";

afterEach(() => {
jest.restoreAllMocks();
});

describe("when there is no current database", () => {
const databaseUI = new DatabaseUI(
app,
{
databaseItems: [],
onDidChangeDatabaseItem: () => {
/**/
},
onDidChangeCurrentDatabaseItem: () => {
/**/
},
setCurrentDatabaseItem: () => {},
currentDatabaseItem: undefined,
} as any,
mockedObject<DatabaseFetcher>({}),
{
onLanguageContextChanged: () => {
/**/
},
} as any,
{} as any,
storageDir,
storageDir,
);

afterAll(() => {
databaseUI.dispose(testDisposeHandler);
});

it("should show an error message", async () => {
const commands = databaseUI.getCommands();
await commands["codeQL.goToFile"]();

expect(mockedSearchSourceArchiveFiles).not.toHaveBeenCalled();
expect(app.logger.showErrorMessage).toHaveBeenCalledWith(
expect.stringContaining("No CodeQL database selected"),
);
});
});

describe("when there is a current database", () => {
const mockDbItem = mockedObject<DatabaseItem>({
databaseUri: Uri.file("/test/db"),
name: "test-db",
language: "javascript",
sourceArchive: Uri.file("/test/db/src.zip"),
});

const databaseUI = new DatabaseUI(
app,
{
databaseItems: [mockDbItem],
onDidChangeDatabaseItem: () => {
/**/
},
onDidChangeCurrentDatabaseItem: () => {
/**/
},
setCurrentDatabaseItem: () => {},
currentDatabaseItem: mockDbItem,
} as any,
mockedObject<DatabaseFetcher>({}),
{
onLanguageContextChanged: () => {
/**/
},
} as any,
{} as any,
storageDir,
storageDir,
);

afterAll(() => {
databaseUI.dispose(testDisposeHandler);
});

it("should call searchSourceArchiveFiles with the current database", async () => {
mockedSearchSourceArchiveFiles.mockResolvedValue(undefined);

const commands = databaseUI.getCommands();
await commands["codeQL.goToFile"]();

expect(mockedSearchSourceArchiveFiles).toHaveBeenCalledWith(mockDbItem);
});
});
});
Loading