Skip to content
2 changes: 2 additions & 0 deletions extensions/ql-vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## [UNRELEASED]

- Add a CodeLens to make the Quick Evaluation command more accessible. Click the `Quick Evaluation` prompt above a predicate definition in the editor to evaluate that predicate on its own. [#1035](https://github.com/github/vscode-codeql/pull/1035)

## 1.5.8 - 2 December 2021

- Emit a more explicit error message when a user tries to add a database with an unzipped source folder to the workspace. [#1021](https://github.com/github/vscode-codeql/pull/1021)
Expand Down
29 changes: 27 additions & 2 deletions extensions/ql-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
window as Window,
env,
window,
QuickPickItem
QuickPickItem,
Range
} from 'vscode';
import { LanguageClient } from 'vscode-languageclient';
import * as os from 'os';
Expand All @@ -21,6 +22,7 @@ import { testExplorerExtensionId, TestHub } from 'vscode-test-adapter-api';

import { AstViewer } from './astViewer';
import * as archiveFilesystemProvider from './archive-filesystem-provider';
import QuickEvalCodeLensProvider from './quickEvalCodeLensProvider';
import { CodeQLCliServer, CliVersionConstraint } from './cli';
import {
CliConfigListener,
Expand Down Expand Up @@ -156,6 +158,7 @@ export interface CodeQLExtensionInterface {
* @returns CodeQLExtensionInterface
*/
export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionInterface | Record<string, never>> {

void logger.log(`Starting ${extensionId} extension`);
if (extension === undefined) {
throw new Error(`Can't find extension ${extensionId}`);
Expand All @@ -166,6 +169,9 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
await initializeTelemetry(extension, ctx);
languageSupport.install();

const codelensProvider = new QuickEvalCodeLensProvider();
languages.registerCodeLensProvider({ scheme: 'file', language: 'ql' }, codelensProvider);

ctx.subscriptions.push(distributionConfigListener);
const codeQlVersionRange = DEFAULT_DISTRIBUTION_VERSION_RANGE;
const distributionManager = new DistributionManager(distributionConfigListener, codeQlVersionRange, ctx);
Expand Down Expand Up @@ -471,6 +477,7 @@ async function activateWithInstalledDistribution(
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
Expand All @@ -485,7 +492,9 @@ async function activateWithInstalledDistribution(
quickEval,
selectedQuery,
progress,
token
token,
undefined,
range
);
const item = qhm.buildCompletedQuery(info);
await showResultsForCompletedQuery(item, WebviewReveal.NotForced);
Expand Down Expand Up @@ -733,6 +742,22 @@ async function activateWithInstalledDistribution(
cancellable: true
})
);

ctx.subscriptions.push(
commandRunnerWithProgress(
'codeQL.codeLensQuickEval',
async (
progress: ProgressCallback,
token: CancellationToken,
uri: Uri,
range: Range
) => await compileAndRunQuery(true, uri, progress, token, undefined, range),
{
title: 'Running query',
cancellable: true
})
);

ctx.subscriptions.push(
commandRunnerWithProgress('codeQL.quickQuery', async (
progress: ProgressCallback,
Expand Down
39 changes: 39 additions & 0 deletions extensions/ql-vscode/src/quickEvalCodeLensProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
CodeLensProvider,
TextDocument,
CodeLens,
Command,
Range
} from 'vscode';

class QuickEvalCodeLensProvider implements CodeLensProvider {
async provideCodeLenses(document: TextDocument): Promise<CodeLens[]> {

const codeLenses: CodeLens[] = [];

for (let index = 0; index < document.lineCount; index++) {
const textLine = document.lineAt(index);
// Match a predicate signature, including predicate name, parameter list, and opening brace.
const regex = new RegExp(/(\w+)\s*\(\s*.*(?:,\s*)*\)\s*\{/);
const matches = textLine.text.match(regex);

if (matches) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure that commented code removes the code lens. It's too tricky to do in a single regex, so I think doing another check is ok.

Suggested change
if (matches) {
if (matches && textLine.text.search(/^\s*\//)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had an illuminating discussion offline about why this works (search returns 0 when it matches the start of the line and -1 on mismatch, which JS converts into false and true respectively). I suggested a slightly more verbose but arguably simpler use of test instead -- hopefully that's still captured the meaning you suggested here!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A downside with the suggestion is that the regex is evaluated twice for every line, even if there is no initial match. The original suggestion only ran the second regex if the first one matched. I'm not sure if the extra overhead would be noticeable, but this code is run after every file change (in a background thread).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. We can keep using !test rather than search, but keep it inline instead of in its own variable.
So if (matches && /.../.test(textLine.text)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep....that makes sense.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made this change. Sorry it took a bit. I forgot I had resolved the CHANGELOG conflict from here but I hadn't updated my local branch yet. Had to get that squared away. Does it look better?

const range: Range = new Range(
textLine.range.start.line, matches.index!,
textLine.range.end.line, matches.index! + 1
);

const command: Command = {
command: 'codeQL.codeLensQuickEval',
title: `Quick Evaluation: ${matches[1]}`,
arguments: [document.uri, range]
};
const codeLens = new CodeLens(range, command);
codeLenses.push(codeLens);
}
}
return codeLenses;
}
}

export default QuickEvalCodeLensProvider;
31 changes: 17 additions & 14 deletions extensions/ql-vscode/src/run-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as tmp from 'tmp-promise';
import {
CancellationToken,
ConfigurationTarget,
Range,
TextDocument,
TextEditor,
Uri,
Expand Down Expand Up @@ -332,17 +333,18 @@ async function convertToQlPath(filePath: string): Promise<string> {


/** Gets the selected position within the given editor. */
async function getSelectedPosition(editor: TextEditor): Promise<messages.Position> {
const pos = editor.selection.start;
const posEnd = editor.selection.end;
// Convert from 0-based to 1-based line and column numbers.
return {
fileName: await convertToQlPath(editor.document.fileName),
line: pos.line + 1,
column: pos.character + 1,
endLine: posEnd.line + 1,
endColumn: posEnd.character + 1
};
async function getSelectedPosition(editor: TextEditor, range?: Range): Promise<messages.Position> {
const selectedRange = range || editor.selection;
const pos = selectedRange.start;
const posEnd = selectedRange.end;
// Convert from 0-based to 1-based line and column numbers.
return {
fileName: await convertToQlPath(editor.document.fileName),
line: pos.line + 1,
column: pos.character + 1,
endLine: posEnd.line + 1,
endColumn: posEnd.character + 1
};
}

/**
Expand Down Expand Up @@ -490,7 +492,7 @@ type SelectedQuery = {
* @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): Promise<SelectedQuery> {
export async function determineSelectedQuery(selectedResourceUri: Uri | undefined, quickEval: boolean, range?: Range): Promise<SelectedQuery> {
const editor = window.activeTextEditor;

// Choose which QL file to use.
Expand Down Expand Up @@ -544,7 +546,7 @@ export async function determineSelectedQuery(selectedResourceUri: Uri | undefine
// 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);
quickEvalPosition = await getSelectedPosition(editor, range);
quickEvalText = editor.document.getText(editor.selection);
}

Expand All @@ -560,13 +562,14 @@ export async function compileAndRunQueryAgainstDatabase(
progress: ProgressCallback,
token: CancellationToken,
templates?: messages.TemplateDefinitions,
range?: Range
): Promise<QueryWithResults> {
if (!db.contents || !db.contents.dbSchemeUri) {
throw new Error(`Database ${db.databaseUri} does not have a CodeQL database scheme.`);
}

// Determine which query to run, based on the selection and the active editor.
const { queryPath, quickEvalPosition, quickEvalText } = await determineSelectedQuery(selectedQueryUri, quickEval);
const { queryPath, quickEvalPosition, quickEvalText } = await determineSelectedQuery(selectedQueryUri, quickEval, range);

const historyItemOptions: QueryHistoryItemOptions = {};
historyItemOptions.isQuickQuery === isQuickQueryPath(queryPath);
Expand Down