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

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

Comment on lines +24 to +30
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);
}
}

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 +67 to +75
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 on lines +97 to +101
});

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