diff --git a/.vscode/launch.json b/.vscode/launch.json index c7fae2611..e25dd54ea 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,9 @@ "request": "launch", "runtimeExecutable": "${execPath}", "args": [ - "--extensionDevelopmentPath=${workspaceRoot}/extensions/ql-vscode" + "--extensionDevelopmentPath=${workspaceRoot}/extensions/ql-vscode", + // Add a reference to a workspace to open. Eg- + // "${workspaceRoot}/../vscode-codeql-starter/vscode-codeql-starter.code-workspace" ], "stopOnEntry": false, "sourceMaps": true, diff --git a/extensions/ql-vscode/src/astViewer.ts b/extensions/ql-vscode/src/astViewer.ts index ffa4c861e..6cc378a0f 100644 --- a/extensions/ql-vscode/src/astViewer.ts +++ b/extensions/ql-vscode/src/astViewer.ts @@ -18,7 +18,7 @@ import { DatabaseItem } from './databases'; import { UrlValue, BqrsId } from './pure/bqrs-cli-types'; import { showLocation } from './interface-utils'; import { isStringLoc, isWholeFileLoc, isLineColumnLoc } from './pure/bqrs-utils'; -import { commandRunner } from './helpers'; +import { commandRunner } from './commandRunner'; import { DisposableObject } from './vscode-utils/disposable-object'; export interface AstItem { diff --git a/extensions/ql-vscode/src/commandRunner.ts b/extensions/ql-vscode/src/commandRunner.ts new file mode 100644 index 000000000..5aff651c5 --- /dev/null +++ b/extensions/ql-vscode/src/commandRunner.ts @@ -0,0 +1,215 @@ +import { + CancellationToken, + ProgressOptions, + window as Window, + commands, + Disposable, + ProgressLocation +} from 'vscode'; +import { showAndLogErrorMessage, showAndLogWarningMessage } from './helpers'; +import { logger } from './logging'; + +export class UserCancellationException extends Error { + /** + * @param message The error message + * @param silent If silent is true, then this exception will avoid showing a warning message to the user. + */ + constructor(message?: string, public readonly silent = false) { + super(message); + } +} + +export interface ProgressUpdate { + /** + * The current step + */ + step: number; + /** + * The maximum step. This *should* be constant for a single job. + */ + maxStep: number; + /** + * The current progress message + */ + message: string; +} + +export type ProgressCallback = (p: ProgressUpdate) => void; + +/** + * A task that handles command invocations from `commandRunner` + * and includes a progress monitor. + * + * + * Arguments passed to the command handler are passed along, + * untouched to this `ProgressTask` instance. + * + * @param progress a progress handler function. Call this + * function with a `ProgressUpdate` instance in order to + * denote some progress being achieved on this task. + * @param token a cencellation token + * @param args arguments passed to this task passed on from + * `commands.registerCommand`. + */ +export type ProgressTask = ( + progress: ProgressCallback, + token: CancellationToken, + ...args: any[] +) => Thenable; + +/** + * A task that handles command invocations from `commandRunner`. + * Arguments passed to the command handler are passed along, + * untouched to this `NoProgressTask` instance. + * + * @param args arguments passed to this task passed on from + * `commands.registerCommand`. + */ +type NoProgressTask = ((...args: any[]) => Promise); + +/** + * This mediates between the kind of progress callbacks we want to + * write (where we *set* current progress position and give + * `maxSteps`) and the kind vscode progress api expects us to write + * (which increment progress by a certain amount out of 100%). + * + * Where possible, the `commandRunner` function below should be used + * instead of this function. The commandRunner is meant for wrapping + * top-level commands and provides error handling and other support + * automatically. + * + * Only use this function if you need a progress monitor and the + * control flow does not always come from a command (eg- during + * extension activation, or from an internal language server + * request). + */ +export function withProgress( + options: ProgressOptions, + task: ProgressTask, + ...args: any[] +): Thenable { + let progressAchieved = 0; + return Window.withProgress(options, + (progress, token) => { + return task(p => { + const { message, step, maxStep } = p; + const increment = 100 * (step - progressAchieved) / maxStep; + progressAchieved = step; + progress.report({ message, increment }); + }, token, ...args); + }); +} + +/** + * A generic wrapper for command registration. This wrapper adds uniform error handling for commands. + * + * In this variant of the command runner, no progress monitor is used. + * + * @param commandId The ID of the command to register. + * @param task The task to run. It is passed directly to `commands.registerCommand`. Any + * arguments to the command handler are passed on to the task. + */ +export function commandRunner( + commandId: string, + task: NoProgressTask, +): Disposable { + return commands.registerCommand(commandId, async (...args: any[]) => { + try { + return await task(...args); + } catch (e) { + const errorMessage = `${e.message || e} (${commandId})`; + if (e instanceof UserCancellationException) { + // User has cancelled this action manually + if (e.silent) { + logger.log(errorMessage); + } else { + showAndLogWarningMessage(errorMessage); + } + } else { + showAndLogErrorMessage(errorMessage); + } + return undefined; + } + }); +} + +/** + * A generic wrapper for command registration. This wrapper adds uniform error handling, + * progress monitoring, and cancellation for commands. + * + * @param commandId The ID of the command to register. + * @param task The task to run. It is passed directly to `commands.registerCommand`. Any + * arguments to the command handler are passed on to the task after the progress callback + * and cancellation token. + * @param progressOptions Progress options to be sent to the progress monitor. + */ +export function commandRunnerWithProgress( + commandId: string, + task: ProgressTask, + progressOptions: Partial +): Disposable { + return commands.registerCommand(commandId, async (...args: any[]) => { + const progressOptionsWithDefaults = { + location: ProgressLocation.Notification, + ...progressOptions + }; + try { + return await withProgress(progressOptionsWithDefaults, task, ...args); + } catch (e) { + const errorMessage = `${e.message || e} (${commandId})`; + if (e instanceof UserCancellationException) { + // User has cancelled this action manually + if (e.silent) { + logger.log(errorMessage); + } else { + showAndLogWarningMessage(errorMessage); + } + } else { + showAndLogErrorMessage(errorMessage); + } + return undefined; + } + }); +} + +/** + * Displays a progress monitor that indicates how much progess has been made + * reading from a stream. + * + * @param readable The stream to read progress from + * @param messagePrefix A prefix for displaying the message + * @param totalNumBytes Total number of bytes in this stream + * @param progress The progress callback used to set messages + */ +export function reportStreamProgress( + readable: NodeJS.ReadableStream, + messagePrefix: string, + totalNumBytes?: number, + progress?: ProgressCallback +) { + if (progress && totalNumBytes) { + let numBytesDownloaded = 0; + const bytesToDisplayMB = (numBytes: number): string => `${(numBytes / (1024 * 1024)).toFixed(1)} MB`; + const updateProgress = () => { + progress({ + step: numBytesDownloaded, + maxStep: totalNumBytes, + message: `${messagePrefix} [${bytesToDisplayMB(numBytesDownloaded)} of ${bytesToDisplayMB(totalNumBytes)}]`, + }); + }; + + // Display the progress straight away rather than waiting for the first chunk. + updateProgress(); + + readable.on('data', data => { + numBytesDownloaded += data.length; + updateProgress(); + }); + } else if (progress) { + progress({ + step: 1, + maxStep: 2, + message: `${messagePrefix} (Size unknown)`, + }); + } +} diff --git a/extensions/ql-vscode/src/contextual/locationFinder.ts b/extensions/ql-vscode/src/contextual/locationFinder.ts index 3c6067f65..1c6e95c45 100644 --- a/extensions/ql-vscode/src/contextual/locationFinder.ts +++ b/extensions/ql-vscode/src/contextual/locationFinder.ts @@ -8,7 +8,7 @@ import fileRangeFromURI from './fileRangeFromURI'; import * as messages from '../pure/messages'; import { QueryServerClient } from '../queryserver-client'; import { QueryWithResults, compileAndRunQueryAgainstDatabase } from '../run-queries'; -import { ProgressCallback } from '../helpers'; +import { ProgressCallback } from '../commandRunner'; import { KeyType } from './keyType'; import { qlpackOfDatabase, resolveQueries } from './queryResolver'; diff --git a/extensions/ql-vscode/src/contextual/templateProvider.ts b/extensions/ql-vscode/src/contextual/templateProvider.ts index 3428207b3..09a538ee7 100644 --- a/extensions/ql-vscode/src/contextual/templateProvider.ts +++ b/extensions/ql-vscode/src/contextual/templateProvider.ts @@ -3,7 +3,8 @@ import * as vscode from 'vscode'; import { decodeSourceArchiveUri, encodeArchiveBasePath, zipArchiveScheme } from '../archive-filesystem-provider'; import { CodeQLCliServer } from '../cli'; import { DatabaseManager } from '../databases'; -import { CachedOperation, ProgressCallback, withProgress } from '../helpers'; +import { CachedOperation } from '../helpers'; +import { ProgressCallback, withProgress } from '../commandRunner'; import * as messages from '../pure/messages'; import { QueryServerClient } from '../queryserver-client'; import { compileAndRunQueryAgainstDatabase, QueryWithResults } from '../run-queries'; diff --git a/extensions/ql-vscode/src/databaseFetcher.ts b/extensions/ql-vscode/src/databaseFetcher.ts index 7075bb449..49d8ff8d2 100644 --- a/extensions/ql-vscode/src/databaseFetcher.ts +++ b/extensions/ql-vscode/src/databaseFetcher.ts @@ -12,10 +12,12 @@ import * as path from 'path'; import { DatabaseManager, DatabaseItem } from './databases'; import { - reportStreamProgress, - ProgressCallback, showAndLogInformationMessage, } from './helpers'; +import { + reportStreamProgress, + ProgressCallback, +} from './commandRunner'; import { logger } from './logging'; import { tmpDir } from './run-queries'; diff --git a/extensions/ql-vscode/src/databases-ui.ts b/extensions/ql-vscode/src/databases-ui.ts index efda3f1ac..bbf872723 100644 --- a/extensions/ql-vscode/src/databases-ui.ts +++ b/extensions/ql-vscode/src/databases-ui.ts @@ -22,8 +22,10 @@ import { import { commandRunner, commandRunnerWithProgress, - getOnDiskWorkspaceFolders, ProgressCallback, +} from './commandRunner'; +import { + getOnDiskWorkspaceFolders, showAndLogErrorMessage, isLikelyDatabaseRoot, isLikelyDbLanguageFolder diff --git a/extensions/ql-vscode/src/databases.ts b/extensions/ql-vscode/src/databases.ts index cfac30b12..7d300d014 100644 --- a/extensions/ql-vscode/src/databases.ts +++ b/extensions/ql-vscode/src/databases.ts @@ -9,9 +9,11 @@ import { showAndLogWarningMessage, showAndLogInformationMessage, isLikelyDatabaseRoot, +} from './helpers'; +import { ProgressCallback, withProgress -} from './helpers'; +} from './commandRunner'; import { zipArchiveScheme, encodeArchiveBasePath, decodeSourceArchiveUri, encodeSourceArchiveUri } from './archive-filesystem-provider'; import { DisposableObject } from './vscode-utils/disposable-object'; import { Logger, logger } from './logging'; diff --git a/extensions/ql-vscode/src/distribution.ts b/extensions/ql-vscode/src/distribution.ts index 2fa27f977..2a41fa549 100644 --- a/extensions/ql-vscode/src/distribution.ts +++ b/extensions/ql-vscode/src/distribution.ts @@ -7,10 +7,15 @@ import * as unzipper from 'unzipper'; import * as url from 'url'; import { ExtensionContext, Event } from 'vscode'; import { DistributionConfig } from './config'; -import { InvocationRateLimiter, InvocationRateLimiterResultKind, showAndLogErrorMessage } from './helpers'; +import { + InvocationRateLimiter, + InvocationRateLimiterResultKind, + showAndLogErrorMessage, + showAndLogWarningMessage +} from './helpers'; import { logger } from './logging'; -import * as helpers from './helpers'; import { getCodeQlCliVersion } from './cli-version'; +import { ProgressCallback, reportStreamProgress } from './commandRunner'; /** * distribution.ts @@ -221,7 +226,7 @@ export class DistributionManager implements DistributionProvider { * Returns a failed promise if an unexpected error occurs during installation. */ public installExtensionManagedDistributionRelease(release: Release, - progressCallback?: helpers.ProgressCallback): Promise { + progressCallback?: ProgressCallback): Promise { return this.extensionSpecificDistributionManager!.installDistributionRelease(release, progressCallback); } @@ -303,14 +308,14 @@ class ExtensionSpecificDistributionManager { * Returns a failed promise if an unexpected error occurs during installation. */ public async installDistributionRelease(release: Release, - progressCallback?: helpers.ProgressCallback): Promise { + progressCallback?: ProgressCallback): Promise { await this.downloadDistribution(release, progressCallback); // Store the installed release within the global extension state. this.storeInstalledRelease(release); } private async downloadDistribution(release: Release, - progressCallback?: helpers.ProgressCallback): Promise { + progressCallback?: ProgressCallback): Promise { try { await this.removeDistribution(); } catch (e) { @@ -338,7 +343,7 @@ class ExtensionSpecificDistributionManager { const contentLength = assetStream.headers.get('content-length'); const totalNumBytes = contentLength ? parseInt(contentLength, 10) : undefined; - helpers.reportStreamProgress(assetStream.body, 'Downloading CodeQL CLI…', totalNumBytes, progressCallback); + reportStreamProgress(assetStream.body, 'Downloading CodeQL CLI…', totalNumBytes, progressCallback); await new Promise((resolve, reject) => assetStream.body.pipe(archiveFile) @@ -707,7 +712,9 @@ export async function getExecutableFromDirectory(directory: string, warnWhenNotF } function warnDeprecatedLauncher() { - helpers.showAndLogWarningMessage( + + showAndLogWarningMessage( + `The "${deprecatedCodeQlLauncherName()!}" launcher has been deprecated and will be removed in a future version. ` + `Please use "${codeQlLauncherName()}" instead. It is recommended to update to the latest CodeQL binaries.` ); diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index a2b4db462..f93bfd465 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -45,6 +45,7 @@ import { GithubRateLimitedError } from './distribution'; import * as helpers from './helpers'; +import { commandRunner, commandRunnerWithProgress, ProgressCallback, ProgressUpdate, withProgress } from './commandRunner'; import { assertNever } from './pure/helpers-pure'; import { spawnIdeServer } from './ide-server'; import { InterfaceManager } from './interface'; @@ -108,7 +109,7 @@ function registerErrorStubs(excludedCommands: string[], stubGenerator: (command: stubbedCommands.forEach(command => { if (excludedCommands.indexOf(command) === -1) { - errorStubs.push(helpers.commandRunner(command, stubGenerator(command))); + errorStubs.push(commandRunner(command, stubGenerator(command))); } }); } @@ -194,7 +195,7 @@ export async function activate(ctx: ExtensionContext): Promise + await withProgress(progressOptions, progress => distributionManager.installExtensionManagedDistributionRelease(result.updatedRelease, progress)); await ctx.globalState.update(shouldUpdateOnNextActivationKey, false); @@ -308,7 +309,7 @@ export async function activate(ctx: ExtensionContext): Promise installOrUpdateThenTryActivate({ + ctx.subscriptions.push(commandRunner(checkForUpdatesCommand, () => installOrUpdateThenTryActivate({ isUserInitiated: true, shouldDisplayMessageWhenNoUpdates: true, allowAutoUpdating: true @@ -430,7 +431,7 @@ async function activateWithInstalledDistribution( async function compileAndRunQuery( quickEval: boolean, selectedQuery: Uri | undefined, - progress: helpers.ProgressCallback, + progress: ProgressCallback, token: CancellationToken, ): Promise { if (qs !== undefined) { @@ -491,10 +492,10 @@ async function activateWithInstalledDistribution( logger.log('Registering top-level command palette commands.'); ctx.subscriptions.push( - helpers.commandRunnerWithProgress( + commandRunnerWithProgress( 'codeQL.runQuery', async ( - progress: helpers.ProgressCallback, + progress: ProgressCallback, token: CancellationToken, uri: Uri | undefined ) => await compileAndRunQuery(false, uri, progress, token), @@ -505,10 +506,10 @@ async function activateWithInstalledDistribution( ) ); ctx.subscriptions.push( - helpers.commandRunnerWithProgress( + commandRunnerWithProgress( 'codeQL.runQueries', async ( - progress: helpers.ProgressCallback, + progress: ProgressCallback, token: CancellationToken, _: Uri | undefined, multi: Uri[] @@ -533,7 +534,7 @@ async function activateWithInstalledDistribution( // Use a wrapped progress so that messages appear with the queries remaining in it. let queriesRemaining = queryUris.length; - function wrappedProgress(update: helpers.ProgressUpdate) { + function wrappedProgress(update: ProgressUpdate) { const message = queriesRemaining > 1 ? `${queriesRemaining} remaining. ${update.message}` : update.message; @@ -569,10 +570,10 @@ async function activateWithInstalledDistribution( }) ); ctx.subscriptions.push( - helpers.commandRunnerWithProgress( + commandRunnerWithProgress( 'codeQL.quickEval', async ( - progress: helpers.ProgressCallback, + progress: ProgressCallback, token: CancellationToken, uri: Uri | undefined ) => await compileAndRunQuery(true, uri, progress, token), @@ -582,8 +583,8 @@ async function activateWithInstalledDistribution( }) ); ctx.subscriptions.push( - helpers.commandRunnerWithProgress('codeQL.quickQuery', async ( - progress: helpers.ProgressCallback, + commandRunnerWithProgress('codeQL.quickQuery', async ( + progress: ProgressCallback, token: CancellationToken ) => displayQuickQuery(ctx, cliServer, databaseUI, progress, token), @@ -594,7 +595,7 @@ async function activateWithInstalledDistribution( ); ctx.subscriptions.push( - helpers.commandRunner('codeQL.restartQueryServer', async () => { + commandRunner('codeQL.restartQueryServer', async () => { await qs.restartQueryServer(); helpers.showAndLogInformationMessage('CodeQL Query Server restarted.', { outputLogger: queryServerLogger, @@ -602,24 +603,24 @@ async function activateWithInstalledDistribution( }) ); ctx.subscriptions.push( - helpers.commandRunner('codeQL.chooseDatabaseFolder', ( - progress: helpers.ProgressCallback, + commandRunner('codeQL.chooseDatabaseFolder', ( + progress: ProgressCallback, token: CancellationToken ) => databaseUI.handleChooseDatabaseFolder(progress, token) ) ); ctx.subscriptions.push( - helpers.commandRunner('codeQL.chooseDatabaseArchive', ( - progress: helpers.ProgressCallback, + commandRunner('codeQL.chooseDatabaseArchive', ( + progress: ProgressCallback, token: CancellationToken ) => databaseUI.handleChooseDatabaseArchive(progress, token) ) ); ctx.subscriptions.push( - helpers.commandRunnerWithProgress('codeQL.chooseDatabaseLgtm', ( - progress: helpers.ProgressCallback, + commandRunnerWithProgress('codeQL.chooseDatabaseLgtm', ( + progress: ProgressCallback, token: CancellationToken ) => databaseUI.handleChooseDatabaseLgtm(progress, token), @@ -628,8 +629,8 @@ async function activateWithInstalledDistribution( }) ); ctx.subscriptions.push( - helpers.commandRunnerWithProgress('codeQL.chooseDatabaseInternet', ( - progress: helpers.ProgressCallback, + commandRunnerWithProgress('codeQL.chooseDatabaseInternet', ( + progress: ProgressCallback, token: CancellationToken ) => databaseUI.handleChooseDatabaseInternet(progress, token), @@ -656,8 +657,8 @@ async function activateWithInstalledDistribution( const astViewer = new AstViewer(); ctx.subscriptions.push(astViewer); - ctx.subscriptions.push(helpers.commandRunnerWithProgress('codeQL.viewAst', async ( - progress: helpers.ProgressCallback, + ctx.subscriptions.push(commandRunnerWithProgress('codeQL.viewAst', async ( + progress: ProgressCallback, token: CancellationToken ) => { const ast = await new TemplatePrintAstProvider(cliServer, qs, dbm, progress, token) diff --git a/extensions/ql-vscode/src/helpers.ts b/extensions/ql-vscode/src/helpers.ts index ecfea422a..71dbf432e 100644 --- a/extensions/ql-vscode/src/helpers.ts +++ b/extensions/ql-vscode/src/helpers.ts @@ -3,181 +3,13 @@ import * as glob from 'glob-promise'; import * as yaml from 'js-yaml'; import * as path from 'path'; import { - CancellationToken, ExtensionContext, - ProgressOptions, window as Window, - workspace, - commands, - Disposable, - ProgressLocation + workspace } from 'vscode'; import { CodeQLCliServer } from './cli'; import { logger } from './logging'; -export class UserCancellationException extends Error { - /** - * @param message The error message - * @param silent If silent is true, then this exception will avoid showing a warning message to the user. - */ - constructor(message?: string, public readonly silent = false) { - super(message); - } -} - -export interface ProgressUpdate { - /** - * The current step - */ - step: number; - /** - * The maximum step. This *should* be constant for a single job. - */ - maxStep: number; - /** - * The current progress message - */ - message: string; -} - -export type ProgressCallback = (p: ProgressUpdate) => void; - -/** - * A task that handles command invocations from `commandRunner` - * and includes a progress monitor. - * - * - * Arguments passed to the command handler are passed along, - * untouched to this `ProgressTask` instance. - * - * @param progress a progress handler function. Call this - * function with a `ProgressUpdate` instance in order to - * denote some progress being achieved on this task. - * @param token a cencellation token - * @param args arguments passed to this task passed on from - * `commands.registerCommand`. - */ -export type ProgressTask = ( - progress: ProgressCallback, - token: CancellationToken, - ...args: any[] -) => Thenable; - -/** - * A task that handles command invocations from `commandRunner`. - * Arguments passed to the command handler are passed along, - * untouched to this `NoProgressTask` instance. - * - * @param args arguments passed to this task passed on from - * `commands.registerCommand`. - */ -type NoProgressTask = ((...args: any[]) => Promise); - -/** - * This mediates between the kind of progress callbacks we want to - * write (where we *set* current progress position and give - * `maxSteps`) and the kind vscode progress api expects us to write - * (which increment progress by a certain amount out of 100%). - * - * Where possible, the `commandRunner` function below should be used - * instead of this function. The commandRunner is meant for wrapping - * top-level commands and provides error handling and other support - * automatically. - * - * Only use this function if you need a progress monitor and the - * control flow does not always come from a command (eg- during - * extension activation, or from an internal language server - * request). - */ -export function withProgress( - options: ProgressOptions, - task: ProgressTask, - ...args: any[] -): Thenable { - let progressAchieved = 0; - return Window.withProgress(options, - (progress, token) => { - return task(p => { - const { message, step, maxStep } = p; - const increment = 100 * (step - progressAchieved) / maxStep; - progressAchieved = step; - progress.report({ message, increment }); - }, token, ...args); - }); -} - -/** - * A generic wrapper for command registration. This wrapper adds uniform error handling for commands. - * - * In this variant of the command runner, no progress monitor is used. - * - * @param commandId The ID of the command to register. - * @param task The task to run. It is passed directly to `commands.registerCommand`. Any - * arguments to the command handler are passed on to the task. - */ -export function commandRunner( - commandId: string, - task: NoProgressTask, -): Disposable { - return commands.registerCommand(commandId, async (...args: any[]) => { - try { - return await task(...args); - } catch (e) { - const errorMessage = `${e.message || e} (${commandId})`; - if (e instanceof UserCancellationException) { - // User has cancelled this action manually - if (e.silent) { - logger.log(errorMessage); - } else { - showAndLogWarningMessage(errorMessage); - } - } else { - showAndLogErrorMessage(errorMessage); - } - return undefined; - } - }); -} - -/** - * A generic wrapper for command registration. This wrapper adds uniform error handling, - * progress monitoring, and cancellation for commands. - * - * @param commandId The ID of the command to register. - * @param task The task to run. It is passed directly to `commands.registerCommand`. Any - * arguments to the command handler are passed on to the task after the progress callback - * and cancellation token. - * @param progressOptions Progress options to be sent to the progress monitor. - */ -export function commandRunnerWithProgress( - commandId: string, - task: ProgressTask, - progressOptions: Partial -): Disposable { - return commands.registerCommand(commandId, async (...args: any[]) => { - const progressOptionsWithDefaults = { - location: ProgressLocation.Notification, - ...progressOptions - }; - try { - return await withProgress(progressOptionsWithDefaults, task, ...args); - } catch (e) { - const errorMessage = `${e.message || e} (${commandId})`; - if (e instanceof UserCancellationException) { - // User has cancelled this action manually - if (e.silent) { - logger.log(errorMessage); - } else { - showAndLogWarningMessage(errorMessage); - } - } else { - showAndLogErrorMessage(errorMessage); - } - return undefined; - } - }); -} - /** * Show an error message and log it to the console * @@ -532,46 +364,3 @@ export async function isLikelyDatabaseRoot(maybeRoot: string) { export function isLikelyDbLanguageFolder(dbPath: string) { return !!path.basename(dbPath).startsWith('db-'); } - - -/** - * Displays a progress monitor that indicates how much progess has been made - * reading from a stream. - * - * @param readable The stream to read progress from - * @param messagePrefix A prefix for displaying the message - * @param totalNumBytes Total number of bytes in this stream - * @param progress The progress callback used to set messages - */ -export function reportStreamProgress( - readable: NodeJS.ReadableStream, - messagePrefix: string, - totalNumBytes?: number, - progress?: ProgressCallback -) { - if (progress && totalNumBytes) { - let numBytesDownloaded = 0; - const bytesToDisplayMB = (numBytes: number): string => `${(numBytes / (1024 * 1024)).toFixed(1)} MB`; - const updateProgress = () => { - progress({ - step: numBytesDownloaded, - maxStep: totalNumBytes, - message: `${messagePrefix} [${bytesToDisplayMB(numBytesDownloaded)} of ${bytesToDisplayMB(totalNumBytes)}]`, - }); - }; - - // Display the progress straight away rather than waiting for the first chunk. - updateProgress(); - - readable.on('data', data => { - numBytesDownloaded += data.length; - updateProgress(); - }); - } else if (progress) { - progress({ - step: 1, - maxStep: 2, - message: `${messagePrefix} (Size unknown)`, - }); - } -} diff --git a/extensions/ql-vscode/src/interface.ts b/extensions/ql-vscode/src/interface.ts index 58e40a747..169135c8b 100644 --- a/extensions/ql-vscode/src/interface.ts +++ b/extensions/ql-vscode/src/interface.ts @@ -30,7 +30,7 @@ import { RawResultsSortState, } from './pure/interface-types'; import { Logger } from './logging'; -import { commandRunner } from './helpers'; +import { commandRunner } from './commandRunner'; import * as messages from './pure/messages'; import { CompletedQuery, interpretResults } from './query-results'; import { QueryInfo, tmpDir } from './run-queries'; diff --git a/extensions/ql-vscode/src/query-history.ts b/extensions/ql-vscode/src/query-history.ts index ffe044a35..79044b98c 100644 --- a/extensions/ql-vscode/src/query-history.ts +++ b/extensions/ql-vscode/src/query-history.ts @@ -4,7 +4,15 @@ import { window as Window } from 'vscode'; import { CompletedQuery } from './query-results'; import { QueryHistoryConfig } from './config'; import { QueryWithResults } from './run-queries'; -import * as helpers from './helpers'; +import { + showAndLogErrorMessage, + showAndLogInformationMessage, + showAndLogWarningMessage, + showBinaryChoiceDialog +} from './helpers'; +import { + commandRunner +} from './commandRunner'; import { logger } from './logging'; import { URLSearchParams } from 'url'; import { QueryServerClient } from './queryserver-client'; @@ -207,55 +215,55 @@ export class QueryHistoryManager extends DisposableObject { logger.log('Registering query history panel commands.'); this.push( - helpers.commandRunner( + commandRunner( 'codeQLQueryHistory.openQuery', this.handleOpenQuery.bind(this) ) ); this.push( - helpers.commandRunner( + commandRunner( 'codeQLQueryHistory.removeHistoryItem', this.handleRemoveHistoryItem.bind(this) ) ); this.push( - helpers.commandRunner( + commandRunner( 'codeQLQueryHistory.setLabel', this.handleSetLabel.bind(this) ) ); this.push( - helpers.commandRunner( + commandRunner( 'codeQLQueryHistory.compareWith', this.handleCompareWith.bind(this) ) ); this.push( - helpers.commandRunner( + commandRunner( 'codeQLQueryHistory.showQueryLog', this.handleShowQueryLog.bind(this) ) ); this.push( - helpers.commandRunner( + commandRunner( 'codeQLQueryHistory.showQueryText', this.handleShowQueryText.bind(this) ) ); this.push( - helpers.commandRunner( + commandRunner( 'codeQLQueryHistory.viewSarif', this.handleViewSarif.bind(this) ) ); this.push( - helpers.commandRunner( + commandRunner( 'codeQLQueryHistory.viewDil', this.handleViewDil.bind(this) ) ); this.push( - helpers.commandRunner( + commandRunner( 'codeQLQueryHistory.itemClicked', async (item: CompletedQuery) => { return this.handleItemClicked(item, [item]); @@ -376,7 +384,7 @@ export class QueryHistoryManager extends DisposableObject { this.doCompareCallback(from, to); } } catch (e) { - helpers.showAndLogErrorMessage(e.message); + showAndLogErrorMessage(e.message); } } @@ -423,7 +431,7 @@ export class QueryHistoryManager extends DisposableObject { if (singleItem.logFileLocation) { await this.tryOpenExternalFile(singleItem.logFileLocation); } else { - helpers.showAndLogWarningMessage('No log file available'); + showAndLogWarningMessage('No log file available'); } } @@ -468,7 +476,7 @@ export class QueryHistoryManager extends DisposableObject { ); } else { const label = singleItem.getLabel(); - helpers.showAndLogInformationMessage( + showAndLogInformationMessage( `Query ${label} has no interpreted results.` ); } @@ -547,7 +555,7 @@ export class QueryHistoryManager extends DisposableObject { ) || e.message.includes('too large to open') ) { - const res = await helpers.showBinaryChoiceDialog( + const res = await showBinaryChoiceDialog( `VS Code does not allow extensions to open files >50MB. This file exceeds that limit. Do you want to open it outside of VS Code? @@ -558,11 +566,11 @@ the file in the file explorer and dragging it into the workspace.` try { await vscode.commands.executeCommand('revealFileInOS', uri); } catch (e) { - helpers.showAndLogErrorMessage(e.message); + showAndLogErrorMessage(e.message); } } } else { - helpers.showAndLogErrorMessage(`Could not open file ${fileLocation}`); + showAndLogErrorMessage(`Could not open file ${fileLocation}`); logger.log(e.message); logger.log(e.stack); } @@ -616,7 +624,7 @@ the file in the file explorer and dragging it into the workspace.` private assertSingleQuery(multiSelect: CompletedQuery[] = [], message = 'Please select a single query.') { if (multiSelect.length > 1) { - helpers.showAndLogErrorMessage( + showAndLogErrorMessage( message ); return false; diff --git a/extensions/ql-vscode/src/quick-query.ts b/extensions/ql-vscode/src/quick-query.ts index af9932da5..62510144c 100644 --- a/extensions/ql-vscode/src/quick-query.ts +++ b/extensions/ql-vscode/src/quick-query.ts @@ -10,11 +10,13 @@ import { getInitialQueryContents, getPrimaryDbscheme, getQlPackForDbscheme, - ProgressCallback, showAndLogErrorMessage, showBinaryChoiceDialog, - UserCancellationException } from './helpers'; +import { + ProgressCallback, + UserCancellationException +} from './commandRunner'; const QUICK_QUERIES_DIR_NAME = 'quick-queries'; const QUICK_QUERY_QUERY_NAME = 'quick-query.ql'; diff --git a/extensions/ql-vscode/src/run-queries.ts b/extensions/ql-vscode/src/run-queries.ts index 5541903ab..a9a1460c7 100644 --- a/extensions/ql-vscode/src/run-queries.ts +++ b/extensions/ql-vscode/src/run-queries.ts @@ -15,7 +15,8 @@ import { ErrorCodes, ResponseError } from 'vscode-languageclient'; import * as cli from './cli'; import * as config from './config'; import { DatabaseItem, getUpgradesDirectories } from './databases'; -import * as helpers from './helpers'; +import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from './helpers'; +import { ProgressCallback, UserCancellationException } from './commandRunner'; import { DatabaseInfo, QueryMetadata, ResultsPaths } from './pure/interface-types'; import { logger } from './logging'; import * as messages from './pure/messages'; @@ -79,7 +80,7 @@ export class QueryInfo { async run( qs: qsClient.QueryServerClient, - progress: helpers.ProgressCallback, + progress: ProgressCallback, token: CancellationToken, ): Promise { let result: messages.EvaluationResult | null = null; @@ -121,7 +122,7 @@ export class QueryInfo { async compile( qs: qsClient.QueryServerClient, - progress: helpers.ProgressCallback, + progress: ProgressCallback, token: CancellationToken, ): Promise { let compiled: messages.CheckQueryResult | undefined; @@ -209,7 +210,7 @@ export interface QueryWithResults { export async function clearCacheInDatabase( qs: qsClient.QueryServerClient, dbItem: DatabaseItem, - progress: helpers.ProgressCallback, + progress: ProgressCallback, token: CancellationToken, ): Promise { if (dbItem.contents === undefined) { @@ -285,10 +286,10 @@ async function checkDbschemeCompatibility( cliServer: cli.CodeQLCliServer, qs: qsClient.QueryServerClient, query: QueryInfo, - progress: helpers.ProgressCallback, + progress: ProgressCallback, token: CancellationToken, ): Promise { - const searchPath = helpers.getOnDiskWorkspaceFolders(); + const searchPath = getOnDiskWorkspaceFolders(); if (query.dbItem.contents !== undefined && query.dbItem.contents.dbSchemeUri !== undefined) { const { scripts, finalDbscheme } = await cliServer.resolveUpgrades(query.dbItem.contents.dbSchemeUri.fsPath, searchPath); @@ -364,7 +365,7 @@ async function promptUserToSaveChanges(document: TextDocument): Promise } if (chosenItem === cancelItem) { - throw new helpers.UserCancellationException('Query run cancelled.', true); + throw new UserCancellationException('Query run cancelled.', true); } } } @@ -454,7 +455,7 @@ export async function compileAndRunQueryAgainstDatabase( db: DatabaseItem, quickEval: boolean, selectedQueryUri: Uri | undefined, - progress: helpers.ProgressCallback, + progress: ProgressCallback, token: CancellationToken, templates?: messages.TemplateDefinitions, ): Promise { @@ -474,7 +475,7 @@ export async function compileAndRunQueryAgainstDatabase( } // Get the workspace folder paths. - const diskWorkspaceFolders = helpers.getOnDiskWorkspaceFolders(); + const diskWorkspaceFolders = getOnDiskWorkspaceFolders(); // Figure out the library path for the query. const packConfig = await cliServer.resolveLibraryPath(diskWorkspaceFolders, queryPath); @@ -533,7 +534,7 @@ export async function compileAndRunQueryAgainstDatabase( if (result.resultType !== messages.QueryResultType.SUCCESS) { const message = result.message || 'Failed to run query'; logger.log(message); - helpers.showAndLogErrorMessage(message); + showAndLogErrorMessage(message); } return { query, @@ -564,9 +565,9 @@ export async function compileAndRunQueryAgainstDatabase( qs.logger.log(formatted); } if (quickEval && formattedMessages.length <= 3) { - helpers.showAndLogErrorMessage('Quick evaluation compilation failed: \n' + formattedMessages.join('\n')); + showAndLogErrorMessage('Quick evaluation compilation failed: \n' + formattedMessages.join('\n')); } else { - helpers.showAndLogErrorMessage((quickEval ? 'Quick evaluation' : 'Query') + + showAndLogErrorMessage((quickEval ? 'Quick evaluation' : 'Query') + ' compilation failed. Please make sure there are no errors in the query, the database is up to date,' + ' and the query and database use the same target language. For more details on the error, go to View > Output,' + ' and choose CodeQL Query Server from the dropdown.'); diff --git a/extensions/ql-vscode/src/upgrades.ts b/extensions/ql-vscode/src/upgrades.ts index a7590e2be..2b2400e59 100644 --- a/extensions/ql-vscode/src/upgrades.ts +++ b/extensions/ql-vscode/src/upgrades.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import { DatabaseItem } from './databases'; -import * as helpers from './helpers'; +import { showAndLogErrorMessage } from './helpers'; +import { ProgressCallback, UserCancellationException } from './commandRunner'; import { logger } from './logging'; import * as messages from './pure/messages'; import * as qsClient from './queryserver-client'; @@ -24,7 +25,7 @@ async function checkAndConfirmDatabaseUpgrade( db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[], - progress: helpers.ProgressCallback, + progress: ProgressCallback, token: vscode.CancellationToken, ): Promise { if (db.contents === undefined || db.contents.dbSchemeUri === undefined) { @@ -75,7 +76,7 @@ async function checkAndConfirmDatabaseUpgrade( if (curSha != targetSha) { // Newlines aren't rendered in notifications: https://github.com/microsoft/vscode/issues/48900 // A modal dialog would be rendered better, but is more intrusive. - await helpers.showAndLogErrorMessage(`Database cannot be upgraded to the target database scheme. + await showAndLogErrorMessage(`Database cannot be upgraded to the target database scheme. Can upgrade from ${checkedUpgrades.initialSha} (current) to ${curSha}, but cannot reach ${targetSha} (target).`); // TODO: give a more informative message if we think the DB is ahead of the target DB scheme return; @@ -114,7 +115,7 @@ async function checkAndConfirmDatabaseUpgrade( return params; } else { - throw new helpers.UserCancellationException('User cancelled the database upgrade.'); + throw new UserCancellationException('User cancelled the database upgrade.'); } } @@ -128,7 +129,7 @@ export async function upgradeDatabase( qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[], - progress: helpers.ProgressCallback, + progress: ProgressCallback, token: vscode.CancellationToken, ): Promise { const upgradeParams = await checkAndConfirmDatabaseUpgrade(qs, db, targetDbScheme, upgradesDirectories, progress, token); @@ -142,7 +143,7 @@ export async function upgradeDatabase( compileUpgradeResult = await compileDatabaseUpgrade(qs, upgradeParams, progress, token); } catch (e) { - helpers.showAndLogErrorMessage(`Compilation of database upgrades failed: ${e}`); + showAndLogErrorMessage(`Compilation of database upgrades failed: ${e}`); return; } finally { @@ -151,7 +152,7 @@ export async function upgradeDatabase( if (compileUpgradeResult.compiledUpgrades === undefined) { const error = compileUpgradeResult.error || '[no error message available]'; - helpers.showAndLogErrorMessage(`Compilation of database upgrades failed: ${error}`); + (`Compilation of database upgrades failed: ${error}`); return; } @@ -168,7 +169,7 @@ export async function upgradeDatabase( return await runDatabaseUpgrade(qs, db, compileUpgradeResult.compiledUpgrades, progress, token); } catch (e) { - helpers.showAndLogErrorMessage(`Database upgrade failed: ${e}`); + showAndLogErrorMessage(`Database upgrade failed: ${e}`); return; } finally { @@ -179,7 +180,7 @@ export async function upgradeDatabase( async function checkDatabaseUpgrade( qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams, - progress: helpers.ProgressCallback, + progress: ProgressCallback, token: vscode.CancellationToken, ): Promise { progress({ @@ -194,7 +195,7 @@ async function checkDatabaseUpgrade( async function compileDatabaseUpgrade( qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams, - progress: helpers.ProgressCallback, + progress: ProgressCallback, token: vscode.CancellationToken, ): Promise { const params: messages.CompileUpgradeParams = { @@ -216,7 +217,7 @@ async function runDatabaseUpgrade( qs: qsClient.QueryServerClient, db: DatabaseItem, upgrades: messages.CompiledUpgrades, - progress: helpers.ProgressCallback, + progress: ProgressCallback, token: vscode.CancellationToken, ): Promise { diff --git a/extensions/ql-vscode/src/vscode-tests/cli-integration/databases.test.ts b/extensions/ql-vscode/src/vscode-tests/cli-integration/databases.test.ts index 8aa888bdc..c8b4d4f0f 100644 --- a/extensions/ql-vscode/src/vscode-tests/cli-integration/databases.test.ts +++ b/extensions/ql-vscode/src/vscode-tests/cli-integration/databases.test.ts @@ -7,7 +7,7 @@ import { extensions, CancellationToken, Uri, window } from 'vscode'; import { CodeQLExtensionInterface } from '../../extension'; import { DatabaseManager } from '../../databases'; import { promptImportLgtmDatabase, importArchiveDatabase, promptImportInternetDatabase } from '../../databaseFetcher'; -import { ProgressCallback } from '../../helpers'; +import { ProgressCallback } from '../../commandRunner'; import { dbLoc, DB_URL, storagePath } from './global.helper'; /** diff --git a/extensions/ql-vscode/src/vscode-tests/minimal-workspace/databases.test.ts b/extensions/ql-vscode/src/vscode-tests/minimal-workspace/databases.test.ts index da289816b..9545ba14d 100644 --- a/extensions/ql-vscode/src/vscode-tests/minimal-workspace/databases.test.ts +++ b/extensions/ql-vscode/src/vscode-tests/minimal-workspace/databases.test.ts @@ -17,7 +17,7 @@ import { import { Logger } from '../../logging'; import { QueryServerClient } from '../../queryserver-client'; import { registerDatabases } from '../../pure/messages'; -import { ProgressCallback } from '../../helpers'; +import { ProgressCallback } from '../../commandRunner'; import { CodeQLCliServer } from '../../cli'; import { encodeArchiveBasePath, encodeSourceArchiveUri } from '../../archive-filesystem-provider'; diff --git a/extensions/ql-vscode/src/vscode-tests/no-workspace/helpers.test.ts b/extensions/ql-vscode/src/vscode-tests/no-workspace/helpers.test.ts index 9432fea43..0deb03320 100644 --- a/extensions/ql-vscode/src/vscode-tests/no-workspace/helpers.test.ts +++ b/extensions/ql-vscode/src/vscode-tests/no-workspace/helpers.test.ts @@ -7,7 +7,8 @@ import * as path from 'path'; import * as fs from 'fs-extra'; import * as sinon from 'sinon'; -import { getInitialQueryContents, InvocationRateLimiter, isLikelyDbLanguageFolder, reportStreamProgress } from '../../helpers'; +import { getInitialQueryContents, InvocationRateLimiter, isLikelyDbLanguageFolder } from '../../helpers'; +import { reportStreamProgress } from '../../commandRunner'; describe('helpers', () => { let sandbox: sinon.SinonSandbox; diff --git a/extensions/ql-vscode/src/vscode-utils/ui-service.ts b/extensions/ql-vscode/src/vscode-utils/ui-service.ts index bacace8c2..91ef4232e 100644 --- a/extensions/ql-vscode/src/vscode-utils/ui-service.ts +++ b/extensions/ql-vscode/src/vscode-utils/ui-service.ts @@ -1,6 +1,6 @@ import { TreeDataProvider, window } from 'vscode'; import { DisposableObject } from './disposable-object'; -import { commandRunner } from '../helpers'; +import { commandRunner } from '../commandRunner'; /** * A VS Code service that interacts with the UI, including handling commands.