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,
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.

Suggested change
prefix: string,
prefix: string,
items: SourceArchiveFileQuickPickItem[] = [],

I'd move items to an accumulator parameter to avoid the N^2 cost of building up intermediate lists

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

Suggested change
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);
Comment on lines +41 to +43
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.

Suggested change
childPath,
);
items.push(...subItems);
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 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
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 on lines +97 to +101
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.

The suggestion here looks good to me.

});

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