QuickEval
This commit is contained in:
@@ -494,6 +494,14 @@
|
||||
"command": "codeQL.getCurrentQuery",
|
||||
"title": "CodeQL: Get Current Query"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.debug.quickEval",
|
||||
"title": "CodeQL Debugger: Quick Evaluation"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.debug.quickEvalContextEditor",
|
||||
"title": "CodeQL Debugger: Quick Evaluation"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.viewAst",
|
||||
"title": "CodeQL: View AST"
|
||||
@@ -1092,6 +1100,14 @@
|
||||
"command": "codeQL.quickEvalContextEditor",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.debug.quickEval",
|
||||
"when": "config.codeQL.canary && editorLangId == ql"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.debug.quickEvalContextEditor",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.openReferencedFile",
|
||||
"when": "resourceExtname == .qlref"
|
||||
@@ -1394,6 +1410,10 @@
|
||||
"command": "codeQL.quickEvalContextEditor",
|
||||
"when": "editorLangId == ql"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.debug.quickEvalContextEditor",
|
||||
"when": "config.codeQL.canary && editorLangId == ql"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.openReferencedFileContextEditor",
|
||||
"when": "resourceExtname == .qlref"
|
||||
|
||||
@@ -96,6 +96,13 @@ export type LocalQueryCommands = {
|
||||
"codeQL.quickEvalContextEditor": (uri: Uri) => Promise<void>;
|
||||
"codeQL.codeLensQuickEval": (uri: Uri, range: Range) => Promise<void>;
|
||||
"codeQL.quickQuery": () => Promise<void>;
|
||||
"codeQL.getCurrentQuery": () => Promise<string>;
|
||||
};
|
||||
|
||||
// Debugger commands
|
||||
export type DebuggerCommands = {
|
||||
"codeQL.debug.quickEval": (uri: Uri) => Promise<void>;
|
||||
"codeQL.debug.quickEvalContextEditor": (uri: Uri) => Promise<void>;
|
||||
};
|
||||
|
||||
export type ResultsViewCommands = {
|
||||
@@ -269,6 +276,7 @@ export type AllExtensionCommands = BaseCommands &
|
||||
ResultsViewCommands &
|
||||
QueryHistoryCommands &
|
||||
LocalDatabasesCommands &
|
||||
DebuggerCommands &
|
||||
VariantAnalysisCommands &
|
||||
DatabasePanelCommands &
|
||||
AstCfgCommands &
|
||||
|
||||
@@ -6,24 +6,38 @@ import {
|
||||
} from "vscode";
|
||||
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from "../helpers";
|
||||
import { LocalQueries } from "../local-queries";
|
||||
import { getQuickEvalContext, validateQueryPath } from "../run-queries-shared";
|
||||
import * as CodeQLDebugProtocol from "./debug-protocol";
|
||||
|
||||
/**
|
||||
* The CodeQL launch arguments, as specified in "launch.json".
|
||||
*/
|
||||
interface QLDebugArgs {
|
||||
query: string;
|
||||
database: string;
|
||||
additionalPacks: string[] | string;
|
||||
extensionPacks: string[] | string;
|
||||
query?: string;
|
||||
database?: string;
|
||||
additionalPacks?: string[] | string;
|
||||
extensionPacks?: string[] | string;
|
||||
quickEval?: boolean;
|
||||
noDebug?: boolean;
|
||||
}
|
||||
|
||||
type QLDebugConfiguration = DebugConfiguration & Partial<QLDebugArgs>;
|
||||
|
||||
interface QLResolvedDebugArgs extends QLDebugArgs {
|
||||
additionalPacks: string[];
|
||||
extensionPacks: string[];
|
||||
}
|
||||
/**
|
||||
* The debug configuration for a CodeQL configuration.
|
||||
*
|
||||
* This just combines `QLDebugArgs` with the standard debug configuration properties.
|
||||
*/
|
||||
type QLDebugConfiguration = DebugConfiguration & QLDebugArgs;
|
||||
|
||||
/**
|
||||
* A CodeQL debug configuration after all variables and defaults have been resolved. This is what
|
||||
* is passed to the debug adapter via the `launch` request.
|
||||
*/
|
||||
export type QLResolvedDebugConfiguration = DebugConfiguration &
|
||||
QLResolvedDebugArgs;
|
||||
CodeQLDebugProtocol.LaunchConfig;
|
||||
|
||||
/**
|
||||
* Implementation of `DebugConfigurationProvider` for CodeQL.
|
||||
*/
|
||||
export class QLDebugConfigurationProvider
|
||||
implements DebugConfigurationProvider
|
||||
{
|
||||
@@ -36,10 +50,12 @@ export class QLDebugConfigurationProvider
|
||||
): DebugConfiguration {
|
||||
const qlConfiguration = <QLDebugConfiguration>debugConfiguration;
|
||||
|
||||
// Fill in defaults
|
||||
// Fill in defaults for properties whose default value is a command invocation. VS Code will
|
||||
// invoke any commands to fill in actual values, then call
|
||||
// `resolveDebugConfigurationWithSubstitutedVariables()`with the result.
|
||||
const resultConfiguration: QLDebugConfiguration = {
|
||||
...qlConfiguration,
|
||||
query: qlConfiguration.query ?? "${file}",
|
||||
query: qlConfiguration.query ?? "${command:currentQuery}",
|
||||
database: qlConfiguration.database ?? "${command:currentDatabase}",
|
||||
};
|
||||
|
||||
@@ -83,12 +99,23 @@ export class QLDebugConfigurationProvider
|
||||
? [qlConfiguration.extensionPacks]
|
||||
: qlConfiguration.extensionPacks;
|
||||
|
||||
const quickEval = qlConfiguration.quickEval ?? false;
|
||||
validateQueryPath(qlConfiguration.query, quickEval);
|
||||
|
||||
const quickEvalContext = quickEval
|
||||
? await getQuickEvalContext(undefined)
|
||||
: undefined;
|
||||
|
||||
const resultConfiguration: QLResolvedDebugConfiguration = {
|
||||
...qlConfiguration,
|
||||
name: qlConfiguration.name,
|
||||
request: qlConfiguration.request,
|
||||
type: qlConfiguration.type,
|
||||
query: qlConfiguration.query,
|
||||
database: qlConfiguration.database,
|
||||
additionalPacks,
|
||||
extensionPacks,
|
||||
quickEvalPosition: quickEvalContext?.quickEvalPosition,
|
||||
noDebug: qlConfiguration.noDebug ?? false,
|
||||
};
|
||||
|
||||
return resultConfiguration;
|
||||
|
||||
@@ -17,6 +17,9 @@ export interface EvaluationStartedEventBody {
|
||||
outputDir: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom event to provide additional information about a running evaluation.
|
||||
*/
|
||||
export interface EvaluationStartedEvent extends DebugProtocol.Event {
|
||||
event: "codeql-evaluation-started";
|
||||
body: EvaluationStartedEventBody;
|
||||
@@ -28,6 +31,9 @@ export interface EvaluationCompletedEventBody {
|
||||
evaluationTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom event to provide additional information about a completed evaluation.
|
||||
*/
|
||||
export interface EvaluationCompletedEvent extends DebugProtocol.Event {
|
||||
event: "codeql-evaluation-completed";
|
||||
body: EvaluationCompletedEventBody;
|
||||
@@ -61,10 +67,28 @@ export type AnyResponse = InitializeResponse;
|
||||
|
||||
export type AnyProtocolMessage = AnyEvent | AnyRequest | AnyResponse;
|
||||
|
||||
export interface LaunchRequestArguments
|
||||
extends DebugProtocol.LaunchRequestArguments {
|
||||
query: string;
|
||||
database: string;
|
||||
additionalPacks: string[];
|
||||
extensionPacks: string[];
|
||||
export interface Position {
|
||||
fileName: string;
|
||||
line: number;
|
||||
column: number;
|
||||
endLine: number;
|
||||
endColumn: number;
|
||||
}
|
||||
|
||||
export interface LaunchConfig {
|
||||
/** Full path to query (.ql) file. */
|
||||
query: string;
|
||||
/** Full path to the database directory. */
|
||||
database: string;
|
||||
/** Full paths to `--additional-packs` directories. */
|
||||
additionalPacks: string[];
|
||||
/** Pack names of extension packs. */
|
||||
extensionPacks: string[];
|
||||
/** Optional quick evaluation position. */
|
||||
quickEvalPosition: Position | undefined;
|
||||
/** Run the query without debugging it. */
|
||||
noDebug: boolean;
|
||||
}
|
||||
|
||||
export type LaunchRequestArguments = DebugProtocol.LaunchRequestArguments &
|
||||
LaunchConfig;
|
||||
|
||||
@@ -5,16 +5,20 @@ import {
|
||||
LoggingDebugSession,
|
||||
OutputEvent,
|
||||
ProgressEndEvent,
|
||||
StoppedEvent,
|
||||
TerminatedEvent,
|
||||
} from "@vscode/debugadapter";
|
||||
import { DebugProtocol } from "@vscode/debugprotocol";
|
||||
import { Disposable } from "vscode";
|
||||
import { CancellationTokenSource } from "vscode-jsonrpc";
|
||||
import { BaseLogger, LogOptions } from "../common";
|
||||
import { BaseLogger, LogOptions, queryServerLogger } from "../common";
|
||||
import { QueryResultType } from "../pure/new-messages";
|
||||
import { CoreQueryResults, CoreQueryRun, QueryRunner } from "../queryRunner";
|
||||
import * as CodeQLDebugProtocol from "./debug-protocol";
|
||||
|
||||
// More complete implementations of `Event` for certain events, because the classes from
|
||||
// `@vscode/debugadapter` make it more difficult to provide some of the message values.
|
||||
|
||||
class ProgressStartEvent
|
||||
extends Event
|
||||
implements DebugProtocol.ProgressStartEvent
|
||||
@@ -95,11 +99,42 @@ class EvaluationCompletedEvent
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Possible states of the debug session. Used primarily to guard against unexpected requests.
|
||||
*/
|
||||
type State =
|
||||
| "uninitialized"
|
||||
| "initialized"
|
||||
| "running"
|
||||
| "stopped"
|
||||
| "terminated";
|
||||
|
||||
// IDs for error messages generated by the debug adapter itself.
|
||||
|
||||
/** Received a DAP message while in an unexpected state. */
|
||||
const ERROR_UNEXPECTED_STATE = 1;
|
||||
|
||||
/** ID of the "thread" that represents the query evaluation. */
|
||||
const QUERY_THREAD_ID = 1;
|
||||
|
||||
/** The user-visible name of the query evaluation thread. */
|
||||
const QUERY_THREAD_NAME = "Evaluation thread";
|
||||
|
||||
/**
|
||||
* An in-process implementation of the debug adapter for CodeQL queries.
|
||||
*
|
||||
* For now, this is pretty much just a wrapper around the query server.
|
||||
*/
|
||||
export class QLDebugSession extends LoggingDebugSession implements Disposable {
|
||||
private state: State = "uninitialized";
|
||||
private terminateOnComplete = false;
|
||||
private args: CodeQLDebugProtocol.LaunchRequestArguments | undefined =
|
||||
undefined;
|
||||
private tokenSource: CancellationTokenSource | undefined = undefined;
|
||||
private queryRun: CoreQueryRun | undefined = undefined;
|
||||
private lastResult:
|
||||
| CodeQLDebugProtocol.EvaluationCompletedEventBody
|
||||
| undefined = undefined;
|
||||
|
||||
constructor(
|
||||
private readonly queryStorageDir: string,
|
||||
@@ -113,22 +148,50 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
|
||||
}
|
||||
|
||||
protected dispatchRequest(request: DebugProtocol.Request): void {
|
||||
// We just defer to the base class implementation, but having this override makes it easy to set
|
||||
// a breakpoint that will be hit for any message received by the debug adapter.
|
||||
void queryServerLogger.log(`DAP request: ${request.command}`);
|
||||
super.dispatchRequest(request);
|
||||
}
|
||||
|
||||
private unexpectedState(response: DebugProtocol.Response): void {
|
||||
this.sendErrorResponse(
|
||||
response,
|
||||
ERROR_UNEXPECTED_STATE,
|
||||
"CodeQL debug adapter received request '{_request}' while in unexpected state '{_actualState}'.",
|
||||
{
|
||||
_request: response.command,
|
||||
_actualState: this.state,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
protected initializeRequest(
|
||||
response: DebugProtocol.InitializeResponse,
|
||||
_args: DebugProtocol.InitializeRequestArguments,
|
||||
): void {
|
||||
response.body = response.body ?? {};
|
||||
response.body.supportsStepBack = false;
|
||||
response.body.supportsStepInTargetsRequest = false;
|
||||
response.body.supportsRestartFrame = false;
|
||||
response.body.supportsGotoTargetsRequest = false;
|
||||
switch (this.state) {
|
||||
case "uninitialized":
|
||||
response.body = response.body ?? {};
|
||||
response.body.supportsStepBack = false;
|
||||
response.body.supportsStepInTargetsRequest = false;
|
||||
response.body.supportsRestartFrame = false;
|
||||
response.body.supportsGotoTargetsRequest = false;
|
||||
response.body.supportsCancelRequest = true;
|
||||
response.body.supportsTerminateRequest = true;
|
||||
response.body.supportsModulesRequest = false;
|
||||
response.body.supportsConfigurationDoneRequest = true;
|
||||
response.body.supportsRestartRequest = false;
|
||||
this.state = "initialized";
|
||||
this.sendResponse(response);
|
||||
|
||||
this.sendResponse(response);
|
||||
this.sendEvent(new InitializedEvent());
|
||||
break;
|
||||
|
||||
this.sendEvent(new InitializedEvent());
|
||||
default:
|
||||
this.unexpectedState(response);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected configurationDoneRequest(
|
||||
@@ -144,8 +207,32 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
|
||||
_args: DebugProtocol.DisconnectArguments,
|
||||
_request?: DebugProtocol.Request,
|
||||
): void {
|
||||
response.body = response.body ?? {};
|
||||
// Neither of the args (`terminateDebuggee` and `restart`) matter for CodeQL.
|
||||
this.terminateOrDisconnect(response);
|
||||
}
|
||||
|
||||
protected terminateRequest(
|
||||
response: DebugProtocol.TerminateResponse,
|
||||
_args: DebugProtocol.TerminateArguments,
|
||||
_request?: DebugProtocol.Request,
|
||||
): void {
|
||||
this.terminateOrDisconnect(response);
|
||||
}
|
||||
|
||||
private terminateOrDisconnect(response: DebugProtocol.Response): void {
|
||||
switch (this.state) {
|
||||
case "running":
|
||||
this.terminateOnComplete = true;
|
||||
this.cancelEvaluation();
|
||||
break;
|
||||
|
||||
case "stopped":
|
||||
this.terminateAndExit();
|
||||
break;
|
||||
|
||||
default:
|
||||
// Ignore
|
||||
break;
|
||||
}
|
||||
|
||||
this.sendResponse(response);
|
||||
}
|
||||
@@ -155,7 +242,47 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
|
||||
args: CodeQLDebugProtocol.LaunchRequestArguments,
|
||||
_request?: DebugProtocol.Request,
|
||||
): void {
|
||||
void this.launch(response, args); //TODO: Cancelation?
|
||||
switch (this.state) {
|
||||
case "initialized":
|
||||
this.args = args;
|
||||
|
||||
// If `noDebug` is set, then terminate after evaluation instead of stopping.
|
||||
this.terminateOnComplete = this.args.noDebug === true;
|
||||
|
||||
response.body = response.body ?? {};
|
||||
|
||||
// Send the response immediately. We'll send a "stopped" message when the evaluation is complete.
|
||||
this.sendResponse(response);
|
||||
|
||||
void this.evaluate();
|
||||
break;
|
||||
|
||||
default:
|
||||
this.unexpectedState(response);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected continueRequest(
|
||||
response: DebugProtocol.ContinueResponse,
|
||||
_args: DebugProtocol.ContinueArguments,
|
||||
_request?: DebugProtocol.Request,
|
||||
): void {
|
||||
switch (this.state) {
|
||||
case "stopped":
|
||||
response.body = response.body ?? {};
|
||||
response.body.allThreadsContinued = true;
|
||||
|
||||
// Send the response immediately. We'll send a "stopped" message when the evaluation is complete.
|
||||
this.sendResponse(response);
|
||||
|
||||
void this.evaluate();
|
||||
break;
|
||||
|
||||
default:
|
||||
this.unexpectedState(response);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected cancelRequest(
|
||||
@@ -163,10 +290,18 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
|
||||
args: DebugProtocol.CancelArguments,
|
||||
_request?: DebugProtocol.Request,
|
||||
): void {
|
||||
if (args.progressId !== undefined) {
|
||||
if (this.queryRun?.id === args.progressId) {
|
||||
this.cancelEvaluation();
|
||||
}
|
||||
switch (this.state) {
|
||||
case "running":
|
||||
if (args.progressId !== undefined) {
|
||||
if (this.queryRun!.id === args.progressId) {
|
||||
this.cancelEvaluation();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Ignore;
|
||||
break;
|
||||
}
|
||||
|
||||
this.sendResponse(response);
|
||||
@@ -174,17 +309,17 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
|
||||
|
||||
protected threadsRequest(
|
||||
response: DebugProtocol.ThreadsResponse,
|
||||
request?: DebugProtocol.Request,
|
||||
_request?: DebugProtocol.Request,
|
||||
): void {
|
||||
response.body = response.body ?? {};
|
||||
response.body.threads = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Evaluation thread",
|
||||
id: QUERY_THREAD_ID,
|
||||
name: QUERY_THREAD_NAME,
|
||||
},
|
||||
];
|
||||
|
||||
super.threadsRequest(response, request);
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
protected stackTraceRequest(
|
||||
@@ -193,22 +328,12 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
|
||||
_request?: DebugProtocol.Request,
|
||||
): void {
|
||||
response.body = response.body ?? {};
|
||||
response.body.stackFrames = [];
|
||||
response.body.stackFrames = []; // No frames for now.
|
||||
|
||||
super.stackTraceRequest(response, _args, _request);
|
||||
}
|
||||
|
||||
private async launch(
|
||||
response: DebugProtocol.LaunchResponse,
|
||||
args: CodeQLDebugProtocol.LaunchRequestArguments,
|
||||
): Promise<void> {
|
||||
response.body = response.body ?? {};
|
||||
|
||||
this.args = args;
|
||||
|
||||
void this.evaluate(response);
|
||||
}
|
||||
|
||||
/** Creates a `BaseLogger` that sends output to the debug console. */
|
||||
private createLogger(): BaseLogger {
|
||||
return {
|
||||
log: async (message: string, _options: LogOptions): Promise<void> => {
|
||||
@@ -217,21 +342,24 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
|
||||
};
|
||||
}
|
||||
|
||||
private async evaluate(
|
||||
response: DebugProtocol.LaunchResponse,
|
||||
): Promise<void> {
|
||||
// Send the response immediately. We'll send a "stopped" message when the evaluation is complete.
|
||||
this.sendResponse(response);
|
||||
|
||||
/**
|
||||
* Runs the query or quickeval, and notifies the debugger client when the evaluation completes.
|
||||
*
|
||||
* This function is invoked from the `launch` and `continue` handlers, without awaiting its
|
||||
* result.
|
||||
*/
|
||||
private async evaluate(): Promise<void> {
|
||||
const args = this.args!;
|
||||
|
||||
this.tokenSource = new CancellationTokenSource();
|
||||
try {
|
||||
// Create the query run, which will give us some information about the query even before the
|
||||
// evaluation has completed.
|
||||
this.queryRun = this.queryRunner.createQueryRun(
|
||||
args.database,
|
||||
{
|
||||
queryPath: args.query,
|
||||
quickEvalPosition: undefined,
|
||||
quickEvalPosition: args.quickEvalPosition,
|
||||
},
|
||||
true,
|
||||
args.additionalPacks,
|
||||
@@ -241,6 +369,8 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
|
||||
undefined,
|
||||
);
|
||||
|
||||
this.state = "running";
|
||||
|
||||
// Send the `EvaluationStarted` event first, to let the client known where the outputs are
|
||||
// going to show up.
|
||||
this.sendEvent(
|
||||
@@ -249,6 +379,8 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
|
||||
this.queryRun.outputDir.querySaveDir,
|
||||
),
|
||||
);
|
||||
|
||||
// Report progress via the debugger protocol.
|
||||
const progressStart = new ProgressStartEvent(
|
||||
this.queryRun.id,
|
||||
"Running query",
|
||||
@@ -286,9 +418,14 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the evaluation as completed, and notify the client of the result.
|
||||
*/
|
||||
private completeEvaluation(
|
||||
result: CodeQLDebugProtocol.EvaluationCompletedEventBody,
|
||||
): void {
|
||||
this.lastResult = result;
|
||||
|
||||
// Report the end of the progress
|
||||
this.sendEvent(new ProgressEndEvent(this.queryRun!.id));
|
||||
// Report the evaluation result
|
||||
@@ -300,13 +437,25 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
|
||||
this.sendEvent(outputEvent);
|
||||
}
|
||||
|
||||
if (this.terminateOnComplete) {
|
||||
this.terminateAndExit();
|
||||
} else {
|
||||
// Report the session as "stopped", but keep the session open.
|
||||
this.sendEvent(new StoppedEvent("entry", QUERY_THREAD_ID));
|
||||
|
||||
this.state = "stopped";
|
||||
}
|
||||
this.queryRun = undefined;
|
||||
}
|
||||
|
||||
private terminateAndExit(): void {
|
||||
// Report the debugging session as terminated.
|
||||
this.sendEvent(new TerminatedEvent());
|
||||
|
||||
// Report the debuggee as exited.
|
||||
this.sendEvent(new ExitedEvent(result.resultType));
|
||||
this.sendEvent(new ExitedEvent(this.lastResult!.resultType));
|
||||
|
||||
this.queryRun = undefined;
|
||||
this.state = "terminated";
|
||||
}
|
||||
|
||||
private disposeTokenSource(): void {
|
||||
|
||||
@@ -4,19 +4,17 @@ import {
|
||||
DebugAdapterDescriptorFactory,
|
||||
DebugAdapterExecutable,
|
||||
DebugAdapterInlineImplementation,
|
||||
DebugAdapterServer,
|
||||
DebugConfigurationProviderTriggerKind,
|
||||
DebugSession,
|
||||
ProviderResult,
|
||||
} from "vscode";
|
||||
import { isCanary } from "../config";
|
||||
import { LocalQueries } from "../local-queries";
|
||||
import { DisposableObject } from "../pure/disposable-object";
|
||||
import { QueryRunner } from "../queryRunner";
|
||||
import { QLDebugConfigurationProvider } from "./debug-configuration";
|
||||
import { QLDebugSession } from "./debug-session";
|
||||
|
||||
const useInlineImplementation = true;
|
||||
|
||||
export class QLDebugAdapterDescriptorFactory
|
||||
extends DisposableObject
|
||||
implements DebugAdapterDescriptorFactory
|
||||
@@ -27,6 +25,7 @@ export class QLDebugAdapterDescriptorFactory
|
||||
localQueries: LocalQueries,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.push(debug.registerDebugAdapterDescriptorFactory("codeql", this));
|
||||
this.push(
|
||||
debug.registerDebugConfigurationProvider(
|
||||
@@ -43,13 +42,12 @@ export class QLDebugAdapterDescriptorFactory
|
||||
_session: DebugSession,
|
||||
_executable: DebugAdapterExecutable | undefined,
|
||||
): ProviderResult<DebugAdapterDescriptor> {
|
||||
if (useInlineImplementation) {
|
||||
return new DebugAdapterInlineImplementation(
|
||||
new QLDebugSession(this.queryStorageDir, this.queryRunner),
|
||||
);
|
||||
} else {
|
||||
return new DebugAdapterServer(2112);
|
||||
if (!isCanary()) {
|
||||
throw new Error("The CodeQL debugger feature is not available yet.");
|
||||
}
|
||||
return new DebugAdapterInlineImplementation(
|
||||
new QLDebugSession(this.queryStorageDir, this.queryRunner),
|
||||
);
|
||||
}
|
||||
|
||||
private handleOnDidStartDebugSession(session: DebugSession): void {
|
||||
|
||||
@@ -6,7 +6,10 @@ import {
|
||||
// window,
|
||||
Uri,
|
||||
CancellationTokenSource,
|
||||
commands,
|
||||
} from "vscode";
|
||||
import { DebuggerCommands } from "../common/commands";
|
||||
import { isCanary } from "../config";
|
||||
import { ResultsView } from "../interface";
|
||||
import { WebviewReveal } from "../interface-utils";
|
||||
import { DatabaseManager } from "../local-databases";
|
||||
@@ -18,11 +21,16 @@ import { QueryOutputDir } from "../run-queries-shared";
|
||||
import { QLResolvedDebugConfiguration } from "./debug-configuration";
|
||||
import * as CodeQLDebugProtocol from "./debug-protocol";
|
||||
|
||||
/**
|
||||
* Listens to messages passing between VS Code and the debug adapter, so that we can supplement the
|
||||
* UI.
|
||||
*/
|
||||
class QLDebugAdapterTracker
|
||||
extends DisposableObject
|
||||
implements DebugAdapterTracker
|
||||
{
|
||||
private readonly configuration: QLResolvedDebugConfiguration;
|
||||
/** The `LocalQueryRun` of the current evaluation, if one is running. */
|
||||
private localQueryRun: LocalQueryRun | undefined;
|
||||
/** The promise of the most recently queued deferred message handler. */
|
||||
private lastDeferredMessageHandler: Promise<void> = Promise.resolve();
|
||||
@@ -66,11 +74,24 @@ class QLDebugAdapterTracker
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a message handler to be executed once all other pending message handlers have completed.
|
||||
*
|
||||
* The `onDidSendMessage()` function is synchronous, so it needs to return before any async
|
||||
* handling of the msssage is completed. We can't just launch the message handler directly from
|
||||
* `onDidSendMessage()`, though, because if the message handler's implementation blocks awaiting
|
||||
* a promise, then another event might be received by `onDidSendMessage()` while the first message
|
||||
* handler is still incomplete.
|
||||
*
|
||||
* To enforce sequential execution of event handlers, we queue each new handler as a `finally()`
|
||||
* handler for the most recently queued message.
|
||||
*/
|
||||
private queueMessageHandler(handler: () => Promise<void>): void {
|
||||
this.lastDeferredMessageHandler =
|
||||
this.lastDeferredMessageHandler.finally(handler);
|
||||
}
|
||||
|
||||
/** Updates the UI to track the currently executing query. */
|
||||
private async onEvaluationStarted(
|
||||
body: CodeQLDebugProtocol.EvaluationStartedEventBody,
|
||||
): Promise<void> {
|
||||
@@ -83,11 +104,17 @@ class QLDebugAdapterTracker
|
||||
debug.stopDebugging(this.session),
|
||||
);
|
||||
|
||||
const quickEval =
|
||||
this.configuration.quickEvalPosition !== undefined
|
||||
? {
|
||||
quickEvalPosition: this.configuration.quickEvalPosition,
|
||||
quickEvalText: "quickeval!!!", // TODO: Have the debug adapter return the range, and extract the text from the editor.
|
||||
}
|
||||
: undefined;
|
||||
this.localQueryRun = await this.localQueries.createLocalQueryRun(
|
||||
{
|
||||
queryPath: this.configuration.query,
|
||||
quickEvalPosition: undefined,
|
||||
quickEvalText: undefined,
|
||||
quickEval,
|
||||
},
|
||||
dbItem,
|
||||
new QueryOutputDir(body.outputDir),
|
||||
@@ -95,6 +122,7 @@ class QLDebugAdapterTracker
|
||||
);
|
||||
}
|
||||
|
||||
/** Update the UI after a query has finished evaluating. */
|
||||
private async onEvaluationCompleted(
|
||||
body: CodeQLDebugProtocol.EvaluationCompletedEventBody,
|
||||
): Promise<void> {
|
||||
@@ -106,6 +134,7 @@ class QLDebugAdapterTracker
|
||||
}
|
||||
}
|
||||
|
||||
/** Service handling the UI for CodeQL debugging. */
|
||||
export class DebuggerUI
|
||||
extends DisposableObject
|
||||
implements DebugAdapterTrackerFactory
|
||||
@@ -119,7 +148,16 @@ export class DebuggerUI
|
||||
) {
|
||||
super();
|
||||
|
||||
this.push(debug.registerDebugAdapterTrackerFactory("codeql", this));
|
||||
if (isCanary()) {
|
||||
this.push(debug.registerDebugAdapterTrackerFactory("codeql", this));
|
||||
}
|
||||
}
|
||||
|
||||
public getCommands(): DebuggerCommands {
|
||||
return {
|
||||
"codeQL.debug.quickEval": this.quickEval.bind(this),
|
||||
"codeQL.debug.quickEvalContextEditor": this.quickEval.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
public createDebugAdapterTracker(
|
||||
@@ -143,6 +181,14 @@ export class DebuggerUI
|
||||
this.sessions.delete(session.id);
|
||||
}
|
||||
|
||||
private async quickEval(_uri: Uri): Promise<void> {
|
||||
await commands.executeCommand("workbench.action.debug.start", {
|
||||
config: {
|
||||
quickEval: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private getTrackerForSession(
|
||||
session: DebugSession,
|
||||
): QLDebugAdapterTracker | undefined {
|
||||
|
||||
@@ -863,16 +863,13 @@ async function activateWithInstalledDistribution(
|
||||
ctx.subscriptions.push(localQueries);
|
||||
|
||||
void extLogger.log("Initializing debugger factory.");
|
||||
const debuggerFactory = ctx.subscriptions.push(
|
||||
ctx.subscriptions.push(
|
||||
new QLDebugAdapterDescriptorFactory(queryStorageDir, qs, localQueries),
|
||||
);
|
||||
void debuggerFactory;
|
||||
|
||||
void extLogger.log("Initializing debugger UI.");
|
||||
const debuggerUI = ctx.subscriptions.push(
|
||||
new DebuggerUI(localQueryResultsView, localQueries, dbm),
|
||||
);
|
||||
void debuggerUI;
|
||||
const debuggerUI = new DebuggerUI(localQueryResultsView, localQueries, dbm);
|
||||
ctx.subscriptions.push(debuggerUI);
|
||||
|
||||
void extLogger.log("Initializing QLTest interface.");
|
||||
const testExplorerExtension = extensions.getExtension<TestHub>(
|
||||
@@ -940,6 +937,7 @@ async function activateWithInstalledDistribution(
|
||||
...summaryLanguageSupport.getCommands(),
|
||||
...testUiCommands,
|
||||
...mockServer.getCommands(),
|
||||
...debuggerUI.getCommands(),
|
||||
};
|
||||
|
||||
for (const [commandName, command] of Object.entries(allCommands)) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
ThemeIcon,
|
||||
ThemeColor,
|
||||
workspace,
|
||||
ProgressLocation,
|
||||
} from "vscode";
|
||||
import { pathExists, stat, readdir, remove } from "fs-extra";
|
||||
|
||||
@@ -21,7 +22,12 @@ import {
|
||||
DatabaseItem,
|
||||
DatabaseManager,
|
||||
} from "./local-databases";
|
||||
import { ProgressCallback, withProgress } from "./progress";
|
||||
import {
|
||||
ProgressCallback,
|
||||
ProgressContext,
|
||||
withInheritedProgress,
|
||||
withProgress,
|
||||
} from "./progress";
|
||||
import {
|
||||
isLikelyDatabaseRoot,
|
||||
isLikelyDbLanguageFolder,
|
||||
@@ -255,7 +261,7 @@ export class DatabaseUI extends DisposableObject {
|
||||
token: CancellationToken,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.chooseAndSetDatabase(true, progress, token);
|
||||
await this.chooseAndSetDatabase(true, { progress, token });
|
||||
} catch (e) {
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
@@ -416,7 +422,7 @@ export class DatabaseUI extends DisposableObject {
|
||||
token: CancellationToken,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.chooseAndSetDatabase(false, progress, token);
|
||||
await this.chooseAndSetDatabase(false, { progress, token });
|
||||
} catch (e: unknown) {
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
@@ -604,7 +610,8 @@ export class DatabaseUI extends DisposableObject {
|
||||
}
|
||||
|
||||
private async handleGetCurrentDatabase(): Promise<string | undefined> {
|
||||
return this.databaseManager.currentDatabaseItem?.databaseUri.fsPath;
|
||||
const dbItem = await this.getDatabaseItemInternal(undefined);
|
||||
return dbItem?.databaseUri.fsPath;
|
||||
}
|
||||
|
||||
private async handleSetCurrentDatabase(uri: Uri): Promise<void> {
|
||||
@@ -722,9 +729,24 @@ export class DatabaseUI extends DisposableObject {
|
||||
public async getDatabaseItem(
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<DatabaseItem | undefined> {
|
||||
return await this.getDatabaseItemInternal({ progress, token });
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current database directory. If we don't already have a
|
||||
* current database, ask the user for one, and return that, or
|
||||
* undefined if they cancel.
|
||||
*
|
||||
* Unlike `getDatabaseItem()`, this function does not require the caller to pass in a progress
|
||||
* context. If `progress` is `undefined`, then this command will create a new progress
|
||||
* notification if it tries to perform any long-running operations.
|
||||
*/
|
||||
private async getDatabaseItemInternal(
|
||||
progress: ProgressContext | undefined,
|
||||
): Promise<DatabaseItem | undefined> {
|
||||
if (this.databaseManager.currentDatabaseItem === undefined) {
|
||||
await this.chooseAndSetDatabase(false, progress, token);
|
||||
await this.chooseAndSetDatabase(false, progress);
|
||||
}
|
||||
|
||||
return this.databaseManager.currentDatabaseItem;
|
||||
@@ -754,31 +776,40 @@ export class DatabaseUI extends DisposableObject {
|
||||
*/
|
||||
private async chooseAndSetDatabase(
|
||||
byFolder: boolean,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
progress: ProgressContext | undefined,
|
||||
): Promise<DatabaseItem | undefined> {
|
||||
const uri = await chooseDatabaseDir(byFolder);
|
||||
if (!uri) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (byFolder) {
|
||||
const fixedUri = await this.fixDbUri(uri);
|
||||
// we are selecting a database folder
|
||||
return await this.setCurrentDatabase(progress, token, fixedUri);
|
||||
} else {
|
||||
// we are selecting a database archive. Must unzip into a workspace-controlled area
|
||||
// before importing.
|
||||
return await importArchiveDatabase(
|
||||
this.app.commands,
|
||||
uri.toString(true),
|
||||
this.databaseManager,
|
||||
this.storagePath,
|
||||
progress,
|
||||
token,
|
||||
this.queryServer?.cliServer,
|
||||
);
|
||||
}
|
||||
return await withInheritedProgress(
|
||||
progress,
|
||||
async (progress, token) => {
|
||||
if (byFolder) {
|
||||
const fixedUri = await this.fixDbUri(uri);
|
||||
// we are selecting a database folder
|
||||
return await this.setCurrentDatabase(progress, token, fixedUri);
|
||||
} else {
|
||||
// we are selecting a database archive. Must unzip into a workspace-controlled area
|
||||
// before importing.
|
||||
return await importArchiveDatabase(
|
||||
this.app.commands,
|
||||
uri.toString(true),
|
||||
this.databaseManager,
|
||||
this.storagePath,
|
||||
progress,
|
||||
token,
|
||||
this.queryServer?.cliServer,
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
location: ProgressLocation.Notification,
|
||||
cancellable: true,
|
||||
title: "Opening database",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Range,
|
||||
Uri,
|
||||
window,
|
||||
workspace,
|
||||
} from "vscode";
|
||||
import { BaseLogger, extLogger, Logger, TeeLogger } from "./common";
|
||||
import { MAX_QUERIES } from "./config";
|
||||
@@ -33,14 +34,16 @@ import { ResultsView } from "./interface";
|
||||
import { DatabaseItem, DatabaseManager } from "./local-databases";
|
||||
import {
|
||||
createInitialQueryInfo,
|
||||
determineSelectedQuery,
|
||||
EvaluatorLogPaths,
|
||||
generateEvalLogSummaries,
|
||||
getQuickEvalContext,
|
||||
logEndSummary,
|
||||
promptUserToSaveChanges,
|
||||
QueryEvaluationInfo,
|
||||
QueryOutputDir,
|
||||
QueryWithResults,
|
||||
SelectedQuery,
|
||||
validateQueryUri,
|
||||
} from "./run-queries-shared";
|
||||
import { CompletedLocalQueryInfo, LocalQueryInfo } from "./query-results";
|
||||
import { WebviewReveal } from "./interface-utils";
|
||||
@@ -74,6 +77,25 @@ function formatResultMessage(result: CoreQueryResults): string {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If either the query file or the quickeval file is dirty, give the user the chance to save them.
|
||||
*/
|
||||
async function promptToSaveQueryIfNeeded(query: SelectedQuery): Promise<void> {
|
||||
// There seems to be no way to ask VS Code to find an existing text document by name, without
|
||||
// automatically opening the document if it is not found.
|
||||
const queryUri = Uri.file(query.queryPath).toString();
|
||||
const quickEvalUri =
|
||||
query.quickEval !== undefined
|
||||
? Uri.file(query.quickEval.quickEvalPosition.fileName).toString()
|
||||
: undefined;
|
||||
for (const openDocument of workspace.textDocuments) {
|
||||
const documentUri = openDocument.uri.toString();
|
||||
if (documentUri === queryUri || documentUri === quickEvalUri) {
|
||||
await promptUserToSaveChanges(openDocument);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks the evaluation of a local query, including its interactions with the UI.
|
||||
*
|
||||
@@ -237,6 +259,7 @@ export class LocalQueries extends DisposableObject {
|
||||
"codeQL.quickEvalContextEditor": this.quickEval.bind(this),
|
||||
"codeQL.codeLensQuickEval": this.codeLensQuickEval.bind(this),
|
||||
"codeQL.quickQuery": this.quickQuery.bind(this),
|
||||
"codeQL.getCurrentQuery": this.getCurrentQuery.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -375,6 +398,23 @@ export class LocalQueries extends DisposableObject {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current active query.
|
||||
*
|
||||
* For now, the "active query" is just whatever query is in the active text editor. Once we have a
|
||||
* propery "queries" panel, we can provide a way to select the current query there.
|
||||
*/
|
||||
private async getCurrentQuery(): Promise<string> {
|
||||
const editor = window.activeTextEditor;
|
||||
if (editor === undefined) {
|
||||
throw new Error(
|
||||
"No query was selected. Please select a query and try again.",
|
||||
);
|
||||
}
|
||||
|
||||
return validateQueryUri(editor.document.uri, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new `LocalQueryRun` object to track a query evaluation. This creates a timestamp
|
||||
* file in the query's output directory, creates a `LocalQueryInfo` object, and registers that
|
||||
@@ -445,15 +485,24 @@ export class LocalQueries extends DisposableObject {
|
||||
databaseItem: DatabaseItem | undefined,
|
||||
range?: Range,
|
||||
): Promise<CoreCompletedQuery> {
|
||||
const selectedQuery = await determineSelectedQuery(
|
||||
queryUri,
|
||||
quickEval,
|
||||
range,
|
||||
);
|
||||
let queryPath: string;
|
||||
if (queryUri !== undefined) {
|
||||
// The query URI is provided by the command, most likely because the command was run from an
|
||||
// editor context menu. Use the provided URI, but make sure it's a valid query.
|
||||
queryPath = validateQueryUri(queryUri, quickEval);
|
||||
} else {
|
||||
// Use the currently selected query.
|
||||
queryPath = await this.getCurrentQuery();
|
||||
}
|
||||
|
||||
const selectedQuery: SelectedQuery = {
|
||||
queryPath,
|
||||
quickEval: quickEval ? await getQuickEvalContext(range) : undefined,
|
||||
};
|
||||
|
||||
// If no databaseItem is specified, use the database currently selected in the Databases UI
|
||||
databaseItem =
|
||||
databaseItem || (await this.databaseUI.getDatabaseItem(progress, token));
|
||||
databaseItem ?? (await this.databaseUI.getDatabaseItem(progress, token));
|
||||
if (databaseItem === undefined) {
|
||||
throw new Error("Can't run query without a selected database");
|
||||
}
|
||||
@@ -461,11 +510,13 @@ export class LocalQueries extends DisposableObject {
|
||||
const additionalPacks = getOnDiskWorkspaceFolders();
|
||||
const extensionPacks = await this.getDefaultExtensionPacks(additionalPacks);
|
||||
|
||||
await promptToSaveQueryIfNeeded(selectedQuery);
|
||||
|
||||
const coreQueryRun = this.queryRunner.createQueryRun(
|
||||
databaseItem.databaseUri.fsPath,
|
||||
{
|
||||
queryPath: selectedQuery.queryPath,
|
||||
quickEvalPosition: selectedQuery.quickEvalPosition,
|
||||
quickEvalPosition: selectedQuery.quickEval?.quickEvalPosition,
|
||||
},
|
||||
true,
|
||||
additionalPacks,
|
||||
|
||||
@@ -82,6 +82,29 @@ export function withProgress<R>(
|
||||
);
|
||||
}
|
||||
|
||||
export interface ProgressContext {
|
||||
progress: ProgressCallback;
|
||||
token: CancellationToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `withProgress()`, except that the caller is not required to provide a progress context. If
|
||||
* the caller does provide one, any long-running operations performed by `task` will use the
|
||||
* supplied progress context. Otherwise, this function wraps `task` in a new progress context with
|
||||
* the supplied options.
|
||||
*/
|
||||
export function withInheritedProgress<R>(
|
||||
parent: ProgressContext | undefined,
|
||||
task: ProgressTask<R>,
|
||||
options: ProgressOptions,
|
||||
): Thenable<R> {
|
||||
if (parent !== undefined) {
|
||||
return task(parent.progress, parent.token);
|
||||
} else {
|
||||
return withProgress(task, options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a progress monitor that indicates how much progess has been made
|
||||
* reading from a stream.
|
||||
|
||||
@@ -389,55 +389,33 @@ export interface QueryWithResults {
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about which query will be to be run. `quickEvalPosition` and `quickEvalText`
|
||||
* is only filled in if the query is a quick query.
|
||||
* Validates that the specified URI represents a QL query, and returns the file system path to that
|
||||
* query.
|
||||
*
|
||||
* If `allowLibraryFiles` is set, ".qll" files will also be allowed as query files.
|
||||
*/
|
||||
export interface SelectedQuery {
|
||||
queryPath: string;
|
||||
quickEvalPosition?: messages.Position;
|
||||
quickEvalText?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which QL file to run during an invocation of `Run Query` or `Quick Evaluation`, as follows:
|
||||
* - If the command was called by clicking on a file, then use that file.
|
||||
* - Otherwise, use the file open in the current editor.
|
||||
* - In either case, prompt the user to save the file if it is open with unsaved changes.
|
||||
* - For `Quick Evaluation`, ensure the selected file is also the one open in the editor,
|
||||
* and use the selected region.
|
||||
* @param selectedResourceUri The selected resource when the command was run.
|
||||
* @param quickEval Whether the command being run is `Quick Evaluation`.
|
||||
*/
|
||||
export async function determineSelectedQuery(
|
||||
selectedResourceUri: Uri | undefined,
|
||||
quickEval: boolean,
|
||||
range?: Range,
|
||||
): Promise<SelectedQuery> {
|
||||
const editor = window.activeTextEditor;
|
||||
|
||||
// Choose which QL file to use.
|
||||
let queryUri: Uri;
|
||||
if (selectedResourceUri) {
|
||||
// A resource was passed to the command handler, so use it.
|
||||
queryUri = selectedResourceUri;
|
||||
} else {
|
||||
// No resource was passed to the command handler, so obtain it from the active editor.
|
||||
// This usually happens when the command is called from the Command Palette.
|
||||
if (editor === undefined) {
|
||||
throw new Error(
|
||||
"No query was selected. Please select a query and try again.",
|
||||
);
|
||||
} else {
|
||||
queryUri = editor.document.uri;
|
||||
}
|
||||
}
|
||||
|
||||
export function validateQueryUri(
|
||||
queryUri: Uri,
|
||||
allowLibraryFiles: boolean,
|
||||
): string {
|
||||
if (queryUri.scheme !== "file") {
|
||||
throw new Error("Can only run queries that are on disk.");
|
||||
}
|
||||
const queryPath = queryUri.fsPath;
|
||||
validateQueryPath(queryPath, allowLibraryFiles);
|
||||
return queryPath;
|
||||
}
|
||||
|
||||
if (quickEval) {
|
||||
/**
|
||||
* Validates that the specified path represents a QL query
|
||||
*
|
||||
* If `allowLibraryFiles` is set, ".qll" files will also be allowed as query files.
|
||||
*/
|
||||
export function validateQueryPath(
|
||||
queryPath: string,
|
||||
allowLibraryFiles: boolean,
|
||||
): void {
|
||||
if (allowLibraryFiles) {
|
||||
if (!(queryPath.endsWith(".ql") || queryPath.endsWith(".qll"))) {
|
||||
throw new Error(
|
||||
'The selected resource is not a CodeQL file; It should have the extension ".ql" or ".qll".',
|
||||
@@ -450,40 +428,52 @@ export async function determineSelectedQuery(
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Whether we chose the file from the active editor or from a context menu,
|
||||
// if the same file is open with unsaved changes in the active editor,
|
||||
// then prompt the user to save it first.
|
||||
if (editor !== undefined && editor.document.uri.fsPath === queryPath) {
|
||||
if (await promptUserToSaveChanges(editor.document)) {
|
||||
await editor.document.save();
|
||||
}
|
||||
export interface QuickEvalContext {
|
||||
quickEvalPosition: messages.Position;
|
||||
quickEvalText: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the selection to be used for quick evaluation.
|
||||
*
|
||||
* If `range` is specified, then that range will be used. Otherwise, the current selection will be
|
||||
* used.
|
||||
*/
|
||||
export async function getQuickEvalContext(
|
||||
range: Range | undefined,
|
||||
): Promise<QuickEvalContext> {
|
||||
const editor = window.activeTextEditor;
|
||||
if (editor === undefined) {
|
||||
throw new Error("Can't run quick evaluation without an active editor.");
|
||||
}
|
||||
// For Quick Evaluation, the selected position comes from the active editor, but it's possible
|
||||
// that query itself was a different file. We need to validate the path of the file we're using
|
||||
// for the QuickEval selection in case it was different.
|
||||
validateQueryUri(editor.document.uri, true);
|
||||
const quickEvalPosition = await getSelectedPosition(editor, range);
|
||||
let quickEvalText: string;
|
||||
if (!editor.selection?.isEmpty) {
|
||||
quickEvalText = editor.document.getText(editor.selection);
|
||||
} else {
|
||||
// capture the entire line if the user didn't select anything
|
||||
const line = editor.document.lineAt(editor.selection.active.line);
|
||||
quickEvalText = line.text.trim();
|
||||
}
|
||||
|
||||
let quickEvalPosition: messages.Position | undefined = undefined;
|
||||
let quickEvalText: string | undefined = undefined;
|
||||
if (quickEval) {
|
||||
if (editor === undefined) {
|
||||
throw new Error("Can't run quick evaluation without an active editor.");
|
||||
}
|
||||
if (editor.document.fileName !== queryPath) {
|
||||
// For Quick Evaluation we expect these to be the same.
|
||||
// Report an error if we end up in this (hopefully unlikely) situation.
|
||||
throw new Error(
|
||||
"The selected resource for quick evaluation should match the active editor.",
|
||||
);
|
||||
}
|
||||
quickEvalPosition = await getSelectedPosition(editor, range);
|
||||
if (!editor.selection?.isEmpty) {
|
||||
quickEvalText = editor.document.getText(editor.selection);
|
||||
} else {
|
||||
// capture the entire line if the user didn't select anything
|
||||
const line = editor.document.lineAt(editor.selection.active.line);
|
||||
quickEvalText = line.text.trim();
|
||||
}
|
||||
}
|
||||
return {
|
||||
quickEvalPosition,
|
||||
quickEvalText,
|
||||
};
|
||||
}
|
||||
|
||||
return { queryPath, quickEvalPosition, quickEvalText };
|
||||
/**
|
||||
* Information about which query will be to be run, optionally including a QuickEval selection.
|
||||
*/
|
||||
export interface SelectedQuery {
|
||||
queryPath: string;
|
||||
quickEval?: QuickEvalContext;
|
||||
}
|
||||
|
||||
/** Gets the selected position within the given editor. */
|
||||
@@ -512,7 +502,7 @@ async function getSelectedPosition(
|
||||
* @returns true if we should save changes and false if we should continue without saving changes.
|
||||
* @throws UserCancellationException if we should abort whatever operation triggered this prompt
|
||||
*/
|
||||
async function promptUserToSaveChanges(
|
||||
export async function promptUserToSaveChanges(
|
||||
document: TextDocument,
|
||||
): Promise<boolean> {
|
||||
if (document.isDirty) {
|
||||
@@ -526,7 +516,9 @@ async function promptUserToSaveChanges(
|
||||
isCloseAffordance: false,
|
||||
};
|
||||
const cancelItem = { title: "Cancel", isCloseAffordance: true };
|
||||
const message = "Query file has unsaved changes. Save now?";
|
||||
const message = `Query file '${basename(
|
||||
document.uri.fsPath,
|
||||
)}' has unsaved changes. Save now?`;
|
||||
const chosenItem = await window.showInformationMessage(
|
||||
message,
|
||||
{ modal: true },
|
||||
@@ -595,7 +587,7 @@ export async function createInitialQueryInfo(
|
||||
selectedQuery: SelectedQuery,
|
||||
databaseInfo: DatabaseInfo,
|
||||
): Promise<InitialQueryInfo> {
|
||||
const isQuickEval = selectedQuery.quickEvalPosition !== undefined;
|
||||
const isQuickEval = selectedQuery.quickEval !== undefined;
|
||||
return {
|
||||
queryPath: selectedQuery.queryPath,
|
||||
isQuickEval,
|
||||
@@ -603,10 +595,10 @@ export async function createInitialQueryInfo(
|
||||
databaseInfo,
|
||||
id: `${basename(selectedQuery.queryPath)}-${nanoid()}`,
|
||||
start: new Date(),
|
||||
...(isQuickEval
|
||||
...(selectedQuery.quickEval !== undefined
|
||||
? {
|
||||
queryText: selectedQuery.quickEvalText!, // if this query is quick eval, it must have quick eval text
|
||||
quickEvalPosition: selectedQuery.quickEvalPosition,
|
||||
queryText: selectedQuery.quickEval.quickEvalText,
|
||||
quickEvalPosition: selectedQuery.quickEval.quickEvalPosition,
|
||||
}
|
||||
: {
|
||||
queryText: await readFile(selectedQuery.queryPath, "utf8"),
|
||||
|
||||
Reference in New Issue
Block a user