Merge remote-tracking branch 'origin/main' into koesie10/packaging-typed-commands

This commit is contained in:
Koen Vlaswinkel
2023-03-22 12:39:36 +01:00
11 changed files with 740 additions and 587 deletions

View File

@@ -89,7 +89,7 @@ export type ProgressTaskWithArgs<R> = (
* @param args arguments passed to this task passed on from
* `commands.registerCommand`.
*/
type NoProgressTask = (...args: any[]) => Promise<any>;
export type NoProgressTask = (...args: any[]) => Promise<any>;
/**
* This mediates between the kind of progress callbacks we want to

View File

@@ -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";
@@ -27,6 +27,23 @@ export type SingleSelectionCommandFunction<Item> = (
// Base commands not tied directly to a module like e.g. variant analysis.
export type BaseCommands = {
"codeQL.openDocumentation": () => Promise<void>;
"codeQL.restartQueryServer": () => Promise<void>;
};
// Commands used for running local queries
export type LocalQueryCommands = {
"codeQL.runQuery": (uri?: Uri) => Promise<void>;
"codeQL.runQueryContextEditor": (uri?: Uri) => Promise<void>;
"codeQL.runQueryOnMultipleDatabases": (uri?: Uri) => Promise<void>;
"codeQL.runQueryOnMultipleDatabasesContextEditor": (
uri?: Uri,
) => Promise<void>;
"codeQL.runQueries": SelectionCommandFunction<Uri>;
"codeQL.quickEval": (uri: Uri) => Promise<void>;
"codeQL.quickEvalContextEditor": (uri: Uri) => Promise<void>;
"codeQL.codeLensQuickEval": (uri: Uri, range: Range) => Promise<void>;
"codeQL.quickQuery": () => Promise<void>;
};
// Commands used for the query history panel
@@ -63,27 +80,42 @@ export type QueryHistoryCommands = {
// Commands used for the local databases panel
export type LocalDatabasesCommands = {
"codeQL.setCurrentDatabase": (uri: Uri) => Promise<void>;
"codeQL.setDefaultTourDatabase": () => Promise<void>;
// Command palette commands
"codeQL.chooseDatabaseFolder": () => Promise<void>;
"codeQL.chooseDatabaseArchive": () => Promise<void>;
"codeQL.chooseDatabaseInternet": () => Promise<void>;
"codeQL.chooseDatabaseGithub": () => Promise<void>;
"codeQL.upgradeCurrentDatabase": () => Promise<void>;
"codeQL.clearCache": () => Promise<void>;
// Explorer context menu
"codeQL.setCurrentDatabase": (uri: Uri) => Promise<void>;
// Database panel view title commands
"codeQLDatabases.chooseDatabaseFolder": () => Promise<void>;
"codeQLDatabases.chooseDatabaseArchive": () => Promise<void>;
"codeQLDatabases.chooseDatabaseInternet": () => Promise<void>;
"codeQLDatabases.chooseDatabaseGithub": () => Promise<void>;
"codeQLDatabases.sortByName": () => Promise<void>;
"codeQLDatabases.sortByDateAdded": () => Promise<void>;
// Database panel context menu
"codeQLDatabases.setCurrentDatabase": (
databaseItem: DatabaseItem,
) => Promise<void>;
"codeQLDatabases.sortByName": () => Promise<void>;
"codeQLDatabases.sortByDateAdded": () => Promise<void>;
"codeQLDatabases.removeOrphanedDatabases": () => Promise<void>;
// Database panel selection commands
"codeQLDatabases.removeDatabase": SelectionCommandFunction<DatabaseItem>;
"codeQLDatabases.upgradeDatabase": SelectionCommandFunction<DatabaseItem>;
"codeQLDatabases.renameDatabase": SelectionCommandFunction<DatabaseItem>;
"codeQLDatabases.openDatabaseFolder": SelectionCommandFunction<DatabaseItem>;
"codeQLDatabases.addDatabaseSource": SelectionCommandFunction<DatabaseItem>;
// Codespace template commands
"codeQL.setDefaultTourDatabase": () => Promise<void>;
// Internal commands
"codeQLDatabases.removeOrphanedDatabases": () => Promise<void>;
};
// Commands tied to variant analysis
@@ -113,11 +145,20 @@ export type PackagingCommands = {
"codeQL.downloadPacks": () => Promise<void>;
};
export type EvalLogViewerCommands = {
"codeQLEvalLogViewer.clear": () => Promise<void>;
};
export type AllCommands = BaseCommands &
QueryHistoryCommands &
LocalDatabasesCommands &
VariantAnalysisCommands &
DatabasePanelCommands &
PackagingCommands;
PackagingCommands &
EvalLogViewerCommands;
export type AppCommandManager = CommandManager<AllCommands>;
// Separate command manager because it uses a different logger
export type QueryServerCommands = LocalQueryCommands;
export type QueryServerCommandManager = CommandManager<QueryServerCommands>;

View File

@@ -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<string, CommandFunction>,
>(): CommandManager<Commands> {
return new CommandManager(commandRunner, wrapExecuteCommand);
>(outputLogger?: OutputChannelLogger): CommandManager<Commands> {
return new CommandManager((commandId, task: NoProgressTask) => {
return commandRunner(commandId, task, outputLogger);
}, wrapExecuteCommand);
}
/**

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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,
@@ -69,6 +64,7 @@ import {
showInformationMessageWithAction,
tmpDir,
tmpDirDisposal,
prepareCodeTour,
} from "./helpers";
import {
asError,
@@ -86,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";
@@ -110,7 +103,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";
@@ -130,7 +122,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
@@ -165,11 +166,28 @@ const extension = extensions.getExtension(extensionId);
/**
* Return all commands that are not tied to the more specific managers.
*/
function getCommands(): BaseCommands {
function getCommands(
cliServer: CodeQLCliServer,
queryRunner: QueryRunner,
): BaseCommands {
return {
"codeQL.openDocumentation": async () => {
await env.openExternal(Uri.parse("https://codeql.github.com/docs/"));
},
"codeQL.restartQueryServer": async () =>
withProgress(
async (progress: ProgressCallback, token: CancellationToken) => {
// We restart the CLI server too, to ensure they are the same version
cliServer.restartCliServer();
await queryRunner.restartQueryServer(progress, token);
void showAndLogInformationMessage("CodeQL Query Server restarted.", {
outputLogger: queryServerLogger,
});
},
{
title: "Restarting Query Server",
},
),
};
}
@@ -223,10 +241,6 @@ interface DistributionUpdateConfig {
allowAutoUpdating: boolean;
}
interface DatabaseQuickPickItem extends QuickPickItem {
databaseItem: DatabaseItem;
}
const shouldUpdateOnNextActivationKey = "shouldUpdateOnNextActivation";
const codeQlVersionRange = DEFAULT_DISTRIBUTION_VERSION_RANGE;
@@ -524,6 +538,14 @@ async function installOrUpdateThenTryActivate(
): Promise<CodeQLExtensionInterface | Record<string, never>> {
await installOrUpdateDistribution(ctx, distributionManager, config);
try {
await prepareCodeTour();
} catch (e: unknown) {
void extLogger.log(
`Could not open tutorial workspace automatically: ${getErrorMessage(e)}`,
);
}
// Display the warnings even if the extension has already activated.
const distributionResult =
await getDistributionDisplayingDistributionWarnings(distributionManager);
@@ -789,303 +811,9 @@ 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(),
...getCommands(cliServer, qs),
...qhm.getCommands(),
...variantAnalysisManager.getCommands(),
...databaseUI.getCommands(),
@@ -1093,12 +821,33 @@ async function activateWithInstalledDistribution(
...getPackagingCommands({
cliServer,
}),
...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",
@@ -1203,66 +952,6 @@ async function activateWithInstalledDistribution(
}),
);
ctx.subscriptions.push(
commandRunnerWithProgress(
"codeQL.restartQueryServer",
async (progress: ProgressCallback, token: CancellationToken) => {
// We restart the CLI server too, to ensure they are the same version
cliServer.restartCliServer();
await qs.restartQueryServer(progress, token);
void showAndLogInformationMessage("CodeQL Query Server restarted.", {
outputLogger: queryServerLogger,
});
},
{
title: "Restarting Query Server",
},
),
);
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: ${
@@ -1578,160 +1267,6 @@ async function showResultsForComparison(
}
}
async function showResultsForCompletedQuery(
localQueryResultsView: ResultsView,
query: CompletedLocalQueryInfo,
forceReveal: WebviewReveal,
): Promise<void> {
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<void> {
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<void> {
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<DatabaseQuickPickItem>((dbItem) => ({
databaseItem: dbItem,
label: dbItem.name,
description: dbItem.language,
}));
/**
* Databases that were selected in the quick pick menu.
*/
const quickpick = await window.showQuickPick<DatabaseQuickPickItem>(
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,

View File

@@ -16,6 +16,7 @@ import {
window as Window,
workspace,
env,
commands,
} from "vscode";
import { CodeQLCliServer, QlpacksInfo } from "./cli";
import { UserCancellationException } from "./commandRunner";
@@ -25,6 +26,7 @@ import { telemetryListener } from "./telemetry";
import { RedactableError } from "./pure/errors";
import { getQlPackPath } from "./pure/ql";
import { dbSchemeToLanguage } from "./common/query-language";
import { isCodespacesTemplate } from "./config";
// Shared temporary folder for the extension.
export const tmpDir = dirSync({
@@ -266,6 +268,51 @@ export function isFolderAlreadyInWorkspace(folderName: string) {
);
}
/** Check if the current workspace is the CodeTour and open the workspace folder.
* Without this, we can't run the code tour correctly.
**/
export async function prepareCodeTour(): Promise<void> {
if (workspace.workspaceFolders?.length) {
const currentFolder = workspace.workspaceFolders[0].uri.fsPath;
const tutorialWorkspacePath = join(
currentFolder,
"tutorial.code-workspace",
);
const toursFolderPath = join(currentFolder, ".tours");
/** We're opening the tutorial workspace, if we detect it.
* This will only happen if the following three conditions are met:
* - the .tours folder exists
* - the tutorial.code-workspace file exists
* - the CODESPACES_TEMPLATE setting doesn't exist (it's only set if the user has already opened
* the tutorial workspace so it's a good indicator that the user is in the folder but has ignored
* the prompt to open the workspace)
*/
if (
(await pathExists(tutorialWorkspacePath)) &&
(await pathExists(toursFolderPath)) &&
!isCodespacesTemplate()
) {
const answer = await showBinaryChoiceDialog(
"We've detected you're in the CodeQL Tour repo. We will need to open the workspace file to continue. Reload?",
);
if (!answer) {
return;
}
const tutorialWorkspaceUri = Uri.parse(tutorialWorkspacePath);
void extLogger.log(
`In prepareCodeTour() method, going to open the tutorial workspace file: ${tutorialWorkspacePath}`,
);
await commands.executeCommand("vscode.openFolder", tutorialWorkspaceUri);
}
}
}
/**
* Provides a utility method to invoke a function only if a minimum time interval has elapsed since
* the last invocation of that function.

View File

@@ -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<void> {
@@ -268,6 +275,17 @@ export class DatabaseUI extends DisposableObject {
);
}
private async handleChooseDatabaseFolderFromPalette(): Promise<void> {
return withProgress(
async (progress, token) => {
await this.chooseDatabaseFolder(progress, token);
},
{
title: "Choose a Database from a Folder",
},
);
}
private async handleSetDefaultTourDatabase(): Promise<void> {
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<void> {
@@ -418,23 +436,27 @@ export class DatabaseUI extends DisposableObject {
);
}
public async chooseDatabaseInternet(
progress: ProgressCallback,
token: CancellationToken,
): Promise<DatabaseItem | undefined> {
return await promptImportInternetDatabase(
this.databaseManager,
this.storagePath,
progress,
token,
this.queryServer?.cliServer,
private async handleChooseDatabaseArchiveFromPalette(): Promise<void> {
return withProgress(
async (progress, token) => {
await this.chooseDatabaseArchive(progress, token);
},
{
title: "Choose a Database from an Archive",
},
);
}
private async handleChooseDatabaseInternet(): Promise<void> {
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<DatabaseItem | undefined> {
const credentials = isCanary() ? this.app.credentials : undefined;
return await promptImportGithubDatabase(
this.databaseManager,
this.storagePath,
credentials,
progress,
token,
this.queryServer?.cliServer,
);
}
private async handleChooseDatabaseGithub(): Promise<void> {
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",

View File

@@ -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<void> {
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<void> {
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<DatabaseQuickPickItem>((dbItem) => ({
databaseItem: dbItem,
label: dbItem.name,
description: dbItem.language,
}));
/**
* Databases that were selected in the quick pick menu.
*/
const quickpick = await window.showQuickPick<DatabaseQuickPickItem>(
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<void> {
await localQueryResultsView.showResults(query, forceReveal, false);
}

View File

@@ -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<string> {
const storagePath = ctx.storagePath;
async function getQuickQueriesDir(app: App): Promise<string> {
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

View File

@@ -1,4 +1,5 @@
import {
commands,
EnvironmentVariableCollection,
EnvironmentVariableMutator,
Event,
@@ -15,7 +16,14 @@ import {
import { dump } from "js-yaml";
import * as tmp from "tmp";
import { join } from "path";
import { writeFileSync, mkdirSync, ensureDirSync, symlinkSync } from "fs-extra";
import {
writeFileSync,
mkdirSync,
ensureDirSync,
symlinkSync,
writeFile,
mkdir,
} from "fs-extra";
import { DirResult } from "tmp";
import {
@@ -24,6 +32,7 @@ import {
isFolderAlreadyInWorkspace,
isLikelyDatabaseRoot,
isLikelyDbLanguageFolder,
prepareCodeTour,
showBinaryChoiceDialog,
showBinaryChoiceWithUrlDialog,
showInformationMessageWithAction,
@@ -31,6 +40,7 @@ import {
} from "../../../src/helpers";
import { reportStreamProgress } from "../../../src/commandRunner";
import { QueryLanguage } from "../../../src/common/query-language";
import { Setting } from "../../../src/config";
describe("helpers", () => {
describe("Invocation rate limiter", () => {
@@ -559,3 +569,113 @@ describe("isFolderAlreadyInWorkspace", () => {
expect(isFolderAlreadyInWorkspace("/third/path")).toBe(false);
});
});
describe("prepareCodeTour", () => {
let dir: tmp.DirResult;
let showInformationMessageSpy: jest.SpiedFunction<
typeof window.showInformationMessage
>;
beforeEach(() => {
dir = tmp.dirSync();
const mockWorkspaceFolders = [
{
uri: Uri.file(dir.name),
name: "test",
index: 0,
},
] as WorkspaceFolder[];
jest
.spyOn(workspace, "workspaceFolders", "get")
.mockReturnValue(mockWorkspaceFolders);
showInformationMessageSpy = jest
.spyOn(window, "showInformationMessage")
.mockResolvedValue({ title: "Yes" });
});
afterEach(() => {
dir.removeCallback();
});
describe("if we're in the tour repo", () => {
describe("if the workspace is not already open", () => {
it("should open the tutorial workspace", async () => {
// set up directory to have a 'tutorial.code-workspace' file
const tutorialWorkspacePath = join(dir.name, "tutorial.code-workspace");
await writeFile(tutorialWorkspacePath, "{}");
// set up a .tours directory to indicate we're in the tour codespace
const tourDirPath = join(dir.name, ".tours");
await mkdir(tourDirPath);
// spy that we open the workspace file by calling the 'vscode.openFolder' command
const commandSpy = jest.spyOn(commands, "executeCommand");
commandSpy.mockImplementation(() => Promise.resolve());
await prepareCodeTour();
expect(showInformationMessageSpy).toHaveBeenCalled();
expect(commandSpy).toHaveBeenCalledWith(
"vscode.openFolder",
expect.objectContaining({
path: Uri.parse(tutorialWorkspacePath).fsPath,
}),
);
});
});
describe("if the workspace is already open", () => {
it("should not open the tutorial workspace", async () => {
// Set isCodespaceTemplate to true to indicate the workspace has already been opened
jest.spyOn(Setting.prototype, "getValue").mockReturnValue(true);
// set up directory to have a 'tutorial.code-workspace' file
const tutorialWorkspacePath = join(dir.name, "tutorial.code-workspace");
await writeFile(tutorialWorkspacePath, "{}");
// set up a .tours directory to indicate we're in the tour codespace
const tourDirPath = join(dir.name, ".tours");
await mkdir(tourDirPath);
// spy that we open the workspace file by calling the 'vscode.openFolder' command
const commandSpy = jest.spyOn(commands, "executeCommand");
commandSpy.mockImplementation(() => Promise.resolve());
await prepareCodeTour();
expect(commandSpy).not.toHaveBeenCalled();
});
});
});
describe("if we're in a different tour repo", () => {
it("should not open the tutorial workspace", async () => {
// set up a .tours directory
const tourDirPath = join(dir.name, ".tours");
await mkdir(tourDirPath);
// spy that we open the workspace file by calling the 'vscode.openFolder' command
const commandSpy = jest.spyOn(commands, "executeCommand");
commandSpy.mockImplementation(() => Promise.resolve());
await prepareCodeTour();
expect(commandSpy).not.toHaveBeenCalled();
});
});
describe("if we're in a different repo with no tour", () => {
it("should not open the tutorial workspace", async () => {
// spy that we open the workspace file by calling the 'vscode.openFolder' command
const commandSpy = jest.spyOn(commands, "executeCommand");
commandSpy.mockImplementation(() => Promise.resolve());
await prepareCodeTour();
expect(commandSpy).not.toHaveBeenCalled();
});
});
});