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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Exported for testing
import type { CodeQLCliServer, DbInfo } from "../../codeql-cli/cli";
import { Uri, workspace } from "vscode";
import { FileType, Uri, workspace } from "vscode";
import type { FullDatabaseOptions } from "./database-options";
import { basename, dirname, extname, join } from "path";
import {
Expand All @@ -9,7 +9,11 @@ import {
encodeSourceArchiveUri,
zipArchiveScheme,
} from "../../common/vscode/archive-filesystem-provider";
import type { DatabaseItem, PersistedDatabaseItem } from "./database-item";
import type {
DatabaseItem,
PersistedDatabaseItem,
SourceArchiveFile,
} from "./database-item";
import { isLikelyDatabaseRoot } from "./db-contents-heuristics";
import { stat } from "fs-extra";
import { containsPath, pathsEqual } from "../../common/files";
Expand All @@ -22,6 +26,8 @@ export class DatabaseItemImpl implements DatabaseItem {
public contents: DatabaseContents | undefined;
/** A cache of database info */
private _dbinfo: DbInfo | undefined;
/** A cache of source archive files */
private _sourceArchiveFiles: SourceArchiveFile[] | undefined;

public constructor(
public readonly databaseUri: Uri,
Expand Down Expand Up @@ -234,4 +240,66 @@ export class DatabaseItemImpl implements DatabaseItem {
return false;
}
}

public async getSourceArchiveFiles(): Promise<SourceArchiveFile[]> {
if (this._sourceArchiveFiles === undefined) {
this._sourceArchiveFiles = await this.collectSourceArchiveFiles();
}
return this._sourceArchiveFiles;
}

private async collectSourceArchiveFiles(): Promise<SourceArchiveFile[]> {
const explorerUri = this.getSourceArchiveExplorerUri();
const sourceArchiveZipPath =
decodeSourceArchiveUri(explorerUri).sourceArchiveZipPath;

const items: SourceArchiveFile[] = [];
await this.collectFilesRecursive(
explorerUri,
sourceArchiveZipPath,
"",
items,
);
// Sort by file name, then by path
items.sort((a, b) => {
const nameCmp = a.name.localeCompare(b.name);
if (nameCmp !== 0) {
return nameCmp;
}
return a.path.localeCompare(b.path);
});
return items;
}

private async collectFilesRecursive(
dirUri: Uri,
sourceArchiveZipPath: string,
prefix: string,
items: SourceArchiveFile[],
): Promise<void> {
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}`,
});

if (type === FileType.File) {
items.push({
name,
path: prefix,
uri: childUri,
});
} else if (type === FileType.Directory) {
await this.collectFilesRecursive(
childUri,
sourceArchiveZipPath,
childPath,
items,
);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ import type { DatabaseContents } from "./database-contents";
import type { DatabaseOptions } from "./database-options";
import type { DatabaseOrigin } from "./database-origin";

/** A file entry from the database's source archive. */
export interface SourceArchiveFile {
/** The file name (basename). */
name: string;
/** The path prefix (directory path relative to the source archive root). */
path: string;
/** The URI that can be used to open the file. */
uri: Uri;
}

/** An item in the list of available databases */
export interface DatabaseItem {
/** The URI of the database */
Expand Down Expand Up @@ -92,6 +102,12 @@ export interface DatabaseItem {
* Verifies that this database item has a zipped source folder. Returns an error message if it does not.
*/
verifyZippedSources(): string | undefined;

/**
* Returns all files in the database's source archive.
* The result is lazily computed and cached.
*/
getSourceArchiveFiles(): Promise<SourceArchiveFile[]>;
}

export interface PersistedDatabaseItem {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export {
DatabaseKind,
} from "./database-contents";
export { DatabaseChangedEvent, DatabaseEventKind } from "./database-events";
export { DatabaseItem } from "./database-item";
export { DatabaseItem, SourceArchiveFile } from "./database-item";
export { DatabaseItemImpl } from "./database-item-impl";
export { DatabaseManager } from "./database-manager";
export { DatabaseResolver } from "./database-resolver";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { QuickPickItem, Uri } from "vscode";
import { window, workspace } from "vscode";
import type { DatabaseItem } from "./local-databases";

interface SourceArchiveFileQuickPickItem extends QuickPickItem {
uri: Uri;
}

/**
* 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> {
const filesPromise = databaseItem.getSourceArchiveFiles();

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

try {
const files = await filesPromise;
quickPick.items = files.map((f) => ({
label: f.name,
description: f.path,
uri: f.uri,
}));
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