From 75bdc89ae393f1da66d0de2b89f2f6d54d630256 Mon Sep 17 00:00:00 2001 From: Elie Richa Date: Thu, 27 Jun 2024 11:45:38 +0000 Subject: [PATCH] Provide SPARK CodeLenses on subprograms when gnatprove is on PATH --- .../vscode/ada/src/AdaCodeLensProvider.ts | 17 ++- integration/vscode/ada/src/ExtensionState.ts | 29 +++- integration/vscode/ada/src/commands.ts | 93 ++++++++++++- integration/vscode/ada/src/helpers.ts | 44 ++++++ integration/vscode/ada/src/taskProviders.ts | 58 +++----- .../ada/test/suite/general/codelens.test.ts | 110 ++++++++------- .../ada/test/suite/general/helpers.test.ts | 82 +++++++++++ integration/vscode/ada/test/suite/utils.ts | 128 ++++++++++++++++++ .../test/workspaces/general/src/symbols.adb | 30 ++++ 9 files changed, 483 insertions(+), 108 deletions(-) create mode 100644 integration/vscode/ada/test/suite/general/helpers.test.ts create mode 100644 integration/vscode/ada/test/workspaces/general/src/symbols.adb diff --git a/integration/vscode/ada/src/AdaCodeLensProvider.ts b/integration/vscode/ada/src/AdaCodeLensProvider.ts index c9c4df2bb..83db4209d 100644 --- a/integration/vscode/ada/src/AdaCodeLensProvider.ts +++ b/integration/vscode/ada/src/AdaCodeLensProvider.ts @@ -11,12 +11,10 @@ import { commands, Uri, } from 'vscode'; -import { CMD_BUILD_AND_DEBUG_MAIN, CMD_BUILD_AND_RUN_MAIN } from './commands'; -import { getMains, getSymbols } from './helpers'; +import { CMD_BUILD_AND_DEBUG_MAIN, CMD_BUILD_AND_RUN_MAIN, CMD_SPARK_PROVE_SUBP } from './commands'; +import { envHasExec, getMains, getSymbols } from './helpers'; export class AdaCodeLensProvider implements CodeLensProvider { - static readonly ENABLE_SPARK_CODELENS = false; - onDidChangeCodeLenses?: Event | undefined; provideCodeLenses( document: TextDocument, @@ -73,7 +71,7 @@ export class AdaCodeLensProvider implements CodeLensProvider { }); let res2; - if (AdaCodeLensProvider.ENABLE_SPARK_CODELENS) { + if (envHasExec('gnatprove')) { /** * This is tentative deactivated code in preparation of SPARK support. */ @@ -88,10 +86,11 @@ export class AdaCodeLensProvider implements CodeLensProvider { if (token?.isCancellationRequested) { throw new CancellationError(); } - // TODO make SPARK codelenses conditional to the availability of SPARK on PATH - return new CodeLens(f.range, { - title: '$(play-circle) Prove', - command: 'TODO', + + return new CodeLens(f.selectionRange, { + title: '$(check) Prove', + command: CMD_SPARK_PROVE_SUBP, + arguments: [document.uri, f.selectionRange], }); }); }); diff --git a/integration/vscode/ada/src/ExtensionState.ts b/integration/vscode/ada/src/ExtensionState.ts index 29aeee947..905b66a6b 100644 --- a/integration/vscode/ada/src/ExtensionState.ts +++ b/integration/vscode/ada/src/ExtensionState.ts @@ -3,12 +3,18 @@ import { Disposable, ExecuteCommandRequest, LanguageClient } from 'vscode-langua import { AdaCodeLensProvider } from './AdaCodeLensProvider'; import { createClient } from './clients'; import { AdaInitialDebugConfigProvider, initializeDebugging } from './debugConfigProvider'; +import { logger } from './extension'; import { GnatTaskProvider } from './gnatTaskProvider'; import { initializeTesting } from './gnattest'; import { GprTaskProvider } from './gprTaskProvider'; import { TERMINAL_ENV_SETTING_NAME } from './helpers'; -import { registerTaskProviders } from './taskProviders'; -import { logger } from './extension'; +import { + SimpleTaskProvider, + TASK_TYPE_ADA, + TASK_TYPE_SPARK, + createAdaTaskProvider, + createSparkTaskProvider, +} from './taskProviders'; /** * This class encapsulates all state that should be maintained throughout the @@ -45,6 +51,9 @@ export class ExtensionState { cachedExecutables: string[] | undefined; cachedAlireTomls: vscode.Uri[] | undefined; + private adaTaskProvider?: SimpleTaskProvider; + private sparkTaskProvider?: SimpleTaskProvider; + public clearALSCache() { this.cachedProjectFile = undefined; this.cachedObjectDir = undefined; @@ -89,13 +98,18 @@ export class ExtensionState { }; public registerTaskProviders = (): void => { + this.adaTaskProvider = createAdaTaskProvider(); + this.sparkTaskProvider = createSparkTaskProvider(); + this.registeredTaskProviders = [ vscode.tasks.registerTaskProvider(GnatTaskProvider.gnatType, new GnatTaskProvider()), vscode.tasks.registerTaskProvider( GprTaskProvider.gprTaskType, new GprTaskProvider(this.adaClient) ), - ].concat(registerTaskProviders()); + vscode.tasks.registerTaskProvider(TASK_TYPE_ADA, this.adaTaskProvider), + vscode.tasks.registerTaskProvider(TASK_TYPE_SPARK, this.sparkTaskProvider), + ]; }; public unregisterTaskProviders = (): void => { @@ -215,4 +229,13 @@ export class ExtensionState { return this.cachedAlireTomls; } + + /** + * + * @returns the SPARK task provider which can be useful for resolving tasks + * created on the fly, e.g. when running SPARK CodeLenses. + */ + public getSparkTaskProvider() { + return this.sparkTaskProvider; + } } diff --git a/integration/vscode/ada/src/commands.ts b/integration/vscode/ada/src/commands.ts index 4c4980b0e..16947078d 100644 --- a/integration/vscode/ada/src/commands.ts +++ b/integration/vscode/ada/src/commands.ts @@ -1,5 +1,6 @@ import assert from 'assert'; import { existsSync } from 'fs'; +import { basename } from 'path'; import * as vscode from 'vscode'; import { SymbolKind } from 'vscode'; import { Disposable } from 'vscode-jsonrpc'; @@ -10,10 +11,13 @@ import { adaExtState, logger, mainOutputChannel } from './extension'; import { findAdaMain, getProjectFileRelPath, getSymbols } from './helpers'; import { SimpleTaskDef, + TASK_PROVE_SUPB_PLAIN_NAME, + TASK_TYPE_SPARK, findBuildAndRunTask, getBuildAndRunTasks, getConventionalTaskLabel, isFromWorkspace, + workspaceTasksFirst, } from './taskProviders'; /** @@ -50,6 +54,7 @@ export const CMD_GET_PROJECT_FILE = 'ada.getProjectFile'; export const CMD_SPARK_LIMIT_SUBP_ARG = 'ada.spark.limitSubpArg'; export const CMD_SPARK_LIMIT_REGION_ARG = 'ada.spark.limitRegionArg'; +export const CMD_SPARK_PROVE_SUBP = 'ada.spark.proveSubprogram'; export function registerCommands(context: vscode.ExtensionContext, clients: ExtensionState) { context.subscriptions.push(vscode.commands.registerCommand('ada.otherFile', otherFileHandler)); @@ -125,6 +130,9 @@ export function registerCommands(context: vscode.ExtensionContext, clients: Exte context.subscriptions.push( vscode.commands.registerCommand(CMD_SPARK_LIMIT_REGION_ARG, sparkLimitRegionArg) ); + context.subscriptions.push( + vscode.commands.registerCommand(CMD_SPARK_PROVE_SUBP, sparkProveSubprogram) + ); } /** * Add a subprogram box above the subprogram enclosing the cursor's position, if any. @@ -640,8 +648,8 @@ export async function sparkLimitSubpArg(): Promise { return getEnclosingSymbol(vscode.window.activeTextEditor, [vscode.SymbolKind.Function]).then( (Symbol) => { if (Symbol) { - const subprogram_line: string = (Symbol.range.start.line + 1).toString(); - return [`--limit-subp=\${fileBasename}:${subprogram_line}`]; + const range = Symbol.range; + return [getLimitSubpArg('${fileBasename}', range)]; } else { /** * If we can't find a subprogram, we use the VS Code predefined @@ -658,6 +666,19 @@ export async function sparkLimitSubpArg(): Promise { ); } +/** + * + * @param filename - a filename + * @param range - a {@link vscode.Range} + * @returns the --limit-subp `gnatprove` CLI argument corresponding to the given + * arguments. Note that lines and columns in {@link vscode.Range}es are + * zero-based while the `gnatprove` convention is one-based. This function does + * the conversion. + */ +function getLimitSubpArg(filename: string, range: vscode.Range) { + return `--limit-subp=${filename}:${range.start.line + 1}`; +} + /** * @returns the gnatprove `--limit-region=file:from:to` argument corresponding * to the current editor's selection. @@ -731,3 +752,71 @@ export async function getEnclosingSymbol( return null; } + +/** + * Command corresponding to the 'Prove' CodeLens provided on subprograms. + * + * It is implemented by fetching the 'Prove subbprogram' task and using it as a + * template such that the User can customize the task to impact the CodeLens. + */ +async function sparkProveSubprogram( + uri: vscode.Uri, + range: vscode.Range +): Promise { + /** + * Get the 'Prove subprogram' task. Prioritize workspace tasks so that User + * customization of the task takes precedence. + */ + const task = (await vscode.tasks.fetchTasks({ type: TASK_TYPE_SPARK })) + .sort(workspaceTasksFirst) + .find( + (t) => + getConventionalTaskLabel(t) == `${TASK_TYPE_SPARK}: ${TASK_PROVE_SUPB_PLAIN_NAME}` + ); + assert(task); + + /** + * Create a copy of the task. + */ + const newTask = new vscode.Task( + { ...task.definition }, + task.scope ?? vscode.TaskScope.Workspace, + task.name, + task.source, + undefined, + task.problemMatchers + ); + + /** + * Replace the subp-region argument based on the parameter given to the + * command. + */ + const taskDef = newTask.definition as SimpleTaskDef; + assert(taskDef.args); + const regionArg = '${command:ada.spark.limitSubpArg}'; + const regionArgIdx = taskDef.args.findIndex((arg) => arg == regionArg); + if (regionArgIdx >= 0) { + const fileBasename = basename(uri.fsPath); + taskDef.args[regionArgIdx] = getLimitSubpArg(fileBasename, range); + /** + * Change the task name accordingly, otherwise all invocations appear + * with the same name in the task history. + */ + newTask.name = `${task.name} - ${fileBasename}:${range.start.line + 1}`; + } else { + throw Error( + `Task '${getConventionalTaskLabel(task)}' is missing a '${regionArg}' argument` + ); + } + + /** + * Resolve the task. + */ + const resolvedTask = await adaExtState.getSparkTaskProvider()?.resolveTask(newTask); + assert(resolvedTask); + + /** + * Execute the task. + */ + return await vscode.tasks.executeTask(resolvedTask); +} diff --git a/integration/vscode/ada/src/helpers.ts b/integration/vscode/ada/src/helpers.ts index f13355135..74bdfe4eb 100644 --- a/integration/vscode/ada/src/helpers.ts +++ b/integration/vscode/ada/src/helpers.ts @@ -24,6 +24,7 @@ import winston from 'winston'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { ExtensionState } from './ExtensionState'; import { adaExtState, logger } from './extension'; +import { existsSync } from 'fs'; /** * Substitue any variable reference present in the given string. VS Code @@ -436,3 +437,46 @@ export function getSymbols( export function escapeRegExp(text: string) { return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); } + +/** + * + * @param execName - the name of an executable (without the extension) to find + * using the PATH environment variable. + * @returns `true` if the executable is found, `false` otherwise. + */ +export function envHasExec(execName: string): boolean { + const exePath: string | undefined = which(execName); + + return exePath != undefined; +} + +/** + * Finds the path to an executable using the PATH environment variable. + * + * On Windows, the extension of the executable does not need to be provided. The + * env variable PATHEXT is used to consider all applicable extensions (e.g. + * .exe, .cmd). + * + * @param execName - name of executable to find using PATH environment variable. + * @returns the full path to the executable if found, otherwise `undefined` + */ +export function which(execName: string) { + const env = { ...process.env }; + setTerminalEnvironment(env); + const pathVal = env.PATH; + const paths = pathVal?.split(path.delimiter); + const exeExtensions = + process.platform == 'win32' + ? /** + * On Windows use a default list of extensions in case PATHEXT is + * not set. + */ + env.PATHEXT?.split(path.delimiter) ?? ['.exe', '.cmd', '.bat'] + : ['']; + + const exePath: string | undefined = paths + ?.flatMap((p) => exeExtensions.map((ext) => path.join(p, execName + ext))) + .find(existsSync); + + return exePath; +} diff --git a/integration/vscode/ada/src/taskProviders.ts b/integration/vscode/ada/src/taskProviders.ts index 5768bf2db..180ed606f 100644 --- a/integration/vscode/ada/src/taskProviders.ts +++ b/integration/vscode/ada/src/taskProviders.ts @@ -84,6 +84,8 @@ const TASK_CLEAN_PROJECT = { taskGroup: vscode.TaskGroup.Clean, }; +export const TASK_PROVE_SUPB_PLAIN_NAME = 'Prove subprogram'; + /** * Predefined tasks offered by the extension. Both 'ada' and 'spark' tasks are * included in this array. They are later on split and provided by different @@ -301,7 +303,7 @@ const predefinedTasks: PredefinedTask[] = [ problemMatchers: DEFAULT_PROBLEM_MATCHER, }, { - label: 'Prove subprogram', + label: TASK_PROVE_SUPB_PLAIN_NAME, taskDef: { type: TASK_TYPE_SPARK, command: 'gnatprove', @@ -440,38 +442,12 @@ export class SimpleTaskProvider implements vscode.TaskProvider { throw new vscode.CancellationError(); } - let execution = undefined; - if (tDecl.taskDef.compound) { - /** - * It's a compound task. - */ - execution = new SequentialExecutionByName(tDecl.label, tDecl.taskDef.compound); - } else { - /** - * It's a shell invocation task. - */ - assert(tDecl.taskDef.command); - assert(tDecl.taskDef.args); - - /** - * Ideally we would have liked to provide unresolved tasks and - * let resolving only happen in the resolveTask method, but - * that's not how VS Code works. This method is expected to - * return fully resolved tasks, hence we must provide an - * execution object with arguments evaluated now. - */ - execution = new vscode.ShellExecution( - tDecl.taskDef.command, - await evaluateArgs(tDecl.taskDef.args) - ); - } - const task = new vscode.Task( tDecl.taskDef, vscode.TaskScope.Workspace, tDecl.label, tDecl.taskDef.type, - execution, + undefined, tDecl.problemMatchers ); @@ -479,7 +455,16 @@ export class SimpleTaskProvider implements vscode.TaskProvider { task.group = tDecl.taskGroup; } - result.push(task); + /** + * Ideally we would have liked to provide unresolved tasks and let + * resolving only happen in the resolveTask method, but that's not + * how VS Code works. This method is expected to return fully + * resolved tasks, hence we must resolve pre-defined tasks here. + */ + const resolvedTask = await this.resolveTask(task, token); + assert(resolvedTask); + + result.push(resolvedTask); } return result; @@ -512,10 +497,16 @@ export class SimpleTaskProvider implements vscode.TaskProvider { */ let execution; if (taskDef.compound) { + /** + * It's a compound task. + */ assert(!taskDef.command); assert(!taskDef.args); execution = new SequentialExecutionByName(task.name, taskDef.compound); } else { + /** + * It's a shell invocation task. + */ assert(taskDef.command); /** * We support working with just the command property, in which case @@ -699,13 +690,6 @@ function isEmptyArray(obj: unknown): boolean { return false; } -export function registerTaskProviders() { - return [ - vscode.tasks.registerTaskProvider(TASK_TYPE_ADA, createAdaTaskProvider()), - vscode.tasks.registerTaskProvider(TASK_TYPE_SPARK, createSparkTaskProvider()), - ]; -} - /** * The name of the build task of a main, without the task type. */ @@ -970,7 +954,7 @@ function runTaskSequence( /** * A sorting function that puts tasks defined by the User in the workspace first. */ -const workspaceTasksFirst = (a: vscode.Task, b: vscode.Task): number => { +export const workspaceTasksFirst = (a: vscode.Task, b: vscode.Task): number => { if (a.source == b.source) { return a.name.localeCompare(b.name); } else if (isFromWorkspace(a)) { diff --git a/integration/vscode/ada/test/suite/general/codelens.test.ts b/integration/vscode/ada/test/suite/general/codelens.test.ts index b2b6c34d8..9617ea707 100644 --- a/integration/vscode/ada/test/suite/general/codelens.test.ts +++ b/integration/vscode/ada/test/suite/general/codelens.test.ts @@ -1,7 +1,14 @@ import assert from 'assert'; -import { Uri, window, workspace } from 'vscode'; -import { adaExtState } from '../../../src/extension'; -import { activate, closeAllEditors } from '../utils'; +import { DocumentSymbol, SymbolKind, commands } from 'vscode'; +import { envHasExec, getSymbols } from '../../../src/helpers'; +import { + activate, + closeAllEditors, + codeLensesToString, + getCodeLenses, + showTextDocument, + simplifyCodelenses, +} from '../utils'; suite('CodeLens', function () { this.beforeAll(async () => { @@ -13,47 +20,30 @@ suite('CodeLens', function () { }); test('in main file offer run & debug', async () => { - assert(workspace.workspaceFolders !== undefined); - const rootUri = workspace.workspaceFolders[0].uri; - const mainUri = Uri.joinPath(rootUri, 'src', 'main1.adb'); - const textEditor = await window.showTextDocument(mainUri); - const codelenses = await adaExtState.codelensProvider.provideCodeLenses( - textEditor.document - ); - assert.deepEqual( - /** - * Check a subset of the fields in CodeLenses - */ - codelenses?.map((v) => ({ - ...v.command, - // The argument is expected to be a Uri, in which case use the - // normalized fsPath for comparison with the expected result - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - arguments: v.command?.arguments?.map((a) => (a instanceof Uri ? a.fsPath : a)), - })), - [ - { - command: 'ada.buildAndRunMain', + const codelenses = await getCodeLenses('src', 'main1.adb'); + assert.deepEqual(simplifyCodelenses(codelenses), [ + { + range: '9:0 -> 15:9', + command: { title: '$(run) Run', - arguments: [mainUri.fsPath], + command: 'ada.buildAndRunMain', + arguments: ['src/main1.adb'], }, - { - command: 'ada.buildAndDebugMain', + }, + { + range: '9:0 -> 15:9', + command: { title: '$(debug-alt-small) Debug', - arguments: [mainUri.fsPath], + command: 'ada.buildAndDebugMain', + arguments: ['src/main1.adb'], }, - ] - ); + }, + ]); }); test("in non-main file don't offer run & debug", async () => { - assert(workspace.workspaceFolders !== undefined); - const rootUri = workspace.workspaceFolders[0].uri; - const mainUri = Uri.joinPath(rootUri, 'src', 'foo.ads'); - const textEditor = await window.showTextDocument(mainUri); - const codelenses = await adaExtState.codelensProvider.provideCodeLenses( - textEditor.document - ); + const srcRelPath = ['src', 'foo.ads']; + const codelenses = await getCodeLenses(...srcRelPath); if (codelenses) { /** @@ -70,24 +60,30 @@ suite('CodeLens', function () { ); } }); -}); -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function toString(codelenses: import('vscode').CodeLens[] | null | undefined): unknown { - return JSON.stringify( - codelenses?.map((cl) => ({ - command: cl.command?.command ?? '', - title: cl.command?.title ?? '', - arguments: cl.command?.arguments?.map((o) => - /** - * If the argument is a URI, render it as a relative - * path. Otherwise, keep the object as is. - */ - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - o instanceof Uri ? workspace.asRelativePath(o) : o - ), - })), - null, - 2 - ); -} + test('no SPARK codelenses in SPARK-less env', async function () { + assert(!envHasExec('gnatprove'), 'This test must run in an env without gnatprove'); + + const srcRelPath = ['src', 'bar.ads']; + /** + * Check that the test file contains subprograms. + */ + const textEditor = await showTextDocument(...srcRelPath); + const symbols = await commands.executeCommand( + 'vscode.executeDocumentSymbolProvider', + textEditor.document.uri + ); + assert(getSymbols(symbols, [SymbolKind.Function]).length > 0); + + const codelenses = await getCodeLenses(...srcRelPath); + if (codelenses) { + /** + * Assert that SPARK codelenses were not provided for the subprograms + */ + assert( + !codeLensesToString(codelenses).toLowerCase().includes('prove'), + `CodeLense for SPARK was unexpectedly provided:\n${codeLensesToString(codelenses)}` + ); + } + }); +}); diff --git a/integration/vscode/ada/test/suite/general/helpers.test.ts b/integration/vscode/ada/test/suite/general/helpers.test.ts new file mode 100644 index 000000000..5afcdc76c --- /dev/null +++ b/integration/vscode/ada/test/suite/general/helpers.test.ts @@ -0,0 +1,82 @@ +import assert from 'assert'; +import { envHasExec, getSymbols, which } from '../../../src/helpers'; +import { DocumentSymbol, SymbolKind, Uri, commands, workspace } from 'vscode'; +import { rangeToStr } from '../utils'; + +suite('which and envHasExec', function () { + test('existing', function () { + switch (process.platform) { + case 'win32': + assert(which('where')?.endsWith('where.exe')); + assert(envHasExec('where')); + break; + + default: + assert(which('sh')?.endsWith('/sh')); + assert(envHasExec('sh')); + break; + } + }); + test('non-existing', function () { + assert.equal(which('some-non-existing-exec'), undefined); + assert(!envHasExec('some-non-existing-exec')); + }); +}); + +suite('getSymbols', function () { + let symbols: DocumentSymbol[]; + + this.beforeAll(async function () { + assert(workspace.workspaceFolders); + const uri = Uri.joinPath(workspace.workspaceFolders[0].uri, 'src', 'symbols.adb'); + symbols = await commands.executeCommand( + 'vscode.executeDocumentSymbolProvider', + uri + ); + }); + + test('Root symbols', function () { + assert.deepEqual(simplifySymbols(symbols), [ + { range: '0:0 -> 0:9', kind: 'Namespace', name: 'With clauses' }, + { range: '2:0 -> 25:12', kind: 'Module', name: 'Symbols' }, + ]); + }); + + test('getSymbols default recursion args', function () { + assert.deepEqual(simplifySymbols(getSymbols(symbols, [SymbolKind.Function])), [ + { range: '4:3 -> 23:12', kind: 'Function', name: 'Test' }, + { range: '6:6 -> 17:13', kind: 'Function', name: 'P1' }, + { range: '7:9 -> 14:16', kind: 'Function', name: 'P2' }, + { range: '8:12 -> 11:19', kind: 'Function', name: 'P3' }, + ]); + }); + + test('getSymbols only recurse Module', function () { + assert.deepEqual( + simplifySymbols(getSymbols(symbols, [SymbolKind.Function], [SymbolKind.Module])), + [{ range: '4:3 -> 23:12', kind: 'Function', name: 'Test' }] + ); + }); + + /** + * A simplified type with a subset of DocumentSymbol properties intended for + * checking test results. + */ + type SimpleDocumentSymbol = { + range: string; + kind: string; + name: string; + }; + + function simplifySymbols(s: DocumentSymbol[]): SimpleDocumentSymbol[] { + return s.map(simplifySymbol); + } + + function simplifySymbol(s: DocumentSymbol): SimpleDocumentSymbol { + return { + range: rangeToStr(s.range), + kind: SymbolKind[s.kind], + name: s.name, + }; + } +}); diff --git a/integration/vscode/ada/test/suite/utils.ts b/integration/vscode/ada/test/suite/utils.ts index 3fdca3b5b..bbff86a9e 100644 --- a/integration/vscode/ada/test/suite/utils.ts +++ b/integration/vscode/ada/test/suite/utils.ts @@ -1,7 +1,10 @@ import assert from 'assert'; import { spawnSync } from 'child_process'; import { existsSync, readFileSync, writeFileSync } from 'fs'; +import path from 'path'; import * as vscode from 'vscode'; +import { CodeLens, Uri, window, workspace } from 'vscode'; +import { adaExtState } from '../../src/extension'; import { setTerminalEnvironment } from '../../src/helpers'; import { SimpleTaskProvider, @@ -36,6 +39,11 @@ export function assertEqualToFileContent(actual: string, expectedUri: vscode.Uri } } +/** + * + * Normalize line endings in the given string to the `\n` or to `lineEnding` if + * given. + */ export function normalizeLineEndings(str: string, lineEnding = '\n'): string { return str.replace(/\r?\n/g, lineEnding); } @@ -66,6 +74,14 @@ export async function activate(): Promise { */ await ext.activate(); } + +/** + * + * @param prov - a TaskProvider + * @returns a string representation of the subset of tasks offered by the + * provider that are based on a ShellExecution. The string includes the command + * line of each task. + */ export async function getCommandLines(prov: SimpleTaskProvider) { const tasks = await prov.provideTasks(); assert(tasks); @@ -87,6 +103,15 @@ export async function getCommandLines(prov: SimpleTaskProvider) { .join('\n'); return actualCommandLines; } + +/** + * Execute the given task, wait until it finishes and return the underlying + * process exit code. + * + * @param task - a {@link vscode.Task} + * @returns a Promise that resolves to the underlying process exit code when the + * task finishes execution. + */ export async function runTaskAndGetResult(task: vscode.Task): Promise { return await new Promise((resolve, reject) => { let started = false; @@ -137,6 +162,12 @@ export async function runTaskAndGetResult(task: vscode.Task): Promise, @@ -199,8 +238,97 @@ export async function testTask( } } +/** + * Call the `workbench.action.closeActiveEditor` command to close all open editors. + */ export async function closeAllEditors() { while (vscode.window.activeTextEditor) { await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); } } + +/** + * + * @param srcRelPath - a path to a source file relative to the workspace root + * @returns a text editor opened for the source file. + */ +export async function showTextDocument(...srcRelPath: string[]) { + const mainUri = getWsUri(...srcRelPath); + const textEditor = await window.showTextDocument(mainUri); + return textEditor; +} + +/** + * + * @param srcRelPath - a path relative to the workspace root + * @returns a {@link vscode.Uri} representing the given path. + */ +export function getWsUri(...srcRelPath: string[]) { + assert(workspace.workspaceFolders !== undefined); + const rootUri = workspace.workspaceFolders[0].uri; + const uri = Uri.joinPath(rootUri, ...srcRelPath); + return uri; +} + +/** + * Opens the given source file in an editor and returns the CodeLenses provided + * for that file. + * + * @param srcRelPath - relative path of a source file in the workspace + * @returns array of CodeLenses provided for that source file. + */ +export async function getCodeLenses(...srcRelPath: string[]) { + const textEditor = await showTextDocument(...srcRelPath); + const codelenses = await adaExtState.codelensProvider.provideCodeLenses(textEditor.document); + return codelenses ?? []; +} + +/** + * A testing utility to simplify CodeLenses for convenient comparison with + * expected results. This selects a subset of properties of CodeLenses and + * converts Uris to relative Posix paths and ranges to a convenient string + * representation. + */ +export function simplifyCodelenses(cls: CodeLens[]) { + return cls.map((cl) => ({ + range: rangeToStr(cl.range), + command: { + title: cl.command?.title, + command: cl.command?.command, + arguments: cl.command?.arguments?.map((a) => + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + a instanceof Uri + ? /** + * Normalize URI to a relative Posix path. + */ + workspace.asRelativePath(a.fsPath).split(path.sep).join(path.posix.sep) + : a instanceof vscode.Range + ? /** + * Represent Ranges as a string + */ + rangeToStr(a) + : a + ), + }, + })); +} + +/** + * + * @param codelenses - array of CodeLens + * @returns a JSON string representation of the given CodeLenses. + */ +export function codeLensesToString(codelenses: CodeLens[]): string { + return JSON.stringify(simplifyCodelenses(codelenses), null, 2); +} + +/** + * + * @param range - a {@link vscode.Range} + * @returns a string representation of the range, convenient for comparison to + * references in testing. + */ +export function rangeToStr(range: vscode.Range): string { + // eslint-disable-next-line max-len + return `${range.start.line}:${range.start.character} -> ${range.end.line}:${range.end.character}`; +} diff --git a/integration/vscode/ada/test/workspaces/general/src/symbols.adb b/integration/vscode/ada/test/workspaces/general/src/symbols.adb new file mode 100644 index 000000000..5ff946223 --- /dev/null +++ b/integration/vscode/ada/test/workspaces/general/src/symbols.adb @@ -0,0 +1,30 @@ +with Foo; + +package body Symbols is + + procedure Test is + + procedure P1 is + procedure P2 is + procedure P3 is + begin + null; + end P3; + begin + null; + end P2; + begin + null; + end P1; + + begin + + null; + + end Test; + +end Symbols; + +-- This file is intended for testing the getSymbols helper function. See +-- helpers.test.ts. This comment is provided at the end to avoid disturbing +-- symbol locations above.