diff --git a/extensions/ql-vscode/src/commandRunner.ts b/extensions/ql-vscode/src/commandRunner.ts index 6f316be2b..7079723c1 100644 --- a/extensions/ql-vscode/src/commandRunner.ts +++ b/extensions/ql-vscode/src/commandRunner.ts @@ -89,7 +89,7 @@ export type ProgressTaskWithArgs = ( * @param args arguments passed to this task passed on from * `commands.registerCommand`. */ -type NoProgressTask = (...args: any[]) => Promise; +export type NoProgressTask = (...args: any[]) => Promise; /** * This mediates between the kind of progress callbacks we want to diff --git a/extensions/ql-vscode/src/common/commands.ts b/extensions/ql-vscode/src/common/commands.ts index 862b8caac..e5bfb786c 100644 --- a/extensions/ql-vscode/src/common/commands.ts +++ b/extensions/ql-vscode/src/common/commands.ts @@ -1,5 +1,5 @@ import type { CommandManager } from "../packages/commands"; -import type { Uri } from "vscode"; +import type { Uri, Range } from "vscode"; import type { DbTreeViewItem } from "../databases/ui/db-tree-view-item"; import type { DatabaseItem } from "../local-databases"; import type { QueryHistoryInfo } from "../query-history/query-history-info"; @@ -31,6 +31,21 @@ export type BaseCommands = { "codeQL.restartQueryServer": () => Promise; }; +// Commands used for running local queries +export type LocalQueryCommands = { + "codeQL.runQuery": (uri?: Uri) => Promise; + "codeQL.runQueryContextEditor": (uri?: Uri) => Promise; + "codeQL.runQueryOnMultipleDatabases": (uri?: Uri) => Promise; + "codeQL.runQueryOnMultipleDatabasesContextEditor": ( + uri?: Uri, + ) => Promise; + "codeQL.runQueries": SelectionCommandFunction; + "codeQL.quickEval": (uri: Uri) => Promise; + "codeQL.quickEvalContextEditor": (uri: Uri) => Promise; + "codeQL.codeLensQuickEval": (uri: Uri, range: Range) => Promise; + "codeQL.quickQuery": () => Promise; +}; + // Commands used for the query history panel export type QueryHistoryCommands = { // Commands in the "navigation" group @@ -65,27 +80,42 @@ export type QueryHistoryCommands = { // Commands used for the local databases panel export type LocalDatabasesCommands = { - "codeQL.setCurrentDatabase": (uri: Uri) => Promise; - "codeQL.setDefaultTourDatabase": () => Promise; + // Command palette commands + "codeQL.chooseDatabaseFolder": () => Promise; + "codeQL.chooseDatabaseArchive": () => Promise; + "codeQL.chooseDatabaseInternet": () => Promise; + "codeQL.chooseDatabaseGithub": () => Promise; "codeQL.upgradeCurrentDatabase": () => Promise; "codeQL.clearCache": () => Promise; + // Explorer context menu + "codeQL.setCurrentDatabase": (uri: Uri) => Promise; + + // Database panel view title commands "codeQLDatabases.chooseDatabaseFolder": () => Promise; "codeQLDatabases.chooseDatabaseArchive": () => Promise; "codeQLDatabases.chooseDatabaseInternet": () => Promise; "codeQLDatabases.chooseDatabaseGithub": () => Promise; + "codeQLDatabases.sortByName": () => Promise; + "codeQLDatabases.sortByDateAdded": () => Promise; + + // Database panel context menu "codeQLDatabases.setCurrentDatabase": ( databaseItem: DatabaseItem, ) => Promise; - "codeQLDatabases.sortByName": () => Promise; - "codeQLDatabases.sortByDateAdded": () => Promise; - "codeQLDatabases.removeOrphanedDatabases": () => Promise; + // Database panel selection commands "codeQLDatabases.removeDatabase": SelectionCommandFunction; "codeQLDatabases.upgradeDatabase": SelectionCommandFunction; "codeQLDatabases.renameDatabase": SelectionCommandFunction; "codeQLDatabases.openDatabaseFolder": SelectionCommandFunction; "codeQLDatabases.addDatabaseSource": SelectionCommandFunction; + + // Codespace template commands + "codeQL.setDefaultTourDatabase": () => Promise; + + // Internal commands + "codeQLDatabases.removeOrphanedDatabases": () => Promise; }; // Commands tied to variant analysis @@ -110,10 +140,19 @@ export type DatabasePanelCommands = { "codeQLVariantAnalysisRepositories.removeItemContextMenu": SingleSelectionCommandFunction; }; +export type EvalLogViewerCommands = { + "codeQLEvalLogViewer.clear": () => Promise; +}; + export type AllCommands = BaseCommands & QueryHistoryCommands & LocalDatabasesCommands & VariantAnalysisCommands & - DatabasePanelCommands; + DatabasePanelCommands & + EvalLogViewerCommands; export type AppCommandManager = CommandManager; + +// Separate command manager because it uses a different logger +export type QueryServerCommands = LocalQueryCommands; +export type QueryServerCommandManager = CommandManager; diff --git a/extensions/ql-vscode/src/common/vscode/commands.ts b/extensions/ql-vscode/src/common/vscode/commands.ts index 86e32faa4..fc8db2a78 100644 --- a/extensions/ql-vscode/src/common/vscode/commands.ts +++ b/extensions/ql-vscode/src/common/vscode/commands.ts @@ -1,6 +1,7 @@ import { commands } from "vscode"; -import { commandRunner } from "../../commandRunner"; +import { commandRunner, NoProgressTask } from "../../commandRunner"; import { CommandFunction, CommandManager } from "../../packages/commands"; +import { OutputChannelLogger } from "../logging"; /** * Create a command manager for VSCode, wrapping the commandRunner @@ -8,8 +9,10 @@ import { CommandFunction, CommandManager } from "../../packages/commands"; */ export function createVSCodeCommandManager< Commands extends Record, ->(): CommandManager { - return new CommandManager(commandRunner, wrapExecuteCommand); +>(outputLogger?: OutputChannelLogger): CommandManager { + return new CommandManager((commandId, task: NoProgressTask) => { + return commandRunner(commandId, task, outputLogger); + }, wrapExecuteCommand); } /** diff --git a/extensions/ql-vscode/src/common/vscode/vscode-app.ts b/extensions/ql-vscode/src/common/vscode/vscode-app.ts index 1ae588278..54032a8b1 100644 --- a/extensions/ql-vscode/src/common/vscode/vscode-app.ts +++ b/extensions/ql-vscode/src/common/vscode/vscode-app.ts @@ -3,21 +3,23 @@ import { VSCodeCredentials } from "../../authentication"; import { Disposable } from "../../pure/disposable-object"; import { App, AppMode } from "../app"; import { AppEventEmitter } from "../events"; -import { extLogger, Logger } from "../logging"; +import { extLogger, Logger, queryServerLogger } from "../logging"; import { Memento } from "../memento"; import { VSCodeAppEventEmitter } from "./events"; -import { AppCommandManager } from "../commands"; +import { AppCommandManager, QueryServerCommandManager } from "../commands"; import { createVSCodeCommandManager } from "./commands"; export class ExtensionApp implements App { public readonly credentials: VSCodeCredentials; public readonly commands: AppCommandManager; + public readonly queryServerCommands: QueryServerCommandManager; public constructor( public readonly extensionContext: vscode.ExtensionContext, ) { this.credentials = new VSCodeCredentials(); this.commands = createVSCodeCommandManager(); + this.queryServerCommands = createVSCodeCommandManager(queryServerLogger); extensionContext.subscriptions.push(this.commands); } diff --git a/extensions/ql-vscode/src/eval-log-viewer.ts b/extensions/ql-vscode/src/eval-log-viewer.ts index c2f4b129b..b71d6c22f 100644 --- a/extensions/ql-vscode/src/eval-log-viewer.ts +++ b/extensions/ql-vscode/src/eval-log-viewer.ts @@ -8,11 +8,11 @@ import { EventEmitter, TreeItemCollapsibleState, } from "vscode"; -import { commandRunner } from "./commandRunner"; import { DisposableObject } from "./pure/disposable-object"; import { showAndLogExceptionWithTelemetry } from "./helpers"; import { asError, getErrorMessage } from "./pure/helpers-pure"; import { redactableError } from "./pure/errors"; +import { EvalLogViewerCommands } from "./common/commands"; export interface EvalLogTreeItem { label?: string; @@ -80,11 +80,12 @@ export class EvalLogViewer extends DisposableObject { this.push(this.treeView); this.push(this.treeDataProvider); - this.push( - commandRunner("codeQLEvalLogViewer.clear", async () => { - this.clear(); - }), - ); + } + + public getCommands(): EvalLogViewerCommands { + return { + "codeQLEvalLogViewer.clear": async () => this.clear(), + }; } private clear(): void { diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index 67c52df34..481a3c90c 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -1,7 +1,6 @@ import "source-map-support/register"; import { CancellationToken, - CancellationTokenSource, commands, Disposable, env, @@ -9,8 +8,6 @@ import { extensions, languages, ProgressLocation, - QuickPickItem, - Range, Uri, version as vscodeVersion, window as Window, @@ -36,12 +33,11 @@ import { CliConfigListener, DistributionConfigListener, joinOrderWarningThreshold, - MAX_QUERIES, QueryHistoryConfigListener, QueryServerConfigListener, } from "./config"; import { install } from "./languageSupport"; -import { DatabaseItem, DatabaseManager } from "./local-databases"; +import { DatabaseManager } from "./local-databases"; import { DatabaseUI } from "./local-databases-ui"; import { TemplatePrintAstProvider, @@ -60,7 +56,6 @@ import { GithubRateLimitedError, } from "./distribution"; import { - findLanguage, showAndLogErrorMessage, showAndLogExceptionWithTelemetry, showAndLogInformationMessage, @@ -87,20 +82,17 @@ import { queryServerLogger, } from "./common"; import { QueryHistoryManager } from "./query-history/query-history-manager"; -import { CompletedLocalQueryInfo, LocalQueryInfo } from "./query-results"; +import { CompletedLocalQueryInfo } from "./query-results"; import { QueryServerClient as LegacyQueryServerClient } from "./legacy-query-server/queryserver-client"; import { QueryServerClient } from "./query-server/queryserver-client"; -import { displayQuickQuery } from "./quick-query"; import { QLTestAdapterFactory } from "./test-adapter"; import { TestUIService } from "./test-ui"; import { CompareView } from "./compare/compare-view"; -import { gatherQlFiles } from "./pure/files"; import { initializeTelemetry } from "./telemetry"; import { commandRunner, commandRunnerWithProgress, ProgressCallback, - ProgressUpdate, withProgress, } from "./commandRunner"; import { CodeQlStatusBarHandler } from "./status-bar"; @@ -114,7 +106,6 @@ import { EvalLogViewer } from "./eval-log-viewer"; import { SummaryLanguageSupport } from "./log-insights/summary-language-support"; import { JoinOrderScannerProvider } from "./log-insights/join-order"; import { LogScannerService } from "./log-insights/log-scanner-service"; -import { createInitialQueryInfo } from "./run-queries-shared"; import { LegacyQueryRunner } from "./legacy-query-server/legacyRunner"; import { NewQueryRunner } from "./query-server/query-runner"; import { QueryRunner } from "./queryRunner"; @@ -134,7 +125,16 @@ import { DbModule } from "./databases/db-module"; import { redactableError } from "./pure/errors"; import { QueryHistoryDirs } from "./query-history/query-history-dirs"; import { DirResult } from "tmp"; -import { AllCommands, BaseCommands } from "./common/commands"; +import { + AllCommands, + BaseCommands, + QueryServerCommands, +} from "./common/commands"; +import { + compileAndRunQuery, + getLocalQueryCommands, + showResultsForCompletedQuery, +} from "./local-queries"; /** * extension.ts @@ -244,10 +244,6 @@ interface DistributionUpdateConfig { allowAutoUpdating: boolean; } -interface DatabaseQuickPickItem extends QuickPickItem { - databaseItem: DatabaseItem; -} - const shouldUpdateOnNextActivationKey = "shouldUpdateOnNextActivation"; const codeQlVersionRange = DEFAULT_DISTRIBUTION_VERSION_RANGE; @@ -818,300 +814,6 @@ async function activateWithInstalledDistribution( } void extLogger.log("Registering top-level command palette commands."); - ctx.subscriptions.push( - commandRunnerWithProgress( - "codeQL.runQuery", - async ( - progress: ProgressCallback, - token: CancellationToken, - uri: Uri | undefined, - ) => - await compileAndRunQuery( - qs, - qhm, - databaseUI, - localQueryResultsView, - queryStorageDir, - false, - uri, - progress, - token, - undefined, - ), - { - title: "Running query", - cancellable: true, - }, - - // Open the query server logger on error since that's usually where the interesting errors appear. - queryServerLogger, - ), - ); - - // Since we are tracking extension usage through commands, this command mirrors the runQuery command - ctx.subscriptions.push( - commandRunnerWithProgress( - "codeQL.runQueryContextEditor", - async ( - progress: ProgressCallback, - token: CancellationToken, - uri: Uri | undefined, - ) => - await compileAndRunQuery( - qs, - qhm, - databaseUI, - localQueryResultsView, - queryStorageDir, - false, - uri, - progress, - token, - undefined, - ), - { - title: "Running query", - cancellable: true, - }, - - // Open the query server logger on error since that's usually where the interesting errors appear. - queryServerLogger, - ), - ); - ctx.subscriptions.push( - commandRunnerWithProgress( - "codeQL.runQueryOnMultipleDatabases", - async ( - progress: ProgressCallback, - token: CancellationToken, - uri: Uri | undefined, - ) => - await compileAndRunQueryOnMultipleDatabases( - cliServer, - qs, - qhm, - dbm, - databaseUI, - localQueryResultsView, - queryStorageDir, - progress, - token, - uri, - ), - { - title: "Running query on selected databases", - cancellable: true, - }, - ), - ); - // Since we are tracking extension usage through commands, this command mirrors the runQueryOnMultipleDatabases command - ctx.subscriptions.push( - commandRunnerWithProgress( - "codeQL.runQueryOnMultipleDatabasesContextEditor", - async ( - progress: ProgressCallback, - token: CancellationToken, - uri: Uri | undefined, - ) => - await compileAndRunQueryOnMultipleDatabases( - cliServer, - qs, - qhm, - dbm, - databaseUI, - localQueryResultsView, - queryStorageDir, - progress, - token, - uri, - ), - { - title: "Running query on selected databases", - cancellable: true, - }, - ), - ); - ctx.subscriptions.push( - commandRunnerWithProgress( - "codeQL.runQueries", - async ( - progress: ProgressCallback, - token: CancellationToken, - _: Uri | undefined, - multi: Uri[], - ) => { - const maxQueryCount = MAX_QUERIES.getValue() as number; - const [files, dirFound] = await gatherQlFiles( - multi.map((uri) => uri.fsPath), - ); - if (files.length > maxQueryCount) { - throw new Error( - `You tried to run ${files.length} queries, but the maximum is ${maxQueryCount}. Try selecting fewer queries or changing the 'codeQL.runningQueries.maxQueries' setting.`, - ); - } - // warn user and display selected files when a directory is selected because some ql - // files may be hidden from the user. - if (dirFound) { - const fileString = files.map((file) => basename(file)).join(", "); - const res = await showBinaryChoiceDialog( - `You are about to run ${files.length} queries: ${fileString} Do you want to continue?`, - ); - if (!res) { - return; - } - } - const queryUris = files.map((path) => Uri.parse(`file:${path}`, true)); - - // Use a wrapped progress so that messages appear with the queries remaining in it. - let queriesRemaining = queryUris.length; - function wrappedProgress(update: ProgressUpdate) { - const message = - queriesRemaining > 1 - ? `${queriesRemaining} remaining. ${update.message}` - : update.message; - progress({ - ...update, - message, - }); - } - - wrappedProgress({ - maxStep: queryUris.length, - step: queryUris.length - queriesRemaining, - message: "", - }); - - await Promise.all( - queryUris.map(async (uri) => - compileAndRunQuery( - qs, - qhm, - databaseUI, - localQueryResultsView, - queryStorageDir, - false, - uri, - wrappedProgress, - token, - undefined, - ).then(() => queriesRemaining--), - ), - ); - }, - { - title: "Running queries", - cancellable: true, - }, - - // Open the query server logger on error since that's usually where the interesting errors appear. - queryServerLogger, - ), - ); - - ctx.subscriptions.push( - commandRunnerWithProgress( - "codeQL.quickEval", - async ( - progress: ProgressCallback, - token: CancellationToken, - uri: Uri | undefined, - ) => - await compileAndRunQuery( - qs, - qhm, - databaseUI, - localQueryResultsView, - queryStorageDir, - true, - uri, - progress, - token, - undefined, - ), - { - title: "Running query", - cancellable: true, - }, - // Open the query server logger on error since that's usually where the interesting errors appear. - queryServerLogger, - ), - ); - - // Since we are tracking extension usage through commands, this command mirrors the "codeQL.quickEval" command - ctx.subscriptions.push( - commandRunnerWithProgress( - "codeQL.quickEvalContextEditor", - async ( - progress: ProgressCallback, - token: CancellationToken, - uri: Uri | undefined, - ) => - await compileAndRunQuery( - qs, - qhm, - databaseUI, - localQueryResultsView, - queryStorageDir, - true, - uri, - progress, - token, - undefined, - ), - { - title: "Running query", - cancellable: true, - }, - // Open the query server logger on error since that's usually where the interesting errors appear. - queryServerLogger, - ), - ); - - ctx.subscriptions.push( - commandRunnerWithProgress( - "codeQL.codeLensQuickEval", - async ( - progress: ProgressCallback, - token: CancellationToken, - uri: Uri, - range: Range, - ) => - await compileAndRunQuery( - qs, - qhm, - databaseUI, - localQueryResultsView, - queryStorageDir, - true, - uri, - progress, - token, - undefined, - range, - ), - { - title: "Running query", - cancellable: true, - }, - - // Open the query server logger on error since that's usually where the interesting errors appear. - queryServerLogger, - ), - ); - - ctx.subscriptions.push( - commandRunnerWithProgress( - "codeQL.quickQuery", - async (progress: ProgressCallback, token: CancellationToken) => - displayQuickQuery(ctx, cliServer, databaseUI, progress, token), - { - title: "Run Quick Query", - }, - - // Open the query server logger on error since that's usually where the interesting errors appear. - queryServerLogger, - ), - ); const allCommands: AllCommands = { ...getCommands(cliServer, qs), @@ -1119,12 +821,33 @@ async function activateWithInstalledDistribution( ...variantAnalysisManager.getCommands(), ...databaseUI.getCommands(), ...dbModule.getCommands(), + ...evalLogViewer.getCommands(), }; for (const [commandName, command] of Object.entries(allCommands)) { app.commands.register(commandName as keyof AllCommands, command); } + const queryServerCommands: QueryServerCommands = { + ...getLocalQueryCommands({ + app, + queryRunner: qs, + queryHistoryManager: qhm, + databaseManager: dbm, + cliServer, + databaseUI, + localQueryResultsView, + queryStorageDir, + }), + }; + + for (const [commandName, command] of Object.entries(queryServerCommands)) { + app.queryServerCommands.register( + commandName as keyof QueryServerCommands, + command, + ); + } + ctx.subscriptions.push( commandRunner( "codeQL.copyVariantAnalysisRepoList", @@ -1229,49 +952,6 @@ async function activateWithInstalledDistribution( }), ); - ctx.subscriptions.push( - commandRunnerWithProgress( - "codeQL.chooseDatabaseFolder", - (progress: ProgressCallback, token: CancellationToken) => - databaseUI.chooseDatabaseFolder(progress, token), - { - title: "Choose a Database from a Folder", - }, - ), - ); - ctx.subscriptions.push( - commandRunnerWithProgress( - "codeQL.chooseDatabaseArchive", - (progress: ProgressCallback, token: CancellationToken) => - databaseUI.chooseDatabaseArchive(progress, token), - { - title: "Choose a Database from an Archive", - }, - ), - ); - ctx.subscriptions.push( - commandRunnerWithProgress( - "codeQL.chooseDatabaseGithub", - async (progress: ProgressCallback, token: CancellationToken) => { - await databaseUI.chooseDatabaseGithub(progress, token); - }, - { - title: "Adding database from GitHub", - }, - ), - ); - ctx.subscriptions.push( - commandRunnerWithProgress( - "codeQL.chooseDatabaseInternet", - (progress: ProgressCallback, token: CancellationToken) => - databaseUI.chooseDatabaseInternet(progress, token), - - { - title: "Adding database from URL", - }, - ), - ); - ctx.subscriptions.push( commandRunner("codeQL.copyVersion", async () => { const text = `CodeQL extension version: ${ @@ -1609,160 +1289,6 @@ async function showResultsForComparison( } } -async function showResultsForCompletedQuery( - localQueryResultsView: ResultsView, - query: CompletedLocalQueryInfo, - forceReveal: WebviewReveal, -): Promise { - await localQueryResultsView.showResults(query, forceReveal, false); -} -async function compileAndRunQuery( - qs: QueryRunner, - qhm: QueryHistoryManager, - databaseUI: DatabaseUI, - localQueryResultsView: ResultsView, - queryStorageDir: string, - quickEval: boolean, - selectedQuery: Uri | undefined, - progress: ProgressCallback, - token: CancellationToken, - databaseItem: DatabaseItem | undefined, - range?: Range, -): Promise { - if (qs !== undefined) { - // If no databaseItem is specified, use the database currently selected in the Databases UI - databaseItem = - databaseItem || (await databaseUI.getDatabaseItem(progress, token)); - if (databaseItem === undefined) { - throw new Error("Can't run query without a selected database"); - } - const databaseInfo = { - name: databaseItem.name, - databaseUri: databaseItem.databaseUri.toString(), - }; - - // handle cancellation from the history view. - const source = new CancellationTokenSource(); - token.onCancellationRequested(() => source.cancel()); - - const initialInfo = await createInitialQueryInfo( - selectedQuery, - databaseInfo, - quickEval, - range, - ); - const item = new LocalQueryInfo(initialInfo, source); - qhm.addQuery(item); - try { - const completedQueryInfo = await qs.compileAndRunQueryAgainstDatabase( - databaseItem, - initialInfo, - queryStorageDir, - progress, - source.token, - undefined, - item, - ); - qhm.completeQuery(item, completedQueryInfo); - await showResultsForCompletedQuery( - localQueryResultsView, - item as CompletedLocalQueryInfo, - WebviewReveal.Forced, - ); - // Note we must update the query history view after showing results as the - // display and sorting might depend on the number of results - } catch (e) { - const err = asError(e); - err.message = `Error running query: ${err.message}`; - item.failureReason = err.message; - throw e; - } finally { - await qhm.refreshTreeView(); - source.dispose(); - } - } -} - -async function compileAndRunQueryOnMultipleDatabases( - cliServer: CodeQLCliServer, - qs: QueryRunner, - qhm: QueryHistoryManager, - dbm: DatabaseManager, - databaseUI: DatabaseUI, - localQueryResultsView: ResultsView, - queryStorageDir: string, - progress: ProgressCallback, - token: CancellationToken, - uri: Uri | undefined, -): Promise { - let filteredDBs = dbm.databaseItems; - if (filteredDBs.length === 0) { - void showAndLogErrorMessage( - "No databases found. Please add a suitable database to your workspace.", - ); - return; - } - // If possible, only show databases with the right language (otherwise show all databases). - const queryLanguage = await findLanguage(cliServer, uri); - if (queryLanguage) { - filteredDBs = dbm.databaseItems.filter( - (db) => db.language === queryLanguage, - ); - if (filteredDBs.length === 0) { - void showAndLogErrorMessage( - `No databases found for language ${queryLanguage}. Please add a suitable database to your workspace.`, - ); - return; - } - } - const quickPickItems = filteredDBs.map((dbItem) => ({ - databaseItem: dbItem, - label: dbItem.name, - description: dbItem.language, - })); - /** - * Databases that were selected in the quick pick menu. - */ - const quickpick = await window.showQuickPick( - quickPickItems, - { canPickMany: true, ignoreFocusOut: true }, - ); - if (quickpick !== undefined) { - // Collect all skipped databases and display them at the end (instead of popping up individual errors) - const skippedDatabases = []; - const errors = []; - for (const item of quickpick) { - try { - await compileAndRunQuery( - qs, - qhm, - databaseUI, - localQueryResultsView, - queryStorageDir, - false, - uri, - progress, - token, - item.databaseItem, - ); - } catch (e) { - skippedDatabases.push(item.label); - errors.push(getErrorMessage(e)); - } - } - if (skippedDatabases.length > 0) { - void extLogger.log(`Errors:\n${errors.join("\n")}`); - void showAndLogWarningMessage( - `The following databases were skipped:\n${skippedDatabases.join( - "\n", - )}.\nFor details about the errors, see the logs.`, - ); - } - } else { - void showAndLogErrorMessage("No databases selected."); - } -} - async function previewQueryHelp( cliServer: CodeQLCliServer, qhelpTmpDir: DirResult, diff --git a/extensions/ql-vscode/src/local-databases-ui.ts b/extensions/ql-vscode/src/local-databases-ui.ts index 95b98d4fe..c12081221 100644 --- a/extensions/ql-vscode/src/local-databases-ui.ts +++ b/extensions/ql-vscode/src/local-databases-ui.ts @@ -208,6 +208,13 @@ export class DatabaseUI extends DisposableObject { public getCommands(): LocalDatabasesCommands { return { + "codeQL.chooseDatabaseFolder": + this.handleChooseDatabaseFolderFromPalette.bind(this), + "codeQL.chooseDatabaseArchive": + this.handleChooseDatabaseArchiveFromPalette.bind(this), + "codeQL.chooseDatabaseInternet": + this.handleChooseDatabaseInternet.bind(this), + "codeQL.chooseDatabaseGithub": this.handleChooseDatabaseGithub.bind(this), "codeQL.setCurrentDatabase": this.handleSetCurrentDatabase.bind(this), "codeQL.setDefaultTourDatabase": this.handleSetDefaultTourDatabase.bind(this), @@ -242,7 +249,7 @@ export class DatabaseUI extends DisposableObject { await this.databaseManager.setCurrentDatabaseItem(databaseItem); } - public async chooseDatabaseFolder( + private async chooseDatabaseFolder( progress: ProgressCallback, token: CancellationToken, ): Promise { @@ -268,6 +275,17 @@ export class DatabaseUI extends DisposableObject { ); } + private async handleChooseDatabaseFolderFromPalette(): Promise { + return withProgress( + async (progress, token) => { + await this.chooseDatabaseFolder(progress, token); + }, + { + title: "Choose a Database from a Folder", + }, + ); + } + private async handleSetDefaultTourDatabase(): Promise { return withProgress( async (progress, token) => { @@ -392,7 +410,7 @@ export class DatabaseUI extends DisposableObject { } } - public async chooseDatabaseArchive( + private async chooseDatabaseArchive( progress: ProgressCallback, token: CancellationToken, ): Promise { @@ -418,23 +436,27 @@ export class DatabaseUI extends DisposableObject { ); } - public async chooseDatabaseInternet( - progress: ProgressCallback, - token: CancellationToken, - ): Promise { - return await promptImportInternetDatabase( - this.databaseManager, - this.storagePath, - progress, - token, - this.queryServer?.cliServer, + private async handleChooseDatabaseArchiveFromPalette(): Promise { + return withProgress( + async (progress, token) => { + await this.chooseDatabaseArchive(progress, token); + }, + { + title: "Choose a Database from an Archive", + }, ); } private async handleChooseDatabaseInternet(): Promise { return withProgress( async (progress, token) => { - await this.chooseDatabaseInternet(progress, token); + await promptImportInternetDatabase( + this.databaseManager, + this.storagePath, + progress, + token, + this.queryServer?.cliServer, + ); }, { title: "Adding database from URL", @@ -442,26 +464,19 @@ export class DatabaseUI extends DisposableObject { ); } - public async chooseDatabaseGithub( - progress: ProgressCallback, - token: CancellationToken, - ): Promise { - const credentials = isCanary() ? this.app.credentials : undefined; - - return await promptImportGithubDatabase( - this.databaseManager, - this.storagePath, - credentials, - progress, - token, - this.queryServer?.cliServer, - ); - } - private async handleChooseDatabaseGithub(): Promise { return withProgress( async (progress, token) => { - await this.chooseDatabaseGithub(progress, token); + const credentials = isCanary() ? this.app.credentials : undefined; + + await promptImportGithubDatabase( + this.databaseManager, + this.storagePath, + credentials, + progress, + token, + this.queryServer?.cliServer, + ); }, { title: "Adding database from GitHub", diff --git a/extensions/ql-vscode/src/local-queries.ts b/extensions/ql-vscode/src/local-queries.ts new file mode 100644 index 000000000..3c6b76b03 --- /dev/null +++ b/extensions/ql-vscode/src/local-queries.ts @@ -0,0 +1,394 @@ +import { + ProgressCallback, + ProgressUpdate, + withProgress, +} from "./commandRunner"; +import { + CancellationToken, + CancellationTokenSource, + QuickPickItem, + Range, + Uri, + window, +} from "vscode"; +import { extLogger } from "./common"; +import { MAX_QUERIES } from "./config"; +import { gatherQlFiles } from "./pure/files"; +import { basename } from "path"; +import { + findLanguage, + showAndLogErrorMessage, + showAndLogWarningMessage, + showBinaryChoiceDialog, +} from "./helpers"; +import { displayQuickQuery } from "./quick-query"; +import { QueryRunner } from "./queryRunner"; +import { QueryHistoryManager } from "./query-history/query-history-manager"; +import { DatabaseUI } from "./local-databases-ui"; +import { ResultsView } from "./interface"; +import { DatabaseItem, DatabaseManager } from "./local-databases"; +import { createInitialQueryInfo } from "./run-queries-shared"; +import { CompletedLocalQueryInfo, LocalQueryInfo } from "./query-results"; +import { WebviewReveal } from "./interface-utils"; +import { asError, getErrorMessage } from "./pure/helpers-pure"; +import { CodeQLCliServer } from "./cli"; +import { LocalQueryCommands } from "./common/commands"; +import { App } from "./common/app"; + +type LocalQueryOptions = { + app: App; + queryRunner: QueryRunner; + queryHistoryManager: QueryHistoryManager; + databaseManager: DatabaseManager; + cliServer: CodeQLCliServer; + databaseUI: DatabaseUI; + localQueryResultsView: ResultsView; + queryStorageDir: string; +}; + +export function getLocalQueryCommands({ + app, + queryRunner, + queryHistoryManager, + databaseManager, + cliServer, + databaseUI, + localQueryResultsView, + queryStorageDir, +}: LocalQueryOptions): LocalQueryCommands { + const runQuery = async (uri: Uri | undefined) => + withProgress( + async (progress, token) => { + await compileAndRunQuery( + queryRunner, + queryHistoryManager, + databaseUI, + localQueryResultsView, + queryStorageDir, + false, + uri, + progress, + token, + undefined, + ); + }, + { + title: "Running query", + cancellable: true, + }, + ); + + const runQueryOnMultipleDatabases = async (uri: Uri | undefined) => + withProgress( + async (progress, token) => + await compileAndRunQueryOnMultipleDatabases( + cliServer, + queryRunner, + queryHistoryManager, + databaseManager, + databaseUI, + localQueryResultsView, + queryStorageDir, + progress, + token, + uri, + ), + { + title: "Running query on selected databases", + cancellable: true, + }, + ); + + const runQueries = async (_: Uri | undefined, multi: Uri[]) => + withProgress( + async (progress, token) => { + const maxQueryCount = MAX_QUERIES.getValue() as number; + const [files, dirFound] = await gatherQlFiles( + multi.map((uri) => uri.fsPath), + ); + if (files.length > maxQueryCount) { + throw new Error( + `You tried to run ${files.length} queries, but the maximum is ${maxQueryCount}. Try selecting fewer queries or changing the 'codeQL.runningQueries.maxQueries' setting.`, + ); + } + // warn user and display selected files when a directory is selected because some ql + // files may be hidden from the user. + if (dirFound) { + const fileString = files.map((file) => basename(file)).join(", "); + const res = await showBinaryChoiceDialog( + `You are about to run ${files.length} queries: ${fileString} Do you want to continue?`, + ); + if (!res) { + return; + } + } + const queryUris = files.map((path) => Uri.parse(`file:${path}`, true)); + + // Use a wrapped progress so that messages appear with the queries remaining in it. + let queriesRemaining = queryUris.length; + + function wrappedProgress(update: ProgressUpdate) { + const message = + queriesRemaining > 1 + ? `${queriesRemaining} remaining. ${update.message}` + : update.message; + progress({ + ...update, + message, + }); + } + + wrappedProgress({ + maxStep: queryUris.length, + step: queryUris.length - queriesRemaining, + message: "", + }); + + await Promise.all( + queryUris.map(async (uri) => + compileAndRunQuery( + queryRunner, + queryHistoryManager, + databaseUI, + localQueryResultsView, + queryStorageDir, + false, + uri, + wrappedProgress, + token, + undefined, + ).then(() => queriesRemaining--), + ), + ); + }, + { + title: "Running queries", + cancellable: true, + }, + ); + + const quickEval = async (uri: Uri) => + withProgress( + async (progress, token) => { + await compileAndRunQuery( + queryRunner, + queryHistoryManager, + databaseUI, + localQueryResultsView, + queryStorageDir, + true, + uri, + progress, + token, + undefined, + ); + }, + { + title: "Running query", + cancellable: true, + }, + ); + + const codeLensQuickEval = async (uri: Uri, range: Range) => + withProgress( + async (progress, token) => + await compileAndRunQuery( + queryRunner, + queryHistoryManager, + databaseUI, + localQueryResultsView, + queryStorageDir, + true, + uri, + progress, + token, + undefined, + range, + ), + { + title: "Running query", + cancellable: true, + }, + ); + + const quickQuery = async () => + withProgress( + async (progress, token) => + displayQuickQuery(app, cliServer, databaseUI, progress, token), + { + title: "Run Quick Query", + }, + ); + + return { + "codeQL.runQuery": runQuery, + "codeQL.runQueryContextEditor": runQuery, + "codeQL.runQueryOnMultipleDatabases": runQueryOnMultipleDatabases, + "codeQL.runQueryOnMultipleDatabasesContextEditor": + runQueryOnMultipleDatabases, + "codeQL.runQueries": runQueries, + "codeQL.quickEval": quickEval, + "codeQL.quickEvalContextEditor": quickEval, + "codeQL.codeLensQuickEval": codeLensQuickEval, + "codeQL.quickQuery": quickQuery, + }; +} + +export async function compileAndRunQuery( + qs: QueryRunner, + qhm: QueryHistoryManager, + databaseUI: DatabaseUI, + localQueryResultsView: ResultsView, + queryStorageDir: string, + quickEval: boolean, + selectedQuery: Uri | undefined, + progress: ProgressCallback, + token: CancellationToken, + databaseItem: DatabaseItem | undefined, + range?: Range, +): Promise { + if (qs !== undefined) { + // If no databaseItem is specified, use the database currently selected in the Databases UI + databaseItem = + databaseItem || (await databaseUI.getDatabaseItem(progress, token)); + if (databaseItem === undefined) { + throw new Error("Can't run query without a selected database"); + } + const databaseInfo = { + name: databaseItem.name, + databaseUri: databaseItem.databaseUri.toString(), + }; + + // handle cancellation from the history view. + const source = new CancellationTokenSource(); + token.onCancellationRequested(() => source.cancel()); + + const initialInfo = await createInitialQueryInfo( + selectedQuery, + databaseInfo, + quickEval, + range, + ); + const item = new LocalQueryInfo(initialInfo, source); + qhm.addQuery(item); + try { + const completedQueryInfo = await qs.compileAndRunQueryAgainstDatabase( + databaseItem, + initialInfo, + queryStorageDir, + progress, + source.token, + undefined, + item, + ); + qhm.completeQuery(item, completedQueryInfo); + await showResultsForCompletedQuery( + localQueryResultsView, + item as CompletedLocalQueryInfo, + WebviewReveal.Forced, + ); + // Note we must update the query history view after showing results as the + // display and sorting might depend on the number of results + } catch (e) { + const err = asError(e); + err.message = `Error running query: ${err.message}`; + item.failureReason = err.message; + throw e; + } finally { + await qhm.refreshTreeView(); + source.dispose(); + } + } +} + +interface DatabaseQuickPickItem extends QuickPickItem { + databaseItem: DatabaseItem; +} + +async function compileAndRunQueryOnMultipleDatabases( + cliServer: CodeQLCliServer, + qs: QueryRunner, + qhm: QueryHistoryManager, + dbm: DatabaseManager, + databaseUI: DatabaseUI, + localQueryResultsView: ResultsView, + queryStorageDir: string, + progress: ProgressCallback, + token: CancellationToken, + uri: Uri | undefined, +): Promise { + let filteredDBs = dbm.databaseItems; + if (filteredDBs.length === 0) { + void showAndLogErrorMessage( + "No databases found. Please add a suitable database to your workspace.", + ); + return; + } + // If possible, only show databases with the right language (otherwise show all databases). + const queryLanguage = await findLanguage(cliServer, uri); + if (queryLanguage) { + filteredDBs = dbm.databaseItems.filter( + (db) => db.language === queryLanguage, + ); + if (filteredDBs.length === 0) { + void showAndLogErrorMessage( + `No databases found for language ${queryLanguage}. Please add a suitable database to your workspace.`, + ); + return; + } + } + const quickPickItems = filteredDBs.map((dbItem) => ({ + databaseItem: dbItem, + label: dbItem.name, + description: dbItem.language, + })); + /** + * Databases that were selected in the quick pick menu. + */ + const quickpick = await window.showQuickPick( + quickPickItems, + { canPickMany: true, ignoreFocusOut: true }, + ); + if (quickpick !== undefined) { + // Collect all skipped databases and display them at the end (instead of popping up individual errors) + const skippedDatabases = []; + const errors = []; + for (const item of quickpick) { + try { + await compileAndRunQuery( + qs, + qhm, + databaseUI, + localQueryResultsView, + queryStorageDir, + false, + uri, + progress, + token, + item.databaseItem, + ); + } catch (e) { + skippedDatabases.push(item.label); + errors.push(getErrorMessage(e)); + } + } + if (skippedDatabases.length > 0) { + void extLogger.log(`Errors:\n${errors.join("\n")}`); + void showAndLogWarningMessage( + `The following databases were skipped:\n${skippedDatabases.join( + "\n", + )}.\nFor details about the errors, see the logs.`, + ); + } + } else { + void showAndLogErrorMessage("No databases selected."); + } +} + +export async function showResultsForCompletedQuery( + localQueryResultsView: ResultsView, + query: CompletedLocalQueryInfo, + forceReveal: WebviewReveal, +): Promise { + await localQueryResultsView.showResults(query, forceReveal, false); +} diff --git a/extensions/ql-vscode/src/quick-query.ts b/extensions/ql-vscode/src/quick-query.ts index 4008bdda3..ca0e0ae65 100644 --- a/extensions/ql-vscode/src/quick-query.ts +++ b/extensions/ql-vscode/src/quick-query.ts @@ -1,13 +1,7 @@ import { ensureDir, writeFile, pathExists, readFile } from "fs-extra"; import { dump, load } from "js-yaml"; import { basename, join } from "path"; -import { - CancellationToken, - ExtensionContext, - window as Window, - workspace, - Uri, -} from "vscode"; +import { CancellationToken, window as Window, workspace, Uri } from "vscode"; import { LSPErrorCodes, ResponseError } from "vscode-languageclient"; import { CodeQLCliServer } from "./cli"; import { DatabaseUI } from "./local-databases-ui"; @@ -20,6 +14,7 @@ import { import { ProgressCallback, UserCancellationException } from "./commandRunner"; import { getErrorMessage } from "./pure/helpers-pure"; import { FALLBACK_QLPACK_FILENAME, getQlPackPath } from "./pure/ql"; +import { App } from "./common/app"; const QUICK_QUERIES_DIR_NAME = "quick-queries"; const QUICK_QUERY_QUERY_NAME = "quick-query.ql"; @@ -30,8 +25,8 @@ export function isQuickQueryPath(queryPath: string): boolean { return basename(queryPath) === QUICK_QUERY_QUERY_NAME; } -async function getQuickQueriesDir(ctx: ExtensionContext): Promise { - const storagePath = ctx.storagePath; +async function getQuickQueriesDir(app: App): Promise { + const storagePath = app.workspaceStoragePath; if (storagePath === undefined) { throw new Error("Workspace storage path is undefined"); } @@ -57,7 +52,7 @@ function findExistingQuickQueryEditor() { * Show a buffer the user can enter a simple query into. */ export async function displayQuickQuery( - ctx: ExtensionContext, + app: App, cliServer: CodeQLCliServer, databaseUI: DatabaseUI, progress: ProgressCallback, @@ -73,7 +68,7 @@ export async function displayQuickQuery( } const workspaceFolders = workspace.workspaceFolders || []; - const queriesDir = await getQuickQueriesDir(ctx); + const queriesDir = await getQuickQueriesDir(app); // We need to have a multi-root workspace to make quick query work // at all. Changing the workspace from single-root to multi-root