diff --git a/.vscode/settings.json.tmpl b/.vscode/settings.json.tmpl index 9abce6387..b9d4e5b89 100644 --- a/.vscode/settings.json.tmpl +++ b/.vscode/settings.json.tmpl @@ -78,8 +78,10 @@ // Allow GitLab-specific Yaml tags in .gitlab-ci.yml "yaml.customTags": ["!reference sequence"], "triggerTaskOnSave.tasks": { - "ada: Compile current file": ["*.adb"], - "ada: Check current file": ["*.ads"] + // To work with automatically provided tasks, they + // must be provided without the `ada: ` prefix. + "Compile current file": ["*.adb"], + "Check current file": ["*.ads"] }, "triggerTaskOnSave.restart": true, "files.watcherExclude": { @@ -89,16 +91,23 @@ "**/.hg/store/**": true, ".obj/": true }, - "extension-test-runner.extractSettings": { - "suite": ["describe", "suite"], - "test": ["it", "test"], - "extractWith": "syntax" - }, + // The Extension Test Runner extension loads VS Code Mocha tests into the GUI + // by evaluating the JS test sources. If that fails, the following snippet + // switches it to a syntactic extraction which might will likely miss + // dynamically defined tests but might crashes in test loading. + // "extension-test-runner.extractSettings": { + // "suite": ["describe", "suite"], + // "test": ["it", "test"], + // "extractWith": "syntax" + // }, "extension-test-runner.debugOptions": { "outFiles": [ "${workspaceFolder}/integration/vscode/ada/out/**/*.js", "!**/node_modules/**" ], + "env": { + "MOCHA_TIMEOUT": "0" + }, "preLaunchTask": "npm: watch - integration/vscode/ada" } } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 4e896d59e..dbe3d7810 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -8,16 +8,6 @@ "problemMatcher": ["$ada"], "group": "test" }, - { - "type": "ada", - "configuration": { - "kind": "checkFile", - "projectFile": "${config:ada.projectFile}" - }, - "problemMatcher": ["$ada"], - "group": "build", - "label": "ada: Check current file" - }, { "type": "npm", "script": "watch", diff --git a/Makefile b/Makefile index 73195ea08..dc1933247 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ TESTER=$(ROOTDIR)/.obj/tester/tester-run$(EXE) MOCHA_ALS_UPDATE= GPRBUILD_EXTRA= -GPRBUILD_FLAGS=-m -j4 $(GPRBUILD_EXTRA) +GPRBUILD_FLAGS=-m -j0 $(GPRBUILD_EXTRA) GPRBUILD=gprbuild $(GPRBUILD_FLAGS) -XSUPERPROJECT= GPRCLEAN_EXTRA= GPRCLEAN=gprclean -XSUPERPROJECT= $(GPRCLEAN_EXTRA) diff --git a/README.md b/README.md index 18bd78bb6..76f9cd31c 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ extension at - [Refactoring](#refactoring) - [Tasks](#tasks) - [Task Customization](#task-customization) + - [Tasks for Project Mains](#tasks-for-project-mains) + - [ALIRE Support](#alire-support) - [Commands and Shortcuts](#commands-and-shortcuts) - [Ada: Go to other file](#ada-go-to-other-file) - [Ada: Add subprogram box](#ada-add-subprogram-box) @@ -190,6 +192,10 @@ The extension provides the following auto-detected tasks * `spark: Prove selected region` - launch `gnatprove` on the selected region in the current editor * `spark: Prove line` - launch `gnatprove` on the cursor line in the current editor * `spark: Clean project for proof` - launch `gnatprove` on the current GPR project to clean proof artefacts +* `ada: Analyze the project with GNAT SAS` +* `ada: Analyze the current file with GNAT SAS` +* `ada: Create a report after a GNAT SAS analysis` +* `ada: Analyze the project with GNAT SAS and produce a report` You can bind keyboard shortcuts to them by adding to the `keybindings.json` file: @@ -205,7 +211,7 @@ You can bind keyboard shortcuts to them by adding to the `keybindings.json` file #### Task Customization You can [customize auto-detected tasks](https://code.visualstudio.com/docs/editor/tasks#_customizing-autodetected-tasks) -by providing extra tool command line options via the `args` property of the `configuration` object in the `tasks.json`: +by providing extra tool command line options via the `args` property of the object in the `tasks.json`: ```json { @@ -213,19 +219,70 @@ by providing extra tool command line options via the `args` property of the `con "tasks": [ { "type": "ada", - "configuration": { - "kind": "buildProject", - "projectFile": "${config:ada.projectFile}", - "args": ["-gargs", "-vh"] + "command": "gprbuild", + "args": [ + "${command:ada.gprProjectArgs}", + "-cargs:ada", + "-gnatef", + "-gargs", + "-vh" + ], + "problemMatcher": ["$ada"], + "group": "build", + "label": "ada: Build current project" + } + ] +} +``` + +You can also customize the working directory of the task or the environment variables via the `options` property: + +```json +{ + "version": "2.0.0", + "tasks": [ + { + "type": "ada", + "command": "gprbuild", + "args": [ + "${command:ada.gprProjectArgs}", + "-cargs:ada", + "-gnatef" + ], + "options": { + "cwd": "${workspaceFolder}/my/subdir", + "env": { + "MY_ENV_VAR": "value" + } }, "problemMatcher": ["$ada"], "group": "build", - "label": "ada: Build current project with custom options" + "label": "ada: Build current project" } ] } ``` +#### Tasks for Project Mains + +If your GPR project defines main programs via the project attribute `Main`, additional tasks are automatically provided for each defined main. +For example, if the project defines a `main1.adb` and `main2.adb` located under the `src/` source directory, the following tasks will be available: + +* `ada: Build main - src/main1.adb` +* `ada: Run main - src/main1.adb` +* `ada: Build and run main - src/main1.adb` +* `ada: Build main - src/main2.adb` +* `ada: Run main - src/main2.adb` +* `ada: Build and run main - src/main2.adb` + +#### ALIRE Support + +When the workspace is an ALIRE project (i.e. it contains an `alire.toml` file), tasks automatically use standard ALIRE commands. + +For example, the `ada: Build current project` task uses the command `alr build` and the `ada: Clean current project` task uses the command `alr clean`. + +All other tasks use `alr exec -- ...` to execute the command in the environment provided by ALIRE. + ### Commands and Shortcuts The extension contributes commands and a few default key bindings. @@ -259,12 +316,13 @@ The VS Code extension has a few limitations and some differences compared to [GN * **Indentation/formatting**: it does not support automatic indentation when adding a newline and range/document formatting might no succeed on incomplete/illegal code. -* **Tooling support**: we currently provide minimal support for *SPARK* (see *Prove/Examine* tasks in the [Auto-detected tasks](#auto-detected-tasks) section), but there is no support for tools such as *CodePeer*, *GNATcheck*, *GNATtest* or *GNATcoverage*. +* **Tooling support**: we currently provide support for some *SPARK*, *GNATtest* and *GNAT SAS* [Tasks](#tasks), but there is no support for tools such as *GNATcheck* or *GNATcoverage* yet. * **Alire support**: if the root folder contains an `alire.toml` file and there is `alr` executable in the `PATH`, then the language server fetches the project's search path, environment variables and the project's file - name from the crate description. + name from the crate description. [Tasks](#tasks) are also automatically + invoked with ALIRE in this case. * **Project support**: there is no `Scenario` view: users should configure scenarios via the *ada.scenarioVariables* setting (see the settings list available [here](doc/settings.md)). Saving the settings file after changing the values will automatically reload the project and update the predefined tasks to take into account the new scenario values. diff --git a/integration/vscode/ada/.eslintrc.json b/integration/vscode/ada/.eslintrc.json index 6fae7a418..bcbfeec22 100644 --- a/integration/vscode/ada/.eslintrc.json +++ b/integration/vscode/ada/.eslintrc.json @@ -15,20 +15,14 @@ }, "project": "tsconfig.json" }, - "plugins": [ - "@typescript-eslint/eslint-plugin", - "eslint-plugin-tsdoc" - ], + "plugins": ["@typescript-eslint/eslint-plugin", "eslint-plugin-tsdoc"], "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking", "plugin:prettier/recommended" ], - "ignorePatterns": [ - "out", - "**/*.d.ts" - ], + "ignorePatterns": ["out", "**/*.d.ts"], "rules": { "tsdoc/syntax": "warn", "max-len": [ @@ -37,6 +31,7 @@ "code": 100, "ignoreUrls": true } - ] + ], + "@typescript-eslint/switch-exhaustiveness-check": "error" } -} \ No newline at end of file +} diff --git a/integration/vscode/ada/package.json b/integration/vscode/ada/package.json index 727978420..5d80b4348 100644 --- a/integration/vscode/ada/package.json +++ b/integration/vscode/ada/package.json @@ -479,11 +479,28 @@ "taskDefinitions": [ { "type": "ada", - "required": [ - "configuration" - ], + "when": "shellExecutionSupported", "properties": { + "command": { + "description": "The name of the command to call.", + "type": "string" + }, + "args": { + "description": "The arguments to pass to the command.", + "type": "array", + "items": { + "type": "string" + } + }, + "compound": { + "description": "List of task names to be executed sequentially.", + "type": "array", + "items": { + "type": "string" + } + }, "configuration": { + "deprecationMessage": "The task property 'configuration' is deprecated. Instead, use the properties 'command' and 'args' to configure the task.", "type": "object", "required": [ "kind" @@ -613,7 +630,26 @@ }, { "type": "spark", + "when": "shellExecutionSupported", "properties": { + "command": { + "description": "The name of the command to call.", + "type": "string" + }, + "args": { + "description": "The arguments to pass to the command.", + "type": "array", + "items": { + "type": "string" + } + }, + "compound": { + "description": "List of task names to be executed sequentially.", + "type": "array", + "items": { + "type": "string" + } + }, "configuration": { "type": "object", "required": [ diff --git a/integration/vscode/ada/src/ExtensionState.ts b/integration/vscode/ada/src/ExtensionState.ts index ebb495147..29aeee947 100644 --- a/integration/vscode/ada/src/ExtensionState.ts +++ b/integration/vscode/ada/src/ExtensionState.ts @@ -37,18 +37,20 @@ export class ExtensionState { public readonly testData: Map = new Map(); /** - * The following fields are caches for ALS requests + * The following fields are caches for ALS requests or costly properties. */ cachedProjectFile: string | undefined; cachedObjectDir: string | undefined; cachedMains: string[] | undefined; cachedExecutables: string[] | undefined; + cachedAlireTomls: vscode.Uri[] | undefined; public clearALSCache() { this.cachedProjectFile = undefined; this.cachedObjectDir = undefined; this.cachedMains = undefined; this.cachedExecutables = undefined; + this.cachedAlireTomls = undefined; } constructor(context: vscode.ExtensionContext) { @@ -205,4 +207,12 @@ export class ExtensionState { return this.cachedExecutables; } + + public async getAlireTomls(): Promise { + if (!this.cachedAlireTomls) { + this.cachedAlireTomls = await vscode.workspace.findFiles('alire.toml'); + } + + return this.cachedAlireTomls; + } } diff --git a/integration/vscode/ada/src/commands.ts b/integration/vscode/ada/src/commands.ts index e90edabf2..4c4980b0e 100644 --- a/integration/vscode/ada/src/commands.ts +++ b/integration/vscode/ada/src/commands.ts @@ -7,13 +7,12 @@ import { ExecuteCommandRequest } from 'vscode-languageclient'; import { ExtensionState } from './ExtensionState'; import { AdaConfig, getOrAskForProgram, initializeConfig } from './debugConfigProvider'; import { adaExtState, logger, mainOutputChannel } from './extension'; -import { findAdaMain, getProjectFileRelPath } from './helpers'; +import { findAdaMain, getProjectFileRelPath, getSymbols } from './helpers'; import { - CustomTaskDefinition, + SimpleTaskDef, findBuildAndRunTask, getBuildAndRunTasks, getConventionalTaskLabel, - getEnclosingSymbol, isFromWorkspace, } from './taskProviders'; @@ -35,6 +34,23 @@ export const CMD_BUILD_AND_RUN_MAIN = 'ada.buildAndRunMain'; */ export const CMD_BUILD_AND_DEBUG_MAIN = 'ada.buildAndDebugMain'; +/** + * Identifier for a hidden command that returns an array of strings constituting + * the -P and -X project and scenario arguments. + */ +export const CMD_GPR_PROJECT_ARGS = 'ada.gprProjectArgs'; + +/** + * Identifier for a hidden command that returns a string referencing the current + * project. That string is either `"$\{config:ada.projectFile\}"` if that + * setting is configured, or otherwise the full path to the project file + * returned from a query to the + */ +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 function registerCommands(context: vscode.ExtensionContext, clients: ExtensionState) { context.subscriptions.push(vscode.commands.registerCommand('ada.otherFile', otherFileHandler)); context.subscriptions.push( @@ -96,6 +112,19 @@ export function registerCommands(context: vscode.ExtensionContext, clients: Exte context.subscriptions.push( vscode.commands.registerCommand(CMD_BUILD_AND_DEBUG_MAIN, buildAndDebugSpecifiedMain) ); + + context.subscriptions.push( + vscode.commands.registerCommand(CMD_GPR_PROJECT_ARGS, gprProjectArgs) + ); + context.subscriptions.push( + vscode.commands.registerCommand(CMD_GET_PROJECT_FILE, getProjectFromConfigOrALS) + ); + context.subscriptions.push( + vscode.commands.registerCommand(CMD_SPARK_LIMIT_SUBP_ARG, sparkLimitSubpArg) + ); + context.subscriptions.push( + vscode.commands.registerCommand(CMD_SPARK_LIMIT_REGION_ARG, sparkLimitRegionArg) + ); } /** * Add a subprogram box above the subprogram enclosing the cursor's position, if any. @@ -285,8 +314,8 @@ async function buildAndRunMainAsk() { // If the task doesn't exist, create it // Copy the definition and add a label - const def: CustomTaskDefinition = { - ...(e.item.task.definition as CustomTaskDefinition), + const def: SimpleTaskDef = { + ...(e.item.task.definition as SimpleTaskDef), label: getConventionalTaskLabel(e.item.task), }; tasks.push(def); @@ -571,3 +600,134 @@ async function buildAndDebugSpecifiedMain(main: vscode.Uri): Promise { ); } } + +/** + * @returns an array of -P and -X project and scenario command lines arguments + * for use with GPR-based tools. + */ +export async function gprProjectArgs(): Promise { + const vars: string[][] = Object.entries( + vscode.workspace.getConfiguration('ada').get('scenarioVariables') ?? [] + ); + return ['-P', await getProjectFromConfigOrALS()].concat( + vars.map(([key, value]) => `-X${key}=${value}`) + ); +} + +export const PROJECT_FROM_CONFIG = '${config:ada.projectFile}'; + +/** + * @returns `"$\{config:ada.projectFile\}"` if that setting has a value, or else + * queries the ALS for the current project and returns the full path. + */ +export async function getProjectFromConfigOrALS(): Promise { + /** + * If ada.projectFile is set, use the $\{config:ada.projectFile\} macro + */ + return vscode.workspace.getConfiguration().get('ada.projectFile') + ? PROJECT_FROM_CONFIG + : await adaExtState.getProjectFile(); +} + +/** + * @returns the gnatprove `--limit-subp=file:line` argument associated to the + * subprogram enclosing the the current editor's cursor position. If a enclosing + * subprogram is not found at the current location, we use the \$\{lineNumber\} + * predefined variable as a fallback to avoid raising an Error which would + * prevent the task provider from offering the task. + */ +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}`]; + } else { + /** + * If we can't find a subprogram, we use the VS Code predefined + * variable lineNumber to avoid raising an Error. This function + * is called through the corresponding command during task + * resolution in the task provider. Raising an error would + * prevent the task from appear in the list of provided tasks. + * For this reason we use the acceptable fallback of lineNumber + * and rely on SPARK tooling to provide an explanatory message. + */ + return [`--limit-subp=\${fileBasename}:\${lineNumber}`]; + } + } + ); +} + +/** + * @returns the gnatprove `--limit-region=file:from:to` argument corresponding + * to the current editor's selection. + */ +export const sparkLimitRegionArg = (): Promise => { + return new Promise((resolve) => { + resolve([ + `--limit-region=\${fileBasename}:${getSelectedRegion(vscode.window.activeTextEditor)}`, + ]); + }); +}; + +/** + * + * @param editor - the editor to get the selection from + * @returns a `:` string representation of the editor's + * current selection where lines are indexed starting 1. If the given editor is + * undefined, returns '0:0'. + */ +export const getSelectedRegion = (editor: vscode.TextEditor | undefined): string => { + if (editor) { + const selection = editor.selection; + // Line numbers start at 0 in VS Code, and at 1 in GNAT + return `${selection.start.line + 1}:${selection.end.line + 1}`; + } else { + return '0:0'; + } +}; + +/** + * Return the closest DocumentSymbol of the given kinds enclosing the + * the given editor's cursor position, if any. + * @param editor - The editor in which we want + * to find the closest symbol enclosing the cursor's position. + * @returns Return the closest enclosing symbol. + */ + +export async function getEnclosingSymbol( + editor: vscode.TextEditor | undefined, + kinds: vscode.SymbolKind[] +): Promise { + if (editor) { + const line = editor.selection.active.line; + + // First get all symbols for current file + const symbols: vscode.DocumentSymbol[] = await vscode.commands.executeCommand( + 'vscode.executeDocumentSymbolProvider', + editor.document.uri + ); + + // Then filter them according to the specified kinds + const filtered_symbols = getSymbols(symbols, kinds, [ + SymbolKind.Function, + SymbolKind.Module, + ]); + + // Finally select from the filtered symbols the smallest one containing the current line + const scopeSymbols = filtered_symbols.filter( + (sym) => line >= sym.range.start.line && line <= sym.range.end.line + ); + + if (scopeSymbols.length > 0) { + scopeSymbols.sort( + (a, b) => + a.range.end.line - a.range.start.line - (b.range.end.line - b.range.start.line) + ); + + return scopeSymbols[0]; + } + } + + return null; +} diff --git a/integration/vscode/ada/src/gnatTaskProvider.ts b/integration/vscode/ada/src/gnatTaskProvider.ts index a8b608c72..ad08407eb 100644 --- a/integration/vscode/ada/src/gnatTaskProvider.ts +++ b/integration/vscode/ada/src/gnatTaskProvider.ts @@ -15,18 +15,174 @@ -- of the license. -- ----------------------------------------------------------------------------*/ +import commandExists from 'command-exists'; import * as vscode from 'vscode'; -import { - AllTaskKinds, - DEFAULT_PROBLEM_MATCHER, - WarningMessageExecution, - TaskProperties, - alire, - allTaskProperties, - getDiagnosticArgs, - getProjectArgs, - getScenarioArgs, -} from './taskProviders'; +import { getProjectFromConfigOrALS, sparkLimitRegionArg, sparkLimitSubpArg } from './commands'; +import { DEFAULT_PROBLEM_MATCHER, WarningMessageExecution } from './taskProviders'; + +/** + * Callback to provide an extra argument for a tool + */ +type ExtraArgCallback = () => Promise; + +/** + * Tool description + */ +interface TaskProperties { + // Executable like gprbuild, gprclean, gnatprove, etc. and static list of + // arguments. + command?: string[]; + // Dynamic argument callback called at the time of task execution. Args and + // extra argument will be wrapped with getGnatArgs if this is set. + extra?: ExtraArgCallback; + // Short title displayed in task list + title: string; + // Long description displayed in the task list on a separate line + description?: string; + // Use project and scenario args. Treated as true if unspecified. + projectArgs?: boolean; + // Use -cargs:ada -gnatef to obtain full paths in diagnostics. Treated as true if unspecified. + diagnosticArgs?: boolean; +} + +// The following pair of declarations allow creating a set of string values both +// as an iterable (constant) array, and as a union type. +export const adaTaskKinds = [ + 'buildProject', + 'checkFile', + 'cleanProject', + 'buildMain', + 'runMain', + 'buildAndRunMain', + 'gnatsasAnalyze', + 'gnatsasReport', + 'gnatsasAnalyzeAndReport', + 'gnatdoc', + 'gnattest', +] as const; +export type AdaTaskKinds = (typeof adaTaskKinds)[number]; + +// The following pair of declarations allow creating a set of string values both +// as an iterable (constant) array, and as a union type. +const sparkTaskKinds = [ + 'cleanProjectForProof', + 'examineProject', + 'examineFile', + 'examineSubprogram', + 'proveProject', + 'proveFile', + 'proveSubprogram', + 'proveRegion', + 'proveLine', +] as const; +type SparkTaskKinds = (typeof sparkTaskKinds)[number]; + +export type AllTaskKinds = AdaTaskKinds | SparkTaskKinds; + +/** + * Map of known tasks/tools indexed by a string/taskKind + */ +const allTaskProperties: { [id in AllTaskKinds]: TaskProperties } = { + cleanProjectForProof: { + command: ['gnatprove', '--clean'], + title: 'Clean project for proof', + diagnosticArgs: false, + }, + examineProject: { + command: ['gnatprove', '-j0', '--mode=flow'], + title: 'Examine project', + }, + examineFile: { + command: ['gnatprove', '-j0', '--mode=flow', '-u', '${fileBasename}'], + title: 'Examine file', + }, + examineSubprogram: { + command: ['gnatprove', '-j0', '--mode=flow'], + extra: sparkLimitSubpArg, + title: 'Examine subprogram', + }, + proveProject: { + command: ['gnatprove', '-j0'], + title: 'Prove project', + }, + proveFile: { + command: ['gnatprove', '-j0', '-u', '${fileBasename}'], + title: 'Prove file', + }, + proveSubprogram: { + command: ['gnatprove', '-j0'], + extra: sparkLimitSubpArg, + title: 'Prove subprogram', + }, + proveRegion: { + command: ['gnatprove', '-j0', '-u', '${fileBasename}'], + extra: sparkLimitRegionArg, + title: 'Prove selected region', + }, + proveLine: { + command: [ + 'gnatprove', + '-j0', + '-u', + '${fileBasename}', + '--limit-line=${fileBasename}:${lineNumber}', + ], + title: 'Prove line', + }, + buildProject: { + command: ['gprbuild'], + title: 'Build current project', + }, + checkFile: { + command: ['gprbuild', '-q', '-f', '-c', '-u', '-gnatc', '${fileBasename}'], + title: 'Check current file', + }, + cleanProject: { + command: ['gprclean'], + title: 'Clean current project', + diagnosticArgs: false, + }, + buildMain: { + command: ['gprbuild'], + title: 'Build main - ', + }, + runMain: { + command: [], + title: 'Run main - ', + projectArgs: false, + diagnosticArgs: false, + }, + buildAndRunMain: { + title: 'Build and run main - ', + // description: 'Run the build task followed by the run task for the given main', + }, + gnatsasAnalyze: { + command: ['gnatsas', 'analyze'], + title: 'Analyze the project with GNAT SAS', + diagnosticArgs: false, + }, + gnatsasReport: { + command: ['gnatsas', 'report'], + title: 'Create a report after a GNAT SAS analysis', + // We set this flag to false because project args are added later as + // part of the 'args' task property + projectArgs: false, + diagnosticArgs: false, + }, + gnatsasAnalyzeAndReport: { + title: 'Analyze the project with GNAT SAS and produce a report', + }, + gnatdoc: { + command: ['gnatdoc'], + title: 'Generate documentation from the project', + diagnosticArgs: false, + }, + gnattest: { + command: ['gnattest'], + title: 'Create/update test skeletons for the project', + diagnosticArgs: false, + }, +}; /** * @deprecated This interface defines the data structures matching the JSON @@ -138,7 +294,7 @@ export async function getTasks(): Promise { }; const extraArgsFromTask = item.extra ? item.extra() : []; const cmd = alr.concat( - item.command, + item.command ?? [], await getProjectArgs(), getScenarioArgs(), await extraArgsFromTask, @@ -160,3 +316,43 @@ export async function getTasks(): Promise { return result; }); } +export async function getProjectArgs() { + return await computeProject() + .then((prj) => ['-P', prj]) + .catch(() => []); +} +export async function computeProject(): Promise { + // If the task definition defines a project file, use that. Otherwise if + // ada.projectFile is defined, use ${config:ada.projectFile}. Finally, + // fallback to querying the ALS for the full path to the project file. + return getProjectFromConfigOrALS(); +} // Call commonArgs on args and append `-gnatef` to generate full file names in errors/warnings + +export const getDiagnosticArgs = (): string[] => { + const p_gnatef = ['-cargs:ada', '-gnatef']; + return p_gnatef; +}; +export function getScenarioArgs() { + const vars: string[][] = Object.entries( + vscode.workspace.getConfiguration('ada').get('scenarioVariables') ?? [] + ); + const fold = (args: string[], item: string[]): string[] => { + const option = '-X' + item[0] + '=' + item[1]; + return args.concat([option]); + }; + + // for each scenarioVariables put `-Xname=value` option + return vars.reduce(fold, []); +} +// Alire `exec` command if we have `alr` installed and `alire.toml` + +export async function alire(): Promise { + return vscode.workspace.findFiles('alire.toml').then((found) => + found.length == 0 + ? [] // not alire.toml found, return no command + : // if alire.toml found, search for `alr` + commandExists('alr') + .then(() => ['alr', 'exec', '--']) + .catch(() => []) + ); +} diff --git a/integration/vscode/ada/src/gnattest.ts b/integration/vscode/ada/src/gnattest.ts index e2746c29a..8bd2378ef 100644 --- a/integration/vscode/ada/src/gnattest.ts +++ b/integration/vscode/ada/src/gnattest.ts @@ -422,7 +422,8 @@ export async function resolveHandler( await refreshTestItemTree(); } else { const testItemData = testData.get(item); - switch (testItemData?.type) { + assert(testItemData?.type !== undefined); + switch (testItemData.type) { case TestItemType.Unit: resolveUnitItem(item, testItemData.data as Unit); break; diff --git a/integration/vscode/ada/src/taskProviders.ts b/integration/vscode/ada/src/taskProviders.ts index 855d2f9a1..3f28d2f21 100644 --- a/integration/vscode/ada/src/taskProviders.ts +++ b/integration/vscode/ada/src/taskProviders.ts @@ -16,719 +16,711 @@ ----------------------------------------------------------------------------*/ import assert from 'assert'; -import commandExists from 'command-exists'; +import { basename } from 'path'; import * as vscode from 'vscode'; -import { SymbolKind } from 'vscode'; +import { CMD_GPR_PROJECT_ARGS } from './commands'; import { adaExtState } from './extension'; -import { AdaMain, getAdaMains, getProjectFile, getSymbols } from './helpers'; -import path from 'path'; +import { AdaMain, getAdaMains } from './helpers'; -export const ADA_TASK_TYPE = 'ada'; +export const TASK_TYPE_ADA = 'ada'; +export const TASK_TYPE_SPARK = 'spark'; +export const DEFAULT_PROBLEM_MATCHER = '$ada'; /** - * Callback to provide an extra argument for a tool + * A type representing task definitions as they appear in tasks.json files. */ -type ExtraArgCallback = () => Promise; +export interface SimpleTaskDef extends vscode.TaskDefinition { + /** + * The name of the executable to invoke. + */ + command?: string | vscode.ShellQuotedString; + /** + * Arguments to pass to the invocation. + */ + args?: (string | vscode.ShellQuotedString)[]; + /** + * This property should not occur at the same time as command and args. + * {@link SimpleTaskProvider.resolveTask} checks that in case it occurs in + * User task definitions and raises errors accordingly. + */ + compound?: string[]; +} /** - * Tool description + * An internal type to represent predefined tasks that will offered by the extension. */ -export interface TaskProperties { - // Executable like gprbuild, gprclean, gnatprove, etc. and static list of - // arguments. - command: string[]; - // Dynamic argument callback called at the time of task execution. Args and - // extra argument will be wrapped with getGnatArgs if this is set. - extra: ExtraArgCallback | undefined; - // Short title displayed in task list - title: string; - // Long description displayed in the task list on a separate line +interface PredefinedTask { + label: string; description?: string; + taskDef: SimpleTaskDef; + problemMatchers?: string | string[]; + taskGroup?: vscode.TaskGroup; } /** - * Return the `--limit-subp=file:line` associated to the subprogram enclosing the - * the current editor's cursor position, if any. Return an empty string otherwise. - * @returns Return the option corresponding to the enclosing subprogram as a string - * or '' if not found. + * The main build project task defined as a constant to allow it to be referenced directly. */ -const limitSubp = async (): 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}`]; - } else { - return []; - } - } - ); -}; - -/** - * Return the `--limit-region=file:from:to` associated to the current editor's selection. - * @returns Return the option corresponding to the current selected region. - */ -const limitRegion = (): Promise => { - return new Promise((resolve) => { - resolve([ - `--limit-region=\${fileBasename}:${getSelectedRegion(vscode.window.activeTextEditor)}`, - ]); - }); +const TASK_BUILD_PROJECT: PredefinedTask = { + label: 'Build current project', + taskDef: { + type: TASK_TYPE_ADA, + command: 'gprbuild', + args: ['${command:ada.gprProjectArgs}', '-cargs:ada', '-gnatef'], + }, + problemMatchers: DEFAULT_PROBLEM_MATCHER, + taskGroup: vscode.TaskGroup.Build, }; - -async function computeProject(taskDef?: CustomTaskDefinition): Promise { - // If the task definition defines a project file, use that. Otherwise if - // ada.projectFile is defined, use ${config:ada.projectFile}. Finally, - // fallback to querying the ALS for the full path to the project file. - return taskDef?.configuration?.projectFile ?? getProjectFromConfigOrALS(); -} - -// Call commonArgs on args and append `-gnatef` to generate full file names in errors/warnings -export const getDiagnosticArgs = (): string[] => { - const p_gnatef = ['-cargs:ada', '-gnatef']; - return p_gnatef; +// eslint-disable-next-line max-len +export const BUILD_PROJECT_TASK_NAME = `${TASK_BUILD_PROJECT.taskDef.type}: ${TASK_BUILD_PROJECT.label}`; + +const TASK_CLEAN_PROJECT = { + label: 'Clean current project', + taskDef: { + type: TASK_TYPE_ADA, + command: 'gprclean', + args: ['${command:ada.gprProjectArgs}'], + }, + problemMatchers: '', + taskGroup: vscode.TaskGroup.Clean, }; -// The following pair of declarations allow creating a set of string values both -// as an iterable (constant) array, and as a union type. -const adaTaskKinds = [ - 'buildProject', - 'checkFile', - 'cleanProject', - 'buildMain', - 'runMain', - 'buildAndRunMain', -] as const; -type AdaTaskKinds = (typeof adaTaskKinds)[number]; - -// The following pair of declarations allow creating a set of string values both -// as an iterable (constant) array, and as a union type. -const sparkTaskKinds = [ - 'cleanProjectForProof', - 'examineProject', - 'examineFile', - 'examineSubprogram', - 'proveProject', - 'proveFile', - 'proveSubprogram', - 'proveRegion', - 'proveLine', -] as const; -type SparkTaskKinds = (typeof sparkTaskKinds)[number]; - -export type AllTaskKinds = AdaTaskKinds | SparkTaskKinds; - -/** - * This interface defines the data structure expected in vscode task - * definitions. It intends to match as closely as possible with the JSON schemas - * defined in the package.json file for the "ada" and "spark" tasks. However - * JSON schemas are more expressive in terms of constraints between properties - * within the data structure. As a result this interface simply marks fields as - * optional when they may or may not occur while the JSON schemas of - * package.json describe the structure more precisely. - */ -export interface CustomTaskDefinition extends vscode.TaskDefinition { - configuration: { - kind: AllTaskKinds; - projectFile?: string; - args?: string[]; - main?: string; - mainArgs?: string[]; - buildTask?: string; - runTask?: string; - }; -} - /** - * Map of known tasks/tools indexed by a string/taskKind + * 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 + * task providers. */ -export const allTaskProperties: { [id in AllTaskKinds]: TaskProperties } = { - cleanProjectForProof: { - command: ['gnatprove', '--clean'], - extra: undefined, - title: 'Clean project for proof', +const predefinedTasks: PredefinedTask[] = [ + /** + * Ada + */ + TASK_CLEAN_PROJECT, + TASK_BUILD_PROJECT, + { + label: 'Check current file', + taskDef: { + type: TASK_TYPE_ADA, + command: 'gprbuild', + args: [ + '-q', + '-f', + '-c', + '-u', + '-gnatc', + '${command:ada.gprProjectArgs}', + '${fileBasename}', + '-cargs:ada', + '-gnatef', + ], + }, + problemMatchers: DEFAULT_PROBLEM_MATCHER, }, - examineProject: { - command: ['gnatprove', '-j0', '--mode=flow'], - extra: undefined, - title: 'Examine project', + { + label: 'Analyze the project with GNAT SAS', + taskDef: { + type: TASK_TYPE_ADA, + command: 'gnatsas', + args: ['analyze', '${command:ada.gprProjectArgs}'], + }, + /** + * Analysis results are not printed on stdio so no need to parse them + * with a problem matcher. Results should be viewed with the + * `gnatsas report` task below + */ + problemMatchers: '', }, - examineFile: { - command: ['gnatprove', '-j0', '--mode=flow', '-u', '${fileBasename}'], - extra: undefined, - title: 'Examine file', + { + label: 'Analyze the current file with GNAT SAS', + taskDef: { + type: TASK_TYPE_ADA, + command: 'gnatsas', + args: ['analyze', '${command:ada.gprProjectArgs}', '--file=${fileBasename}'], + }, + /** + * Analysis results are not printed on stdio so no need to parse them + * with a problem matcher. Results should be viewed with the + * `gnatsas report` task below + */ + problemMatchers: '', + }, + { + label: 'Create a report after a GNAT SAS analysis', + taskDef: { + type: TASK_TYPE_ADA, + command: 'gnatsas', + args: ['report', 'sarif', '${command:ada.gprProjectArgs}', '-o', 'report.sarif'], + }, + /** + * Analysis results are not printed on stdio so no need to parse them + * with a problem matcher. + */ + problemMatchers: '', }, - examineSubprogram: { - command: ['gnatprove', '-j0', '--mode=flow'], - extra: limitSubp, - title: 'Examine subprogram', + { + label: 'Analyze the project with GNAT SAS and produce a report', + taskDef: { + type: TASK_TYPE_ADA, + compound: [ + 'Analyze the project with GNAT SAS', + 'Create a report after a GNAT SAS analysis', + ], + }, + /** + * Analysis results are not printed on stdio so no need to parse them + * with a problem matcher. + */ + problemMatchers: '', }, - proveProject: { - command: ['gnatprove', '-j0'], - extra: undefined, - title: 'Prove project', + { + label: 'Generate documentation from the project', + taskDef: { + type: TASK_TYPE_ADA, + command: 'gnatdoc', + args: ['${command:ada.gprProjectArgs}'], + }, + problemMatchers: '', }, - proveFile: { - command: ['gnatprove', '-j0', '-u', '${fileBasename}'], - extra: undefined, - title: 'Prove file', + { + label: 'Create/update test skeletons for the project', + taskDef: { + type: TASK_TYPE_ADA, + command: 'gnattest', + args: ['${command:ada.gprProjectArgs}'], + }, + problemMatchers: '', }, - proveSubprogram: { - command: ['gnatprove', '-j0'], - extra: limitSubp, - title: 'Prove subprogram', + /** + * SPARK + */ + { + label: 'Clean project for proof', + taskDef: { + type: TASK_TYPE_SPARK, + command: 'gnatprove', + args: ['${command:ada.gprProjectArgs}', '--clean'], + }, + problemMatchers: '', }, - proveRegion: { - command: ['gnatprove', '-j0', '-u', '${fileBasename}'], - extra: limitRegion, - title: 'Prove selected region', + { + label: 'Examine project', + taskDef: { + type: TASK_TYPE_SPARK, + command: 'gnatprove', + args: ['${command:ada.gprProjectArgs}', '-j0', '--mode=flow', '-cargs', '-gnatef'], + }, + problemMatchers: DEFAULT_PROBLEM_MATCHER, }, - proveLine: { - command: [ - 'gnatprove', - '-j0', - '-u', - '${fileBasename}', - '--limit-line=${fileBasename}:${lineNumber}', - ], - extra: undefined, - title: 'Prove line', + { + label: 'Examine file', + taskDef: { + type: TASK_TYPE_SPARK, + command: 'gnatprove', + args: [ + '${command:ada.gprProjectArgs}', + '-j0', + '--mode=flow', + '-u', + '${fileBasename}', + '-cargs', + '-gnatef', + ], + }, + problemMatchers: DEFAULT_PROBLEM_MATCHER, }, - buildProject: { - command: ['gprbuild'], - extra: undefined, - title: 'Build current project', + { + label: 'Examine subprogram', + taskDef: { + type: TASK_TYPE_SPARK, + command: 'gnatprove', + args: [ + '${command:ada.gprProjectArgs}', + '-j0', + '--mode=flow', + '${command:ada.spark.limitSubpArg}', + '-cargs', + '-gnatef', + ], + }, + problemMatchers: DEFAULT_PROBLEM_MATCHER, }, - checkFile: { - command: ['gprbuild', '-q', '-f', '-c', '-u', '-gnatc', '${fileBasename}'], - extra: undefined, - title: 'Check current file', + { + label: 'Prove project', + taskDef: { + type: TASK_TYPE_SPARK, + command: 'gnatprove', + args: ['${command:ada.gprProjectArgs}', '-j0', '-cargs', '-gnatef'], + }, + problemMatchers: DEFAULT_PROBLEM_MATCHER, }, - cleanProject: { - command: ['gprclean'], - extra: undefined, - title: 'Clean current project', + { + label: 'Prove file', + taskDef: { + type: TASK_TYPE_SPARK, + command: 'gnatprove', + args: [ + '${command:ada.gprProjectArgs}', + '-j0', + '-u', + '${fileBasename}', + '-cargs', + '-gnatef', + ], + }, + problemMatchers: DEFAULT_PROBLEM_MATCHER, }, - buildMain: { - command: ['gprbuild'], - extra: undefined, - title: 'Build main - ', + { + label: 'Prove subprogram', + taskDef: { + type: TASK_TYPE_SPARK, + command: 'gnatprove', + args: [ + '${command:ada.gprProjectArgs}', + '-j0', + '${command:ada.spark.limitSubpArg}', + '-cargs', + '-gnatef', + ], + }, + problemMatchers: DEFAULT_PROBLEM_MATCHER, }, - runMain: { - command: [], - extra: undefined, - title: 'Run main - ', + { + label: 'Prove selected region', + taskDef: { + type: TASK_TYPE_SPARK, + command: 'gnatprove', + args: [ + '${command:ada.gprProjectArgs}', + '-j0', + '-u', + '${fileBasename}', + '${command:ada.spark.limitRegionArg}', + '-cargs', + '-gnatef', + ], + }, + problemMatchers: DEFAULT_PROBLEM_MATCHER, }, - buildAndRunMain: { - command: ['gprbuild'], - extra: undefined, - title: 'Build and run main - ', - // description: 'Run the build task followed by the run task for the given main', + { + label: 'Prove line', + taskDef: { + type: TASK_TYPE_SPARK, + command: 'gnatprove', + args: [ + '${command:ada.gprProjectArgs}', + '-j0', + '-u', + '${fileBasename}', + '--limit-line=${fileBasename}:${lineNumber}', + '-cargs', + '-gnatef', + ], + }, + problemMatchers: DEFAULT_PROBLEM_MATCHER, }, -}; - -// eslint-disable-next-line max-len -export const BUILD_PROJECT_TASK_NAME = `${ADA_TASK_TYPE}: ${allTaskProperties['buildProject'].title}`; - -export const PROJECT_FROM_CONFIG = '${config:ada.projectFile}'; -async function getProjectFromConfigOrALS(): Promise { - /** - * If ada.projectFile is set, use the $\{config:ada.projectFile\} macro - */ - return vscode.workspace.getConfiguration().get('ada.projectFile') - ? PROJECT_FROM_CONFIG - : await getProjectFile(adaExtState.adaClient); -} - -export function getScenarioArgs() { - const vars: string[][] = Object.entries( - vscode.workspace.getConfiguration('ada').get('scenarioVariables') ?? [] - ); - const fold = (args: string[], item: string[]): string[] => { - const option = '-X' + item[0] + '=' + item[1]; - return args.concat([option]); - }; - - // for each scenarioVariables put `-Xname=value` option - return vars.reduce(fold, []); -} - -export async function getProjectArgs(taskDef?: CustomTaskDefinition) { - return await computeProject(taskDef) - .then((prj) => ['-P', prj]) - .catch(() => []); -} - -// Alire `exec` command if we have `alr` installed and `alire.toml` -export async function alire(): Promise { - return vscode.workspace.findFiles('alire.toml').then((found) => - found.length == 0 - ? [] // not alire.toml found, return no command - : // if alire.toml found, search for `alr` - commandExists('alr') - .then(() => ['alr', 'exec', '--']) - .catch(() => []) - ); -} +]; /** - * This function returns a fully resolved task, either based on a - * TaskDefinition, or on an incomplete task to be resolved. - * - * @param definition - CustomTaskDefinition to base the new task on. If 'task' - * is also given, then it must be that `definition == task.definition`. - * @param commandPrefix - a prefix for the command of the new task - * @param name - the name to give the new task - * @param task - the task to be resolved + * A provider of tasks based on the {@link SimpleTaskDef} task definition. * - * @returns a new fully resolved task based on the definition or based on the - * incomplete task given. + * It is instantiated with a string task type and an array of {@link + * PredefinedTask} to provide. This way the same class can be reused to provide + * both 'ada' and 'spark' tasks. */ -async function createOrResolveTask( - definition: CustomTaskDefinition, - commandPrefix: string[] = [], - name?: string, - task?: vscode.Task -): Promise { - if (task) { - assert(definition == task.definition); - } +export class SimpleTaskProvider implements vscode.TaskProvider { + constructor(public taskType: string, private taskDecls: PredefinedTask[]) {} - name = name ?? task?.name ?? allTaskProperties[definition.configuration.kind].title; + async provideTasks(token?: vscode.CancellationToken): Promise { + if (token?.isCancellationRequested) { + throw new vscode.CancellationError(); + } - let execution; - if (definition.configuration.kind == 'buildAndRunMain') { - execution = new BuildAndRunExecution(definition); - } else { - /** - * Quote the command line so that no shell interpretations can happen. - */ - const cmd = quoteCommandLine( - commandPrefix.concat(await buildFullCommandLine(name, definition)) - ); + const result: vscode.Task[] = []; /** - * It is necessary to use a ShellExecution instead of a ProcessExecution to - * go through a terminal where terminal.integrated.env.* is applicable and - * tools can be resolved and can run according to the User's environment - * settings. Alternatively, a ProcessExecution could be used if the - * extension resolves the full path to the called executable and passes the - * terminal.integrated.env.* environment to the child process. But this is - * deemed overkill for the moment. + * Start with the list of predefined tasks. */ - execution = new vscode.ShellExecution(cmd[0], cmd.slice(1)); - } + let taskDeclsToOffer = this.taskDecls.concat(); + + if (this.taskType == TASK_TYPE_ADA) { + /** + * Add tasks based on the Mains of the project. + */ + taskDeclsToOffer.push( + ...(await getAdaMains()).flatMap((main) => { + if (token?.isCancellationRequested) { + throw new vscode.CancellationError(); + } - /** - * If task was given to be resolved, use its properties in priority. - */ - const newTask = new vscode.Task( - definition, - task?.scope ?? vscode.TaskScope.Workspace, - name, - // Always use the task type as a source string in the UI for consistency - // between the tasks.json definitions and what Users see in the UI - definition.type, - execution, - [] - ); + const buildTask: PredefinedTask = { + label: getBuildTaskPlainName(main), + taskDef: { + type: this.taskType, + command: 'gprbuild', + args: [ + `\${command:${CMD_GPR_PROJECT_ARGS}}`, + main.mainRelPath(), + '-cargs:ada', + '-gnatef', + ], + }, + problemMatchers: DEFAULT_PROBLEM_MATCHER, + taskGroup: vscode.TaskGroup.Build, + }; - newTask.detail = task?.detail ?? allTaskProperties[definition.configuration.kind].description; + const runTask: PredefinedTask = { + label: getRunTaskPlainName(main), + taskDef: { + type: this.taskType, + command: main.execRelPath(), + args: [], + }, + }; - switch (definition.configuration.kind) { - case 'runMain': - case 'buildAndRunMain': - break; + const buildAndRunTask: PredefinedTask = { + label: getBuildAndRunTaskPlainName(main), + taskDef: { + type: this.taskType, + compound: [buildTask.label, runTask.label], + }, + problemMatchers: '', + }; - case 'cleanProject': - case 'cleanProjectForProof': { - newTask.group = vscode.TaskGroup.Clean; - newTask.problemMatchers = [DEFAULT_PROBLEM_MATCHER]; - break; + return [buildTask, runTask, buildAndRunTask]; + }) + ); } - default: { - newTask.group = vscode.TaskGroup.Build; - newTask.problemMatchers = [DEFAULT_PROBLEM_MATCHER]; + /** + * Port tasks to ALIRE if applicable + */ + if (await useAlire()) { + taskDeclsToOffer = taskDeclsToOffer.map((t) => ({ + ...t, + taskDef: updateToAlire(t.taskDef), + })); } - } - return newTask; -} - -/** - * Return the closest DocumentSymbol of the given kinds enclosing the - * the given editor's cursor position, if any. - * @param editor - The editor in which we want - * to find the closest symbol enclosing the cursor's position. - * @returns Return the closest enclosing symbol. - */ -export async function getEnclosingSymbol( - editor: vscode.TextEditor | undefined, - kinds: vscode.SymbolKind[] -): Promise { - if (editor) { - const line = editor.selection.active.line; - - // First get all symbols for current file - const symbols: vscode.DocumentSymbol[] = await vscode.commands.executeCommand( - 'vscode.executeDocumentSymbolProvider', - editor.document.uri - ); - - // Then filter them according to the specified kinds - const filtered_symbols = getSymbols(symbols, kinds, [ - SymbolKind.Function, - SymbolKind.Module, - ]); + /** + * Create vscode.Task objects for all tasks to offer. + */ + for (const tDecl of taskDeclsToOffer) { + if (token?.isCancellationRequested) { + throw new vscode.CancellationError(); + } - // Finally select from the filtered symbols the smallest one containing the current line - const scopeSymbols = filtered_symbols.filter( - (sym) => line >= sym.range.start.line && line <= sym.range.end.line - ); + 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) + ); + } - if (scopeSymbols.length > 0) { - scopeSymbols.sort( - (a, b) => - a.range.end.line - a.range.start.line - (b.range.end.line - b.range.start.line) + const task = new vscode.Task( + tDecl.taskDef, + vscode.TaskScope.Workspace, + tDecl.label, + tDecl.taskDef.type, + execution, + tDecl.problemMatchers ); - return scopeSymbols[0]; + if (tDecl.taskGroup) { + task.group = tDecl.taskGroup; + } + + result.push(task); } + + return result; } - return null; -} + async resolveTask( + task: vscode.Task, + token?: vscode.CancellationToken + ): Promise { + /** + * Note that this method is never called for tasks created by the + * provideTasks method above (see parent method documentation). It is + * called for tasks defined (/customized by the user) in the tasks.json + * file. + */ -export const getSelectedRegion = (editor: vscode.TextEditor | undefined): string => { - if (editor) { - const selection = editor.selection; - // Line numbers start at 0 in VS Code, and at 1 in GNAT - return (selection.start.line + 1).toString() + ':' + (selection.end.line + 1).toString(); - } else { - return '0:0'; - } -}; + if (token?.isCancellationRequested) { + throw new vscode.CancellationError(); + } + /** + * Validate that the task is based on a {@link SimpleTaskDef} and has + * the expected structure. + */ + this.validateTask(task); -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type ObsoleteTaskType = 'gnat' | 'gpr'; -type TaskType = 'ada' | 'spark'; + const taskDef = task.definition as SimpleTaskDef; -export function registerTaskProviders() { - return [ - vscode.tasks.registerTaskProvider('ada', createAdaTaskProvider()), - vscode.tasks.registerTaskProvider('spark', createSparkTaskProvider()), - ]; -} + /** + * Resolve the task. + */ + let execution; + if (taskDef.compound) { + assert(!taskDef.command); + assert(!taskDef.args); + execution = new SequentialExecutionByName(task.name, taskDef.compound); + } else { + assert(taskDef.command); + /** + * We support working with just the command property, in which case + * fallback to an empty args array. + */ + const args = taskDef.args ?? []; + try { + const evaluatedArgs: (string | vscode.ShellQuotedString)[] = await evaluateArgs( + args + ); + execution = new vscode.ShellExecution(taskDef.command, evaluatedArgs); + } catch (err) { + let msg = 'Error while evaluating task arguments.'; + if (err instanceof Error) { + msg += ' ' + err.message; + } + void vscode.window.showErrorMessage(msg); + return undefined; + } + } -export const DEFAULT_PROBLEM_MATCHER = '$ada'; + return new vscode.Task( + task.definition, + task.scope ?? vscode.TaskScope.Workspace, + task.name, + task.source, + execution, + task.problemMatchers + ); + } -/** - * This class implements the TaskProvider interface with some configurable functionality. - */ -export class ConfigurableTaskProvider implements vscode.TaskProvider { /** - * The list of provided tasks can be cached for efficiency, however some - * tasks depend on the cursor location in the active editor. With caching, - * the cursor location gets considered only the first time the tasks get - * computed. The cached tasks keep using that first location. * - * So for now we disable caching of tasks until a solution is found. + * Validate that the given task is based on a {@link SimpleTaskDef} and has + * the expected structure, e.g. command/args properties should not occur + * with the compound property. The method displays an error message in the + * UI and throws an error in case of violations. + * + * @param task - a User-defined {@link vscode.Task} */ - private static DISABLE_CACHING = true; + private validateTask(task: vscode.Task): void { + if ('configuration' in task.definition) { + const msg = `You are trying to use a '${task.definition.type}' task with an + obsolete property 'configuration' that is no longer supported. + It is recommended to remove this task from your workspace + configuration and use tasks automatically provided by the + extension or customize them to your needs.`; + /** + * This is an obsolete configuration, so warn and don't do anything + */ + void vscode.window.showErrorMessage(msg); + throw Error(msg); + } - public static taskTypeAda: TaskType = 'ada'; - public static taskTypeSpark: TaskType = 'spark'; - tasks: vscode.Task[] | undefined = undefined; - taskType: TaskType; - taskKinds: AllTaskKinds[]; + const taskDef = task.definition as SimpleTaskDef; - constructor(taskType: TaskType, taskKinds: AllTaskKinds[]) { - this.taskType = taskType; - this.taskKinds = taskKinds; + /** + * Validate the task. + */ + if (!(taskDef.compound || taskDef.command)) { + /** + * We allow args to be unspecified, but command has to. + */ + const msg = + `A task of type '${this.taskType}' must specify either the 'command' and 'args' ` + + "properties or the 'compound' property."; + void vscode.window.showErrorMessage(msg); + throw Error(msg); + } + + if (taskDef.compound && (taskDef.command || taskDef.args)) { + const msg = + `A task of type '${this.taskType}' must specify either the 'command' and 'args' ` + + "properties or the 'compound' property, but not both."; + void vscode.window.showErrorMessage(msg); + throw Error(msg); + } } +} - async provideTasks(token?: vscode.CancellationToken): Promise { - if (!this.tasks || ConfigurableTaskProvider.DISABLE_CACHING) { - this.tasks = []; - const cmdPrefix = await alire(); - - const projectFile = await getProjectFromConfigOrALS(); - for (const kind of this.taskKinds) { - if (token?.isCancellationRequested) { - this.tasks = undefined; - break; - } - if ( - kind in allTaskProperties && - kind != 'buildMain' && - kind != 'buildAndRunMain' && - kind != 'runMain' - ) { - // Do not provide a task for buildMain because we provide - // one per project main below - - const definition: CustomTaskDefinition = { - type: this.taskType, - configuration: { - kind: kind, - projectFile: projectFile, - args: [], - }, - }; - // provideTasks() is expected to provide fully resolved - // tasks ready for execution - const task = await createOrResolveTask(definition, cmdPrefix); - this.tasks.push(task); - } - } +/** + * + * @returns true if ALIRE should be used for task execution, i.e. when the + * workspace contains a `alire.toml` file. + */ +async function useAlire() { + return (await adaExtState.getAlireTomls()).length > 0; +} - if (this.taskType == 'ada') { - for (const main of await getAdaMains()) { - if (token?.isCancellationRequested) { - this.tasks = undefined; - break; +/** + * Evaluate task arguments with support for commands returning arrays (VS Code + * native evaluation of task arguments only allows commands returning a plain + * string). + * + * @param args - an array of command line arguments from a task + * @returns the array of arguments where items matching the pattern + * '$\{command:ada.*\} have been evaluated. If they return an array of strings, + * then the array is inserted into the argument array at the location of the + * command. + */ +async function evaluateArgs(args: (string | vscode.ShellQuotedString)[]) { + const commandRegex = new RegExp( + `^\\\${command:\\s*((${TASK_TYPE_ADA}|${TASK_TYPE_SPARK})\\.[^}]*)\\s*}$` + ); + const evaluatedArgs: (string | vscode.ShellQuotedString)[] = ( + await Promise.all( + args.flatMap(async function ( + a: string | vscode.ShellQuotedString + ): Promise<(string | vscode.ShellQuotedString)[]> { + if (typeof a == 'string') { + /** + * Perform command evaluation in strings, not in ShellQuotedStrings + */ + const match = a.match(commandRegex); + if (match) { + /** + * The string matches an ada.* command, so evaluate it. + */ + const command = match[1]; + const evalRes = await vscode.commands.executeCommand(command); + if (typeof evalRes == 'string') { + /** + * Result is a string so wrap it in an array for flattening. + */ + return [evalRes]; + } else if (isNonEmptyStringArray(evalRes)) { + /** + * Return the array result. + */ + return evalRes as string[]; + } else if (isEmptyArray(evalRes)) { + /** + * Not sure if evalRes can be casted to string[] in + * this case so it's easier to just return an empty + * array. + */ + return []; + } else { + /** + * Do not use the evaluated result. The original value + * will be returned below. + */ + } } - - let def: CustomTaskDefinition = { - type: this.taskType, - configuration: { - kind: 'buildMain', - projectFile: projectFile, - main: main.mainRelPath(), - args: [], - }, - }; - let name = getBuildTaskPlainName(main); - const buildMainTask = await createOrResolveTask(def, cmdPrefix, name); - this.tasks?.push(buildMainTask); - - def = { - type: this.taskType, - configuration: { - kind: 'runMain', - projectFile: projectFile, - main: main.mainRelPath(), - mainArgs: [], - }, - }; - name = getRunTaskPlainName(main); - const runMainTask = await createOrResolveTask(def, cmdPrefix, name); - this.tasks?.push(runMainTask); - - def = { - type: this.taskType, - configuration: { - kind: 'buildAndRunMain', - buildTask: getBuildTaskName(main), - runTask: getRunTaskName(main), - }, - }; - name = getBuildAndRunTaskName(main); - const buildAndRunTask = await createOrResolveTask(def, cmdPrefix, name); - this.tasks?.push(buildAndRunTask); } + + return [a]; + }) + ) + ).flat(); + return evaluatedArgs; +} + +/** + * + * @param obj - an object + * @returns true if the given object is a non-empty array of string objects. + */ +function isNonEmptyStringArray(obj: unknown): boolean { + if (obj instanceof Array) { + if (obj.length > 0) { + if (typeof obj[0] == 'string') { + return true; } } - - return this.tasks ?? []; } - async resolveTask( - task: vscode.Task, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _token?: vscode.CancellationToken - ): Promise { - // This is called for tasks that are not fully resolved, in particular - // tasks that don't have an undefined 'execution' property. - const definition = task.definition as CustomTaskDefinition; - - // Check that the task is known - if (definition.configuration.kind in allTaskProperties) { - return createOrResolveTask( - task.definition as CustomTaskDefinition, - await alire(), - undefined, - task - ); - } - return undefined; + return false; +} + +/** + * + * @param obj - an object + * @returns true if the given object is an empty array. + */ +function isEmptyArray(obj: unknown): boolean { + if (obj instanceof Array) { + return obj.length === 0; } + + 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. */ -function getBuildTaskPlainName(main: AdaMain) { - return `${allTaskProperties['buildMain'].title}${main.mainRelPath()}`; +function getBuildTaskPlainName(main?: AdaMain) { + return `Build main - ${main?.mainRelPath() ?? ''}`; } /** * The full name of the build task of a main, including the task type. */ -export function getBuildTaskName(main: AdaMain) { - return `ada: ${getBuildTaskPlainName(main)}`; +export function getBuildTaskName(main?: AdaMain) { + return `${TASK_TYPE_ADA}: ${getBuildTaskPlainName(main)}`; } /** * The name of the run task of a main, without the task type. */ -function getRunTaskPlainName(main: AdaMain) { - return `${allTaskProperties['runMain'].title}${main.mainRelPath()}`; +function getRunTaskPlainName(main?: AdaMain) { + return `Run main - ${main?.mainRelPath() ?? ''}`; } /** * The full name of the build task of a main, including the task type. */ -export function getRunTaskName(main: AdaMain) { - return `ada: ${getRunTaskPlainName(main)}`; +export function getRunTaskName(main?: AdaMain) { + return `${TASK_TYPE_ADA}: ${getRunTaskPlainName(main)}`; } -export function getBuildAndRunTaskName(main: AdaMain) { - return `${allTaskProperties['buildAndRunMain'].title}${main.mainRelPath()}`; +export function getBuildAndRunTaskPlainName(main?: AdaMain) { + return `Build and run main - ${main?.mainRelPath() ?? ''}`; } -export function createSparkTaskProvider(): ConfigurableTaskProvider { - return new ConfigurableTaskProvider( - 'spark', - sparkTaskKinds.map((v) => v) - ); +export function getBuildAndRunTaskName(main?: AdaMain) { + return `${TASK_TYPE_ADA}: ${getBuildAndRunTaskPlainName(main)}`; } -export function createAdaTaskProvider(): ConfigurableTaskProvider { - return new ConfigurableTaskProvider( - 'ada', - adaTaskKinds.map((v) => v) +export function createSparkTaskProvider(): SimpleTaskProvider { + return new SimpleTaskProvider( + TASK_TYPE_SPARK, + predefinedTasks.filter((v) => v.taskDef.type == TASK_TYPE_SPARK) ); } -/** - * - * @param task - the task for which to resolve the full command line - * @param extraArgs - User-provided arguments if the command line is being - * resolved in the context of an explicit task definition in tasks.json - * @returns The full command line after adding common arguments and task-specific arguments. - */ -async function buildFullCommandLine( - name: string, - taskDef: CustomTaskDefinition, - extraArgs?: string[] -): Promise { - const task = allTaskProperties[taskDef.configuration.kind]; - - let cmd = task.command.concat(); - - if (taskDef.configuration.kind != 'runMain') { - // Add project and scenario args - cmd = cmd.concat(await getProjectArgs(taskDef), getScenarioArgs()); - } - - // If the task has a callback to compute extra arguments, call it. This is - // used e.g. to get the current file or location for tasks that call SPARK - // on a specific location. - if (task.extra) { - cmd = cmd.concat(await task.extra()); - } - - const alsProjectFullPath = await getProjectFile(adaExtState.adaClient); - const alsProjectRelPath = vscode.workspace.asRelativePath(alsProjectFullPath); - const taskProject = taskDef.configuration.projectFile; - - const taskProjectIsALSProject: boolean = - [PROJECT_FROM_CONFIG, alsProjectFullPath, alsProjectRelPath].find( - (v) => v == taskProject - ) != undefined; - - // Determine main in the case of tasks based on a main - let adaMain; - switch (taskDef.configuration.kind) { - case 'runMain': - case 'buildMain': { - assert(taskDef.configuration.main); - - if (taskProjectIsALSProject) { - // The task project is the same as the ALS project. Check that the main is found. - adaMain = await getAdaMain(taskDef); - if (adaMain) { - // A matching main was found. Continue normally. - } else { - const msg = - `Task '${name}': ` + - `The specified main '${taskDef.configuration.main}' does not ` + - `match any value of the Mains attribute of the main GPR project: ` + - `${alsProjectRelPath}.`; - void vscode.window.showWarningMessage(msg); - } - } else { - // The specified project is not the same as the ALS project. We - // cannot lookup the main using the ALS. So we can't make any checks. - } - - break; - } - } - - // Add task- and definition-specific args - if (taskDef.configuration.kind == 'buildMain') { - assert(taskDef.configuration.main); - - // Add the main source file to the build command - cmd = cmd.concat([taskDef.configuration.main]); - } - - // Append User args before diagnostic args because the latter use `-cargs` - if (taskDef.configuration.args) { - cmd = cmd.concat(taskDef.configuration.args); - } - if (extraArgs) { - cmd = cmd.concat(extraArgs); - } - - // Append diagnostic args except for gprclean which doesn't need them - if (taskDef.configuration.kind != 'runMain' && cmd[0] != 'gprclean') { - cmd = cmd.concat(getDiagnosticArgs()); - } - - if (taskDef.configuration.kind == 'runMain') { - if (adaMain) { - // Append the main executable's relative path, prepending './' - // (or '.\\' on Windows) when needed, to make sure it's executable from - // a shell. - let execRelPath = adaMain.execRelPath(); - if (!execRelPath.includes(path.sep)) { - execRelPath = '.' + path.sep + execRelPath; - } - - cmd.push(execRelPath); - if (taskDef.configuration.mainArgs) { - cmd = cmd.concat(taskDef.configuration.mainArgs); - } - } else { - assert(taskDef.configuration.main); - - if (taskProjectIsALSProject) { - // The task project is the same as the ALS project, and apparently we were - // unable to find the executable. We already warned about it before. - } else { - // The specified project is not the same as the ALS project. We - // cannot lookup the executable using the ALS. Another task type - // must be used. - const msg = - `Task '${name}': ` + - `The project file specified in this task is different than the workspace ` + - `project. It is not possible to automatically compute the path to the ` + - `executable to run. Please use a task of type 'process' or 'shell' to ` + - `invoke the executable directly.`; - void vscode.window.showWarningMessage(msg); - } - } - } - - // Prepend alire command if available - return alire().then((alr) => { - return alr.concat(cmd); - }); +export function createAdaTaskProvider(): SimpleTaskProvider { + return new SimpleTaskProvider( + TASK_TYPE_ADA, + predefinedTasks.filter((v) => v.taskDef.type == TASK_TYPE_ADA) + ); } /** @@ -786,21 +778,15 @@ export class WarningMessageExecution extends vscode.CustomExecution { } /** - * This task execution implements the 'buildAndRunMain' task kind. It is - * initialized with a 'buildAndRunMain' task definition. When executed, it looks - * up the build tasks and run tasks corresponding to the main targeted by the - * task definition, and runs them in sequence. - * + * This is an abstract class providing the scaffolding for running multiple + * tests in sequence. Child classes can override methods to customize which + * tasks should executed. */ -class BuildAndRunExecution extends vscode.CustomExecution { - buildAndRunDef: CustomTaskDefinition; - - constructor(buildAndRunDef: CustomTaskDefinition) { +abstract class SequentialExecution extends vscode.CustomExecution { + constructor() { super(() => { return this.callback(); }); - assert(buildAndRunDef.configuration.kind == 'buildAndRunMain'); - this.buildAndRunDef = buildAndRunDef; } /** @@ -808,65 +794,18 @@ class BuildAndRunExecution extends vscode.CustomExecution { * * @returns a Pseudoterminal object that controls a Terminal in the VS Code UI. */ - callback(): Thenable { + protected callback(): Thenable { return new Promise((resolve) => { - const definition = this.buildAndRunDef; const writeEmitter = new vscode.EventEmitter(); const closeEmitter = new vscode.EventEmitter(); const pseudoTerminal: vscode.Pseudoterminal = { onDidWrite: writeEmitter.event, onDidClose: closeEmitter.event, - open() { - vscode.tasks - .fetchTasks({ type: 'ada' }) - .then( - (adaTasks) => { - assert(definition.configuration.buildTask); - assert(definition.configuration.runTask); - - /** - * Find the tasks that match the task names - * specified in buildTask and runTask, prioritizing - * Workspace tasks. - */ - adaTasks.sort(workspaceTasksFirst); - /** - * Task names contributed by the extension don't - * have the task type prefix while tasks coming from - * the workspace typically do since VS Code includes - * the type prefix when converting an automatic - * extension task into a configurable workspace - * task. getConventionalTaskLabel() takes care of - * that fact. - */ - function findTaskByName(taskName: string): vscode.Task { - const task = adaTasks.find((v) => { - return taskName == getConventionalTaskLabel(v); - }); - if (task) { - return task; - } else { - const msg = `Could not find a task named: ${taskName}`; - throw new Error(msg); - } - } - const buildMainTask = findTaskByName( - definition.configuration.buildTask - ); - const runMainTask = findTaskByName( - definition.configuration.runTask - ); - - const tasks = [buildMainTask, runMainTask]; - const p = runTaskSequence(tasks, writeEmitter); - - return p; - }, - () => { - writeEmitter.fire('Failed to get list of tasks\r\n'); - return 1; - } - ) + open: () => { + this.getTasksToRun() + .then((tasks) => { + return runTaskSequence(tasks, writeEmitter); + }) .then( (status) => { closeEmitter.fire(status); @@ -890,6 +829,70 @@ class BuildAndRunExecution extends vscode.CustomExecution { resolve(pseudoTerminal); }); } + + protected abstract getTasksToRun(): Promise; +} + +/** + * Finds and returns the task of the given name. + * + * Task names contributed by the extension don't have the task type prefix in + * the name while tasks coming from the workspace typically do since VS Code + * includes the type prefix when converting an automatic extension task into a + * configurable workspace task. This function accounts for that fact by search + * for the task with and without the type prefix. + * + * If an array of tasks is given, the search will applied to this array and the + * API will not be queried for tasks. This can be used to achieve a performance + * boost. + * + * @returns the task that has the given name, or the given name with the + * prefix `ada: ` or `spark: `. + * @throws an Error if the task is not found. + */ +export async function findTaskByName( + taskName: string, + tasks?: vscode.Task[] +): Promise { + if (!tasks) { + tasks = ( + await Promise.all([ + vscode.tasks.fetchTasks({ type: 'ada' }), + vscode.tasks.fetchTasks({ type: 'spark' }), + ]) + ).flat(); + } + + if (tasks.length == 0) { + throw Error('The task list is empty.' + ` Cannot find task '${taskName}'`); + } + + const task = tasks.find((v) => { + return taskName == getConventionalTaskLabel(v) || taskName == v.name; + }); + if (task) { + return task; + } else { + const msg = `Could not find a task named '${taskName}' among the tasks:\n${tasks + .map((t) => t.name) + .join('\n')}`; + throw Error(msg); + } +} + +/** + * This class is a task execution that runs other tasks in sequence. The names + * of the tasks to run are given at construction. + */ +class SequentialExecutionByName extends SequentialExecution { + constructor(private taskName: string, private taskNames: string[]) { + super(); + } + + protected async getTasksToRun(): Promise { + const adaTasks = await vscode.tasks.fetchTasks({ type: TASK_TYPE_ADA }); + return Promise.all(this.taskNames.map((name) => findTaskByName(name, adaTasks))); + } } /** @@ -908,7 +911,7 @@ function runTaskSequence( for (const t of tasks) { p = p.then((status) => { if (status == 0) { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { const disposable = vscode.tasks.onDidEndTaskProcess((e) => { if (e.execution.task == t) { disposable.dispose(); @@ -917,7 +920,11 @@ function runTaskSequence( }); writeEmitter.fire(`Executing task: ${getConventionalTaskLabel(t)}\r\n`); - void vscode.tasks.executeTask(t); + vscode.tasks.executeTask(t).then(undefined, (reason) => { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + writeEmitter.fire(`Could not execute task: ${reason}\r\n`); + reject(reason); + }); }); } else { return status; @@ -942,17 +949,16 @@ const workspaceTasksFirst = (a: vscode.Task, b: vscode.Task): number => { /** * - * @returns Array of tasks of type `ada` and kind `buildAndRunMain`. This - * includes tasks automatically provided by the extension as well as - * user-defined tasks in tasks.json. + * @returns Array of tasks of type `ada` and label starting with 'Build and run + * main -'. This includes tasks automatically provided by the extension as well + * as user-defined tasks in tasks.json. */ export async function getBuildAndRunTasks(): Promise { - return await vscode.tasks.fetchTasks({ type: 'ada' }).then((tasks) => + return await vscode.tasks.fetchTasks({ type: TASK_TYPE_ADA }).then((tasks) => tasks - .filter( - (t) => - (t.definition as CustomTaskDefinition).configuration.kind == 'buildAndRunMain' - ) + // Filter to tasks starting with the conventional name of "Build and run main" tasks + .filter((t) => getConventionalTaskLabel(t).startsWith(getBuildAndRunTaskName())) + // Return workspace-defined tasks first .sort(workspaceTasksFirst) ); @@ -962,37 +968,10 @@ export async function findBuildAndRunTask(adaMain: AdaMain): Promise t.name.replace(/^ada: /, '') == getBuildAndRunTaskName(adaMain) + (t) => getConventionalTaskLabel(t) == getBuildAndRunTaskName(adaMain) ); } -/** - * - * @param taskDef - a task definition with a defined main - * @returns the {@link AdaMain} object representing the main program - */ -async function getAdaMain(taskDef: CustomTaskDefinition): Promise { - assert(taskDef.configuration.main); - const projectMains = await getAdaMains(); - return projectMains.find( - (val) => - val.mainRelPath() == taskDef.configuration.main || - val.mainFullPath == taskDef.configuration.main - ); -} - -/** - * Convert a command line into a list of strongly quoted - * {@link vscode.ShellQuotedString} that would be processed verbatim by any - * shells without interpretation or expansion of special symbols. - * - * @param cmd - a list of strings representing a command line - * @returns a list of strongly quoted {@link ShellQuotedString} - */ -function quoteCommandLine(cmd: string[]): vscode.ShellQuotedString[] { - return cmd.map((v) => ({ value: v, quoting: vscode.ShellQuoting.Strong })); -} - /** * * @param task - a task @@ -1014,15 +993,15 @@ export function getConventionalTaskLabel(task: vscode.Task): string { return isFromWorkspace(task) ? task.name : `${task.source}: ${task.name}`; } -/* - * @returns Array of tasks of type `ada` and kind `buildMain`. This - * includes tasks automatically provided by the extension as well as +/** + * @returns Array of tasks of type `ada` and label starting with 'Build main -'. + * This includes tasks automatically provided by the extension as well as * user-defined tasks in tasks.json. */ export async function getBuildMainTasks() { - return await vscode.tasks.fetchTasks({ type: 'ada' }).then((tasks) => + return await vscode.tasks.fetchTasks({ type: TASK_TYPE_ADA }).then((tasks) => tasks - .filter((t) => (t.definition as CustomTaskDefinition).configuration.kind == 'buildMain') + .filter((t) => getConventionalTaskLabel(t).startsWith(getBuildTaskName())) // Return workspace-defined tasks first .sort(workspaceTasksFirst) ); @@ -1032,6 +1011,76 @@ export async function findBuildMainTask(adaMain: AdaMain): Promise t.name.replace(/^ada: /, '') == getBuildTaskPlainName(adaMain) + (t) => getConventionalTaskLabel(t) == getBuildTaskName(adaMain) ); } + +/** + * Create an updated task that uses the `alr` executable. + * + * If the task matches the pre-defined tasks for building and cleaning the project, + * then the commands used will be respectively `alr build` and `alr clean`. + * + * Otherwise `alr exec -- ...` is used. + * + * @param taskDef - a task definition to update to ALIRE + * @returns a copy of the given task where the `alr` executable is used. + */ +function updateToAlire(taskDef: SimpleTaskDef): SimpleTaskDef { + /** + * Only process shell command tasks, if they are not already using ALIRE + */ + if (taskDef.command && !isAlire(taskDef.command)) { + /** + * Create a copy of the task definition to modify its properties + */ + const newTaskDef = { ...taskDef }; + const command = taskDef.command; + const args = taskDef.args?.concat() ?? []; + + /** + * Change command to alire. No need to use `alr.exe` on Windows, just + * `alr` works. + */ + newTaskDef.command = 'alr'; + + if (taskDef == TASK_BUILD_PROJECT.taskDef) { + /** + * Replace the entire command with `alr build`. Ignore project and + * scenario args because they are managed by ALIRE. + * + * TODO what about -cargs:ada -gnatef ? + */ + args.splice(0, args.length, 'build'); + } else if (taskDef == TASK_CLEAN_PROJECT.taskDef) { + /** + * Replace the entire command with `alr clean`. Ignore project and + * scenario args because they are managed by ALIRE. + */ + args.splice(0, args.length, 'clean'); + } else { + /** + * Use `alr exec` for any other commands. + */ + args.splice(0, 0, 'exec', '--', command); + } + + newTaskDef.args = args; + + return newTaskDef; + } + + return taskDef; +} + +/** + * + * @param command - a string or {@link vscode.ShellQuotedString} from a task definition + * @returns true if the command points to ALIRE, i.e. if it's `alr` or `alr.exe` + * or a path to those executables. + */ +function isAlire(command: string | vscode.ShellQuotedString): boolean { + const value = typeof command == 'string' ? command : command.value; + const commandBasename = basename(value); + return commandBasename.match(/^alr(\.exe)?$/) != null; +} diff --git a/integration/vscode/ada/test/suite/general/tasks.test.ts b/integration/vscode/ada/test/suite/general/tasks.test.ts index 30397a713..d661e3916 100644 --- a/integration/vscode/ada/test/suite/general/tasks.test.ts +++ b/integration/vscode/ada/test/suite/general/tasks.test.ts @@ -1,19 +1,17 @@ +/* eslint-disable max-len */ import assert from 'assert'; -import { existsSync } from 'fs'; +import path from 'path'; import * as vscode from 'vscode'; -import { exe, getProjectFile } from '../../../src/helpers'; +import { getEnclosingSymbol, getSelectedRegion } from '../../../src/commands'; +import { getProjectFile } from '../../../src/helpers'; import { - CustomTaskDefinition, - PROJECT_FROM_CONFIG, + SimpleTaskDef, createAdaTaskProvider, - createSparkTaskProvider, - getEnclosingSymbol, - getSelectedRegion, + getConventionalTaskLabel, } from '../../../src/taskProviders'; -import { activate } from '../utils'; -import path from 'path'; +import { activate, getCmdLine, getCommandLines, runTaskAndGetResult, testTask } from '../utils'; -suite('GPR Tasks Provider', function () { +suite('Task Providers', function () { let projectPath: string; this.beforeAll(async () => { @@ -27,128 +25,62 @@ suite('GPR Tasks Provider', function () { test('Ada tasks list', async () => { const prov = createAdaTaskProvider(); const tasks = await prov.provideTasks(); - assert.notStrictEqual(tasks, undefined); + assert(tasks); const expectedTasksList = ` -ada: Build current project - kind: buildProject -ada: Check current file - kind: checkFile -ada: Clean current project - kind: cleanProject -ada: Build main - src/main1.adb - kind: buildMain -ada: Run main - src/main1.adb - kind: runMain -ada: Build and run main - src/main1.adb - kind: buildAndRunMain -ada: Build main - src/test.adb - kind: buildMain -ada: Run main - src/test.adb - kind: runMain -ada: Build and run main - src/test.adb - kind: buildAndRunMain`.trim(); - - const actualTaskList = tasks - .map( - (t) => - `${t.source}: ${t.name} - kind: ${ - (t.definition as CustomTaskDefinition).configuration.kind - }` - ) - .join('\n'); +ada: Clean current project +ada: Build current project +ada: Check current file +ada: Analyze the project with GNAT SAS +ada: Analyze the current file with GNAT SAS +ada: Create a report after a GNAT SAS analysis +ada: Analyze the project with GNAT SAS and produce a report +ada: Generate documentation from the project +ada: Create/update test skeletons for the project +ada: Build main - src/main1.adb +ada: Run main - src/main1.adb +ada: Build and run main - src/main1.adb +ada: Build main - src/test.adb +ada: Run main - src/test.adb +ada: Build and run main - src/test.adb +`.trim(); + + const actualTaskList = tasks.map((t) => `${t.source}: ${t.name}`).join('\n'); assert.strictEqual(actualTaskList, expectedTasksList); }); - /** - * Check that the list of offered SPARK tasks is expected. - */ - test('Spark tasks list', async () => { - const prov = createSparkTaskProvider(); - const tasks = await prov.provideTasks(); - assert.notStrictEqual(tasks, undefined); - const expectedTasksNames: string[] = [ - 'spark: Clean project for proof', - 'spark: Examine project', - 'spark: Examine file', - 'spark: Examine subprogram', - 'spark: Prove project', - 'spark: Prove file', - 'spark: Prove subprogram', - 'spark: Prove selected region', - 'spark: Prove line', - ]; - assert.strictEqual( - tasks.map((t) => `${t.source}: ${t.name}`).join('\n'), - expectedTasksNames.join('\n') - ); - }); - - test('Automatic clean command', async () => { - const task = (await vscode.tasks.fetchTasks({ type: 'ada' })).find( - (t) => t.name == 'Clean current project' - ); - assert(task); - - /** - * Check the command line of the clean task. - */ - const actualCmd = getCmdLine(task.execution as vscode.ShellExecution); - /** - * The workspace doesn't define an ada.projectFile setting, so the full - * path of the project file is obtained from ALS and used in the command - * line. - */ - const expectedCmd = `gprclean -P ${projectPath}`; - assert.equal(actualCmd, expectedCmd); - - /** - * Now try running the task. - */ - const status = await runTaskAndGetResult(task); - assert.equal(status, 0); - assert(!existsSync('obj/main1' + exe)); - }); - - test('Automatic buildMain command', async () => { - const task = (await vscode.tasks.fetchTasks({ type: 'ada' })).find( - (t) => t.name == 'Build main - src/main1.adb' - ); - assert(task); - - /** - * Check the command line of the build task. - */ - const actualCmd = getCmdLine(task.execution as vscode.ShellExecution); - /** - * The workspace doesn't define an ada.projectFile setting, so the full - * path of the project file is obtained from ALS and used in the command - * line. - */ - const expectedCmd = `gprbuild -P ${projectPath} src/main1.adb -cargs:ada -gnatef`; - assert.equal(actualCmd, expectedCmd); + test('Ada task command lines', async function () { + const expectedCmdLines = ` +ada: Clean current project - gprclean -P ${projectPath} +ada: Build current project - gprbuild -P ${projectPath} -cargs:ada -gnatef +ada: Check current file - gprbuild -q -f -c -u -gnatc -P ${projectPath} \${fileBasename} -cargs:ada -gnatef +ada: Analyze the project with GNAT SAS - gnatsas analyze -P ${projectPath} +ada: Analyze the current file with GNAT SAS - gnatsas analyze -P ${projectPath} --file=\${fileBasename} +ada: Create a report after a GNAT SAS analysis - gnatsas report sarif -P ${projectPath} -o report.sarif +ada: Generate documentation from the project - gnatdoc -P ${projectPath} +ada: Create/update test skeletons for the project - gnattest -P ${projectPath} +ada: Build main - src/main1.adb - gprbuild -P ${projectPath} src/main1.adb -cargs:ada -gnatef +ada: Run main - src/main1.adb - obj/main1exec +ada: Build main - src/test.adb - gprbuild -P ${projectPath} src/test.adb -cargs:ada -gnatef +ada: Run main - src/test.adb - obj/test +`.trim(); - /** - * Now try running the task. - */ - const status = await runTaskAndGetResult(task); - assert.equal(status, 0); - - /** - * Check that the executable is produced. The project defines a - * different name for the executable produced by main1.adb. - */ - assert(vscode.workspace.workspaceFolders); - assert( - existsSync(`${vscode.workspace.workspaceFolders[0].uri.fsPath}/obj/main1exec` + exe) - ); + const prov = createAdaTaskProvider(); + const actualCommandLines = await getCommandLines(prov); + assert.equal(actualCommandLines, expectedCmdLines); }); /** * Check that starting from a User-defined task, the task provider is able * to resolve it into a complete task with the expected command line. */ - test('Resolving task', async () => { + test('Resolving User task', async () => { const prov = createAdaTaskProvider(); - const def: CustomTaskDefinition = { + const def: SimpleTaskDef = { type: 'ada', - configuration: { - kind: 'buildProject', - projectFile: PROJECT_FROM_CONFIG, - args: ['-d'], - }, + command: 'gprbuild', + args: ['${command:ada.gprProjectArgs}', '-d'], }; const task = new vscode.Task(def, vscode.TaskScope.Workspace, 'My Task', 'ada'); const resolved = await prov.resolveTask(task); @@ -161,95 +93,146 @@ ada: Build and run main - src/test.adb - kind: buildAndRunMain`.trim(); const actualCmd = getCmdLine(exec); /** - * This task defines the projectFile field as config:ada.projectFile so - * this is reflected as is in the command line. However running this - * task will fail because the workspace doesn't define the - * ada.projectFile setting. + * The workspace doesn't have the ada.projectFile setting set, so the + * extension will use the full path to the project file obtained from + * the ALS. */ - const expectedCmd = 'gprbuild -P ${config:ada.projectFile} -d -cargs:ada -gnatef'; + const expectedCmd = `gprbuild -P ${projectPath} -d`; assert.strictEqual(actualCmd, expectedCmd); + }); + + test('current regions and subprograms', async () => { + assert(vscode.workspace.workspaceFolders); + const testAdbUri = vscode.Uri.joinPath( + vscode.workspace.workspaceFolders[0].uri, + 'src', + 'test.adb' + ); - const status = runTaskAndGetResult(resolved); /** - * The task should fail because the ada.projectFile is not set so the - * command line cannot be resolved. + * Position the cursor within the nested P3 subprogram */ - assert.notEqual(status, 0); - }); + await vscode.window.showTextDocument(testAdbUri, { + selection: new vscode.Range(17, 13, 17, 13), + }); + assert.deepEqual( + (await getEnclosingSymbol(vscode.window.activeTextEditor, [vscode.SymbolKind.Function])) + ?.range, + // The expected range is that of the inner-most subprogram P3 + new vscode.Range(13, 9, 18, 16) + ); + assert.equal(getSelectedRegion(vscode.window.activeTextEditor), '18:18'); - test('Resolving task buildMain', async () => { - const prov = createAdaTaskProvider(); + /** + * Select a multi-line range + */ + await vscode.window.showTextDocument(testAdbUri, { + selection: new vscode.Range(15, 13, 17, 13), + }); + assert.equal(getSelectedRegion(vscode.window.activeTextEditor), '16:18'); + }); - const def: CustomTaskDefinition = { + test('Obsolete task definition causes error', async function () { + const obsoleteTaskDef: vscode.TaskDefinition = { type: 'ada', - configuration: { - kind: 'buildMain', - projectFile: PROJECT_FROM_CONFIG, - main: 'src/program.adb', - }, + configuration: {}, }; - const task = new vscode.Task(def, vscode.TaskScope.Workspace, 'My Task', 'ada'); - const resolved = await prov.resolveTask(task); - - assert(resolved); - assert(resolved.execution); - - const exec = resolved.execution as vscode.ShellExecution; - const actualCmd = getCmdLine(exec); + const obsoleteTask = new vscode.Task( + obsoleteTaskDef, + vscode.TaskScope.Workspace, + 'Obsolete Task', + 'Workspace' + ); - assert(def.configuration.main); - const expectedCmd = - `gprbuild -P \${config:ada.projectFile} ${def.configuration.main} ` + - `-cargs:ada -gnatef`; + const prov = createAdaTaskProvider(); - assert.strictEqual(actualCmd, expectedCmd); + /** + * Assert that an Error is thrown with the word 'obsolete' in the message. + */ + await assert.rejects(prov.resolveTask(obsoleteTask), /obsolete/); }); - test('Resolving task runMain', async () => { - const prov = createAdaTaskProvider(); - - const def: CustomTaskDefinition = { - type: 'ada', - configuration: { - kind: 'runMain', - projectFile: PROJECT_FROM_CONFIG, - main: 'src/main1.adb', - mainArgs: ['arg1', 'arg2'], + test('Invalid task defs', async function () { + const invalidTaskDefs: SimpleTaskDef[] = [ + { + type: 'ada', }, - }; - const task = new vscode.Task(def, vscode.TaskScope.Workspace, 'My Task', 'ada'); - const resolved = await prov.resolveTask(task); - - assert(resolved); - assert(resolved.execution); - - const exec = resolved.execution as vscode.ShellExecution; - const actualCmd = getCmdLine(exec); + { + type: 'ada', + args: [], + }, + { + type: 'ada', + command: 'cmd', + compound: [], + }, + { + type: 'ada', + args: [], + compound: [], + }, + { + type: 'ada', + command: 'cmd', + args: [], + compound: [], + }, + ]; - // Note that the executable is named differently than the source file - // via project attributes - assert(def.configuration.main); - const expectedCmd = `obj/main1exec${exe} arg1 arg2`; + const prov = createAdaTaskProvider(); + for (const t of invalidTaskDefs) { + const invalidTask = new vscode.Task( + t, + vscode.TaskScope.Workspace, + 'Invalid Task', + 'Workspace' + ); - assert.strictEqual(actualCmd, expectedCmd); + /** + * Assert that an Error is thrown + */ + await assert.rejects(prov.resolveTask(invalidTask)); + } }); +}); - test('buildAndRunMain task', async () => { - const adaTasks = await vscode.tasks.fetchTasks({ type: 'ada' }); - const task = adaTasks.find((v) => v.name == 'Build and run main - src/main1.adb'); - assert(task); +suite('Task Execution', function () { + /** + * Use longer timeout to accomodate for tool invocations + */ + this.timeout('10s'); + + const testedTaskLabels = new Set(); - const execStatus: number | undefined = await runTaskAndGetResult(task); + const allProvidedTasks: vscode.Task[] = []; - assert.equal(execStatus, 0); + this.beforeAll(async () => { + await activate(); + allProvidedTasks.push(...(await createAdaTaskProvider().provideTasks())); }); + declTaskTest('ada: Build current project'); + declTaskTest('ada: Run main - src/main1.adb'); + declTaskTest('ada: Run main - src/test.adb'); + declTaskTest('ada: Check current file', openSrcFile); + declTaskTest('ada: Clean current project'); + declTaskTest('ada: Build main - src/main1.adb'); + declTaskTest('ada: Build main - src/test.adb'); + declTaskTest('ada: Build and run main - src/main1.adb'); + declTaskTest('ada: Build and run main - src/test.adb'); + declTaskTest('ada: Analyze the project with GNAT SAS'); + declTaskTest('ada: Create a report after a GNAT SAS analysis'); + declTaskTest('ada: Analyze the project with GNAT SAS and produce a report'); + declTaskTest('ada: Analyze the current file with GNAT SAS', openSrcFile); + declTaskTest('ada: Generate documentation from the project'); + declTaskTest('ada: Create/update test skeletons for the project'); + /** * Check that the 'buildAndRunMain' task works fine with projects that * do not explicitly define an object directory. */ - test('buildAndRunMain task without object directory', async () => { + test('Build and run main task without object directory', async () => { // Load a custom project that does not define any object dir by // changing the 'ada.projectFile' setting. const initialProjectFile = vscode.workspace.getConfiguration().get('ada.projectFile'); @@ -260,13 +243,15 @@ ada: Build and run main - src/test.adb - kind: buildAndRunMain`.trim(); 'ada.projectFile', 'default_without_obj_dir' + path.sep + 'default_without_obj_dir.gpr' ); - const adaTasks = await vscode.tasks.fetchTasks({ type: 'ada' }); - const task = adaTasks.find((v) => v.name == 'Build and run main - src/main1.adb'); - assert(task); - - // Check that the executable has been ran correctly - const execStatus: number | undefined = await runTaskAndGetResult(task); - assert.equal(execStatus, 0); + /** + * Wait a bit until the ALS loads the new project + */ + await new Promise((resolve) => setTimeout(resolve, 1000)); + await testTask( + 'Build and run main - src/main1.adb', + testedTaskLabels, + allProvidedTasks + ); } finally { // Reset the 'ada.projectFile' setting. If the previous value was // empty, update to 'undefined' so that the setting gets removed. @@ -284,14 +269,11 @@ ada: Build and run main - src/test.adb - kind: buildAndRunMain`.trim(); /** * Test that buildAndRunMain fails when configured with non-existing tasks */ - test('buildAndRunMain failure', async () => { + test('Compound task failure', async () => { const prov = createAdaTaskProvider(); - let def: CustomTaskDefinition = { + let def: SimpleTaskDef = { type: 'ada', - configuration: { - kind: 'buildAndRunMain', - buildTask: 'non existing task', - }, + compound: ['non existing task'], }; let task = new vscode.Task(def, vscode.TaskScope.Workspace, 'Task 1', 'ada'); let resolved = await prov.resolveTask(task); @@ -301,14 +283,14 @@ ada: Build and run main - src/test.adb - kind: buildAndRunMain`.trim(); * build and run tasks is 2. */ assert.equal(await runTaskAndGetResult(resolved), 2); + testedTaskLabels.add(task.name); def = { type: 'ada', - configuration: { - kind: 'buildAndRunMain', - buildTask: 'ada: Build current project', // Existing build task - runTask: 'non existing task', - }, + compound: [ + 'ada: Build current project', // Existing build task + 'non existing task', + ], }; task = new vscode.Task(def, vscode.TaskScope.Workspace, 'Task 2', 'ada'); resolved = await prov.resolveTask(task); @@ -316,102 +298,46 @@ ada: Build and run main - src/test.adb - kind: buildAndRunMain`.trim(); assert.equal(await runTaskAndGetResult(resolved), 2); }); - test('current regions and subprograms', async () => { - assert(vscode.workspace.workspaceFolders); - const testAdbUri = vscode.Uri.joinPath( - vscode.workspace.workspaceFolders[0].uri, - 'src', - 'test.adb' - ); + test('All tasks tested', function () { + const allTaskNames = allProvidedTasks.map(getConventionalTaskLabel); - /** - * Position the cursor within the nested P3 subprogram - */ - await vscode.window.showTextDocument(testAdbUri, { - selection: new vscode.Range(17, 13, 17, 13), - }); - assert.deepEqual( - (await getEnclosingSymbol(vscode.window.activeTextEditor, [vscode.SymbolKind.Function])) - ?.range, - // The expected range is that of the inner-most subprogram P3 - new vscode.Range(13, 9, 18, 16) - ); - assert.equal(getSelectedRegion(vscode.window.activeTextEditor), '18:18'); + const untested = allTaskNames.filter((v) => !testedTaskLabels.has(v)); - /** - * Select a multi-line range - */ - await vscode.window.showTextDocument(testAdbUri, { - selection: new vscode.Range(15, 13, 17, 13), - }); - assert.equal(getSelectedRegion(vscode.window.activeTextEditor), '16:18'); + if (untested.length > 0) { + assert.fail(`${untested.length} tasks were not tested:\n${untested.join('\n')}`); + } }); - test('spark tasks on current location', async () => { - assert(vscode.workspace.workspaceFolders); - const testAdbUri = vscode.Uri.joinPath( - vscode.workspace.workspaceFolders[0].uri, - 'src', - 'test.adb' - ); - - { - await vscode.window.showTextDocument(testAdbUri, { - selection: new vscode.Range(17, 13, 17, 13), - }); - const tasks = await vscode.tasks.fetchTasks({ type: 'spark' }); - const subPTask = tasks.find((t) => t.name == 'Prove subprogram'); - assert(subPTask); - assert(subPTask.execution); - assert.equal( - getCmdLine(subPTask.execution as vscode.ShellExecution), - `gnatprove -j0 -P ${await getProjectFile()} ` + - `--limit-subp=\${fileBasename}:14 -cargs:ada -gnatef` - ); - } + /** + * + * This function makes it easier to declare tests that execute a given task. It + * has to be defined in the same module as the testsuite in order for the + * testing GUI to detect the tests in VS Code. + */ + function declTaskTest(taskName: string, prolog?: () => void | Promise): Mocha.Test { + return test(taskName, async function () { + if (prolog) { + await prolog(); + } - { - await vscode.window.showTextDocument(testAdbUri, { - selection: new vscode.Range(20, 0, 23, 0), - }); /** - * Compute the tasks again after the change of selection + * If there was a prolog, don't use the static task list computed at + * the beginning. Tasks are sensitive to the current cursor location + * so we recompute available tasks instead of using the static task + * list. */ - const tasks = await vscode.tasks.fetchTasks({ type: 'spark' }); - const regionTask = tasks.find((t) => t.name == 'Prove selected region'); - assert(regionTask); - assert(regionTask.execution); - assert.equal( - getCmdLine(regionTask.execution as vscode.ShellExecution), - `gnatprove -j0 -u \${fileBasename} -P ${await getProjectFile()} ` + - `--limit-region=\${fileBasename}:21:24 -cargs:ada -gnatef` - ); - } - }); -}); - -async function runTaskAndGetResult(task: vscode.Task): Promise { - return await new Promise((resolve) => { - const disposable = vscode.tasks.onDidEndTaskProcess((e) => { - if (e.execution.task == task) { - disposable.dispose(); - resolve(e.exitCode); - } + await testTask(taskName, testedTaskLabels, prolog ? undefined : allProvidedTasks); }); + } +}); - void vscode.tasks.executeTask(task); - }); -} +async function openSrcFile() { + assert(vscode.workspace.workspaceFolders); + const testAdbUri = vscode.Uri.joinPath( + vscode.workspace.workspaceFolders[0].uri, + 'src', + 'test.adb' + ); -function getCmdLine(exec: vscode.ShellExecution) { - return [exec.command] - .concat(exec.args) - .map((s) => { - if (typeof s == 'object') { - return s.value; - } else { - return s; - } - }) - .join(' '); + await vscode.window.showTextDocument(testAdbUri); } diff --git a/integration/vscode/ada/test/suite/gnattest/gnattest.test.ts b/integration/vscode/ada/test/suite/gnattest/gnattest.test.ts deleted file mode 100644 index 8ab10b063..000000000 --- a/integration/vscode/ada/test/suite/gnattest/gnattest.test.ts +++ /dev/null @@ -1,410 +0,0 @@ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable max-len */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -import assert from 'assert'; -import { spawnSync } from 'child_process'; -import { - anything, - capture, - instance, - mock, - reset, - resetCalls, - spy, - verify, - when, -} from 'ts-mockito'; -import { TestController, TestItem, TestItemCollection, TestMessage, TestRun } from 'vscode'; -import { adaExtState } from '../../../src/extension'; -import { - collectLeafItems, - collectLeafsFromCollection, - controller, - determineTestOutcome, - getGnatTestDriverExecPath, - getGnatTestDriverProjectPath, - refreshTestItemTree, - resolveHandler, - runHandler, -} from '../../../src/gnattest'; -import { activate } from '../utils'; -import { escapeRegExp } from '../../../src/helpers'; - -suite('GNATtest Integration Tests', function () { - this.beforeAll(async () => { - await activate(); - const cmd = ['gnattest', '-P', await adaExtState.getProjectFile()]; - const cp = spawnSync(cmd[0], cmd.slice(1)); - if (cp.status != 0) { - assert.fail( - `'${cmd.join(' ')}' had status code ${cp.status} and output:\n - ${cp.stdout.toLocaleString()}\n${cp.stderr.toLocaleString()}` - ); - } - }); - - test('Resolving tests', async () => { - /** - * First initialization of the test tree - */ - await refreshTestItemTree(); - - /** - * Check initial test list - */ - assert.equal( - getTestTree(controller), - ` -[speed1.ads] -[speed2.ads] -`.trim() - ); - - /** - * Resolve one item and check the outcome - */ - await resolveHandler(controller.items.get('speed2.ads')); - assert.equal( - getTestTree(controller), - ` -[speed1.ads] -[speed2.ads] - [Desired_Speed 10:13] - [Set_Desired_Speed 12:14] - [Adjust_Speed 16:14] -`.trim() - ); - - /** - * Now perform recursive resolution of the same node. - */ - await resolveHandler(controller.items.get('speed2.ads'), true); - assert.equal( - getTestTree(controller), - ` -[speed1.ads] -[speed2.ads] - [Desired_Speed 10:13] - [speed2.ads:10] - [Set_Desired_Speed 12:14] - [speed2.ads:12] - [Adjust_Speed 16:14] - [speed2.ads:16] -`.trim() - ); - - /** - * Now resolve all nodes recursively - */ - await resolveAllTestTree(); - assert.equal( - getTestTree(controller), - ` -[speed1.ads] - [Speed 12:13] - [speed1.ads:12] - [Adjust_Speed 13:14] - [speed1.ads:13] -[speed2.ads] - [Desired_Speed 10:13] - [speed2.ads:10] - [Set_Desired_Speed 12:14] - [speed2.ads:12] - [Adjust_Speed 16:14] - [speed2.ads:16] -`.trim() - ); - }); - - /** - * This tests the choice of IDs and labels for tests in vscode - */ - test('IDs and labels', async () => { - /** - * Resolve all nodes recursively - * - * Even though column information is available in the GNATtest XML, only - * line information is include in test IDs. So a test ID recognized by - * GNATtest is :. - */ - await resolveAllTestTree(); - assert.equal( - getTestTree(controller, (item) => `[${item.id}] ${item.label}`), - ` -[speed1.ads] Tests for speed1.ads - [Speed 12:13] Tests for subprogram Speed - [speed1.ads:12] speed1.ads:12 - [Adjust_Speed 13:14] Tests for subprogram Adjust_Speed - [speed1.ads:13] speed1.ads:13 -[speed2.ads] Tests for speed2.ads - [Desired_Speed 10:13] Tests for subprogram Desired_Speed - [speed2.ads:10] speed2.ads:10 - [Set_Desired_Speed 12:14] Tests for subprogram Set_Desired_Speed - [speed2.ads:12] speed2.ads:12 - [Adjust_Speed 16:14] Tests for subprogram Adjust_Speed - [speed2.ads:16] speed2.ads:16 -`.trim() - ); - }); - - test('Getting leaf tests to run', async () => { - /** - * Resolve the entire tree - */ - await resolveAllTestTree(); - - /** - * Test on second item - */ - const speed2Item = controller.items.get('speed2.ads') as TestItem; - assert.equal( - testItemArrayToStr(collectLeafItems(speed2Item)), - ` -[speed2.ads:10] -[speed2.ads:12] -[speed2.ads:16] -`.trim() - ); - - /** - * Test on all items - */ - assert.equal( - testItemArrayToStr(collectLeafsFromCollection(controller.items)), - ` -[speed1.ads:12] -[speed1.ads:13] -[speed2.ads:10] -[speed2.ads:12] -[speed2.ads:16] -`.trim() - ); - }); - - test('Parsing test results', async () => { - /** - * Resolve the entire test tree - */ - await resolveAllTestTree(); - - /** - * Consider one of the existing tests - */ - const testItem = collectLeafsFromCollection(controller.items)[0]; - - /** - * Create dummy pass and fail test driver outputs - */ - const driverOutputSuccess = ` -foo -bar -${testItem.id}:4: info: corresponding test PASSED -foo -bar -`.trim(); - const driverOutputFail = ` -foo -bar -${testItem.id}:4: error: some test failure message -foo -bar -`.trim(); - - /** - * Create a mock of the TestRun object to test for specific methods - * being called. - */ - // The mock object keeps track of method calls. - const mockRun = mock(); - // The mock instance should be passed to the implementation. - const run: TestRun = instance(mockRun); - - /** - * Test that parsing a success message causes the passed() method to be - * called - */ - determineTestOutcome(testItem, driverOutputSuccess, run); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - verify(mockRun.passed(testItem, anything())).once(); - - /** - * Test that parsing a failure message causes the failed() method to be - * called - */ - resetCalls(mockRun); - determineTestOutcome(testItem, driverOutputFail, run); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - verify(mockRun.failed(testItem, anything(), anything())).once(); - /** - * Now capture the arguments of the call to failed and check that the - * test failure message was stored correctly. - */ - // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/unbound-method, @typescript-eslint/no-unused-vars - const msg = capture(mockRun.failed).last()[1]; - if (msg instanceof TestMessage) { - assert.equal(msg.message, `${testItem.id}:4: error: some test failure message`); - } else { - throw Error('Could not verify expected message'); - } - - /** - * Test that when no messages concerning the test are found, the test is - * marked as errored. - */ - resetCalls(mockRun); - determineTestOutcome(testItem, 'No output about the test', run); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - verify(mockRun.errored(testItem, anything(), anything())).once(); - - /** - * Multiple pass messages should result in an error - */ - resetCalls(mockRun); - determineTestOutcome(testItem, driverOutputSuccess + driverOutputSuccess, run); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - verify(mockRun.errored(testItem, anything(), anything())).once(); - - /** - * Multiple fail messages should result in an error - */ - resetCalls(mockRun); - determineTestOutcome(testItem, driverOutputFail + driverOutputFail + driverOutputFail, run); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - verify(mockRun.errored(testItem, anything(), anything())).once(); - - /** - * Both pass and fail messages should result in an error - */ - resetCalls(mockRun); - determineTestOutcome( - testItem, - driverOutputFail + driverOutputSuccess + driverOutputFail, - run - ); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - verify(mockRun.errored(testItem, anything(), anything())).once(); - }); - - test('Running tests', async () => { - /** - * Set up a spy on the test controller to stub some methods - */ - const spyCtrl = spy(controller); - try { - /** - * Create a mock TestRun - */ - const mockRun = mock(); - /** - * Capture the output by stubbing the appendOutput() method - */ - let outputs = ''; - when(mockRun.appendOutput(anything())).thenCall((output: string) => { - /** - * Remove '\\r's from the console outputs for easier comparison - */ - outputs += output.replace(/\r\n/g, '\n'); - }); - const run = instance(mockRun); - - /** - * Install a stub in the test controller spy to return the TestRun mock - */ - when(spyCtrl.createTestRun(anything(), anything(), anything())).thenReturn(run); - - /** - * Call the run handler with a request for running all tests. - */ - await runHandler( - { include: undefined, exclude: undefined, profile: undefined }, - undefined - ); - - /** - * Since the gprbuild output can change depending on whether a - * previous test run has already executed gprbuild, we don't check - * for it verbatim. Instead we check that the gprbuild command line - * was called, and that the test driver was called and gave the - * expected output. - */ - const buildOutput = ` -Building the test harness project -$ "gprbuild" "-P" "${await getGnatTestDriverProjectPath()}"`.trimStart(); - const runOutput = ` -Running the test driver -$ "${await getGnatTestDriverExecPath()}" "--passed-tests=show" -speed1.ads:12:4: info: corresponding test PASSED -speed1.ads:13:4: info: corresponding test PASSED -speed2.ads:12:4: info: corresponding test PASSED -speed2.ads:16:4: info: corresponding test PASSED -speed1.ads:12:4: inherited at speed2.ads:20:4: info: corresponding test PASSED -speed2.ads:10:4: error: corresponding test FAILED: Test not implemented. (speed2-auto_controller_test_data-auto_controller_tests.adb:46) -6 tests run: 5 passed; 1 failed; 0 crashed.`.trimStart(); - assert.match(outputs, RegExp(escapeRegExp(buildOutput))); - assert.match(outputs, RegExp(escapeRegExp(runOutput))); - - /** - * Check that calling the run handler with an empty include array - * also yields an execution of all tests. - */ - outputs = ''; - await runHandler({ include: [], exclude: undefined, profile: undefined }, undefined); - assert.match(outputs, RegExp(escapeRegExp(buildOutput))); - assert.match(outputs, RegExp(escapeRegExp(runOutput))); - } finally { - /** - * Reset the controller object on which we set a spy - */ - reset(spyCtrl); - } - }); -}); - -async function resolveAllTestTree() { - const promises: Promise[] = []; - controller.items.forEach((item) => promises.push(resolveHandler(item, true))); - await Promise.all(promises); -} - -function getTestTree( - ctrl: TestController, - testItemToStr: (item: TestItem) => string = defaultTestItemToStr -): string { - return testItemCollToStr(ctrl.items, testItemToStr); -} - -function testItemArrayToStr( - items: TestItem[], - testItemToStr: (items: TestItem) => string = defaultTestItemToStr -): string { - return items.map((item) => itemWithChildrenToStr(item, testItemToStr)).join('\n'); -} - -function testItemCollToStr( - col: TestItemCollection, - testItemToStr: (item: TestItem) => string = defaultTestItemToStr -) { - let res = ''; - col.forEach((item) => { - res += '\n' + itemWithChildrenToStr(item, testItemToStr); - }); - return res.trim(); -} - -function itemWithChildrenToStr(item: TestItem, testItemToStr: (item: TestItem) => string) { - let res = testItemToStr(item); - - if (item.children.size > 0) { - res += '\n' + indentString(testItemCollToStr(item.children, testItemToStr), 2); - } - - return res; -} - -function defaultTestItemToStr(item: TestItem) { - return `[${item.id}]`; -} - -const indentString = (str: string, count: number, indent = ' ') => - str.replace(/^/gm, indent.repeat(count)); diff --git a/integration/vscode/ada/test/suite/utils.ts b/integration/vscode/ada/test/suite/utils.ts index da56ae164..3fdca3b5b 100644 --- a/integration/vscode/ada/test/suite/utils.ts +++ b/integration/vscode/ada/test/suite/utils.ts @@ -1,6 +1,13 @@ import assert from 'assert'; +import { spawnSync } from 'child_process'; import { existsSync, readFileSync, writeFileSync } from 'fs'; import * as vscode from 'vscode'; +import { setTerminalEnvironment } from '../../src/helpers'; +import { + SimpleTaskProvider, + findTaskByName, + getConventionalTaskLabel, +} from '../../src/taskProviders'; /** * This function compares some actual output to an expected referenced stored in @@ -59,3 +66,141 @@ export async function activate(): Promise { */ await ext.activate(); } +export async function getCommandLines(prov: SimpleTaskProvider) { + const tasks = await prov.provideTasks(); + assert(tasks); + + const actualCommandLines = ( + await Promise.all( + tasks.map(async (t) => { + return { task: t, execution: (await prov.resolveTask(t))?.execution }; + }) + ) + ) + .filter(function ({ execution }) { + return execution instanceof vscode.ShellExecution; + }) + .map(function ({ task, execution }) { + assert(execution instanceof vscode.ShellExecution); + return `${task.source}: ${task.name} - ${getCmdLine(execution)}`; + }) + .join('\n'); + return actualCommandLines; +} +export async function runTaskAndGetResult(task: vscode.Task): Promise { + return await new Promise((resolve, reject) => { + let started = false; + + const startDisposable = vscode.tasks.onDidStartTask((e) => { + if (e.execution.task == task) { + /** + * Task was started, let's listen to the end. + */ + started = true; + startDisposable.dispose(); + } + }); + + const endDisposable = vscode.tasks.onDidEndTaskProcess((e) => { + if (e.execution.task == task) { + endDisposable.dispose(); + resolve(e.exitCode); + } + }); + + setTimeout(() => { + /** + * If the task has not started within the timeout below, it means an + * error occured during startup. Reject the promise. + */ + if (!started) { + const msg = `The task '${getConventionalTaskLabel( + task + )}' was not started, likely due to an error.\n`; + reject(Error(msg)); + } + }, 3000); + + void vscode.tasks.executeTask(task); + }).catch(async (reason) => { + if (reason instanceof Error) { + let msg = 'The current list of tasks is:\n'; + msg += await vscode.tasks.fetchTasks({ type: task.definition.type }).then( + (list) => list.map(getConventionalTaskLabel).join('\n'), + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + (reason) => `fetchTasks promise was rejected: ${reason}` + ); + + reason.message += '\n' + msg; + } + + return Promise.reject(reason); + }); +} +export function getCmdLine(exec: vscode.ShellExecution) { + return [exec.command] + .concat(exec.args) + .map((s) => { + if (typeof s == 'object') { + return s.value; + } else { + return s; + } + }) + .join(' '); +} + +export async function testTask( + taskName: string, + testedTasks: Set, + allProvidedTasks?: vscode.Task[] +) { + assert(vscode.workspace.workspaceFolders); + + const task = await findTaskByName(taskName, allProvidedTasks); + assert(task); + testedTasks.add(getConventionalTaskLabel(task)); + + const execStatus: number | undefined = await runTaskAndGetResult(task); + + if (execStatus != 0) { + let msg = `Got status ${execStatus ?? "'undefined'"} for task '${taskName}'`; + if (task.execution instanceof vscode.ShellExecution) { + const cmdLine = [task.execution.command].concat(task.execution.args).map((arg) => { + if (typeof arg == 'string') { + return arg; + } else { + return arg.value; + } + }); + msg += ` with command line: ${cmdLine.join(' ')}`; + + try { + /** + * Let's try re-running the command line explicitlely to obtain its output. + */ + const cwd = vscode.workspace.workspaceFolders[0].uri.fsPath; + msg += `\nTrying to re-run the command explicitly in: ${cwd}`; + const env = { ...process.env }; + setTerminalEnvironment(env); + const cp = spawnSync(cmdLine[0], cmdLine.slice(1), { cwd: cwd, env: env }); + + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + msg += `\nProcess ended with exit code ${cp.status} and output:\n`; + // msg += cp.stdout.toString() + cp.stderr.toString(); + msg += cp.output.map((b) => (b != null ? b.toString() : '')).join(''); + } catch (error) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + msg += `\nEncountered an error: ${error}`; + } + } + + assert.fail(msg); + } +} + +export async function closeAllEditors() { + while (vscode.window.activeTextEditor) { + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + } +} diff --git a/integration/vscode/ada/test/workspaces/general/default.gpr b/integration/vscode/ada/test/workspaces/general/default.gpr index e42f9d360..1102cc34d 100644 --- a/integration/vscode/ada/test/workspaces/general/default.gpr +++ b/integration/vscode/ada/test/workspaces/general/default.gpr @@ -16,4 +16,11 @@ project Default is for Default_Switches ("Ada") use ("-g", "-O0"); end Compiler; + package Documentation is + -- This project contains intentionally invalid code which causes gnatdoc + -- to fail when testing the corresponding vscode task. This gets the + -- project skipped by gnatdoc for the purpose of testing. + for Excluded_Project_Files use ("default.gpr"); + end Documentation; + end Default; diff --git a/integration/vscode/ada/test/workspaces/general/src/bar.ads b/integration/vscode/ada/test/workspaces/general/src/bar.ads index 6d5f57080..1de0dff08 100644 --- a/integration/vscode/ada/test/workspaces/general/src/bar.ads +++ b/integration/vscode/ada/test/workspaces/general/src/bar.ads @@ -1,2 +1,12 @@ package Bar is + procedure A_Procedure; + -- This procedure is added for the purposes of testing the gnattest vscode + -- task. Gnattest generates test skeletons for public subprograms and fails + -- if there are no subprograms to test, hence adding this one. + +private + procedure A_Procedure is null; + -- The implementation is provided in the private part to make the build + -- succeed without causing Gnattest to fail because a null subprogram is not + -- testable. end Bar; diff --git a/integration/vscode/ada/test/workspaces/gnattest/.gitignore b/integration/vscode/ada/test/workspaces/gnattest/.gitignore deleted file mode 100644 index a8ef5e6ee..000000000 --- a/integration/vscode/ada/test/workspaces/gnattest/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Object dir of the project -/obj/ diff --git a/integration/vscode/ada/test/workspaces/gnattest/default.gpr b/integration/vscode/ada/test/workspaces/gnattest/default.gpr deleted file mode 100644 index 46c21e4db..000000000 --- a/integration/vscode/ada/test/workspaces/gnattest/default.gpr +++ /dev/null @@ -1,14 +0,0 @@ -project Default is - - for Source_Dirs use ("src"); - for Languages use ("Ada"); - for Object_Dir use "obj"; - for Library_Dir use "lib"; - for Library_Name use "default"; - for Library_Kind use "static"; - - package Compiler is - for Default_Switches ("ada") use ("-g"); - end Compiler; - -end Default; diff --git a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed1-controller_test_data-controller_tests.adb b/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed1-controller_test_data-controller_tests.adb deleted file mode 100644 index dd91887fc..000000000 --- a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed1-controller_test_data-controller_tests.adb +++ /dev/null @@ -1,126 +0,0 @@ --- This package has been generated automatically by GNATtest. --- You are allowed to add your code to the bodies of test routines. --- Such changes will be kept during further regeneration of this file. --- All code placed outside of test routine bodies will be lost. The --- code intended to set up and tear down the test environment should be --- placed into Speed1.Controller_Test_Data. - -with AUnit.Assertions; use AUnit.Assertions; -with System.Assertions; - --- begin read only --- id:2.2/00/ --- --- This section can be used to add with clauses if necessary. --- --- end read only - --- begin read only --- end read only -package body Speed1.Controller_Test_Data.Controller_Tests is - --- begin read only --- id:2.2/01/ --- --- This section can be used to add global variables and other elements. --- --- end read only - --- begin read only --- end read only - --- begin read only - procedure Test_Speed (Gnattest_T : in out Test_Controller); - procedure Test_Speed_bdc804 (Gnattest_T : in out Test_Controller) renames Test_Speed; --- id:2.2/bdc8045e732efa1b/Speed/1/0/ - procedure Test_Speed (Gnattest_T : in out Test_Controller) is - -- speed1.ads:12:4:Speed --- end read only - - pragma Unreferenced (Gnattest_T); - - begin - - null; - -- AUnit.Assertions.Assert - -- (Gnattest_Generated.Default_Assert_Value, - -- "Test not implemented."); - --- begin read only - end Test_Speed; --- end read only - - --- begin read only - procedure Test_Adjust_Speed (Gnattest_T : in out Test_Controller); - procedure Test_Adjust_Speed_6fd48f (Gnattest_T : in out Test_Controller) renames Test_Adjust_Speed; --- id:2.2/6fd48ff933c1edff/Adjust_Speed/1/0/ - procedure Test_Adjust_Speed (Gnattest_T : in out Test_Controller) is - -- speed1.ads:13:4:Adjust_Speed --- end read only - - pragma Unreferenced (Gnattest_T); - - begin - - null; - -- delay 5.0; - -- AUnit.Assertions.Assert - -- (Adjust_Speed (), - -- "Test not implemented."); - --- begin read only - end Test_Adjust_Speed; --- end read only - - --- begin read only - -- procedure Test_Adjust_Speed (Gnattest_T : in out Test_Controller); - -- procedure Test_Adjust_Speed_cea401 (Gnattest_T : in out Test_Controller) renames Test_Adjust_Speed; --- id:2.2/cea4013b78a4a615/Adjust_Speed/0/1/ - -- procedure Test_Adjust_Speed (Gnattest_T : in out Test_Controller) is --- end read only --- --- pragma Unreferenced (Gnattest_T); --- --- begin --- --- AUnit.Assertions.Assert --- (Gnattest_Generated.Default_Assert_Value, --- "Test not implemented."); --- --- begin read only - -- end Test_Adjust_Speed; --- end read only - - --- begin read only - -- procedure Test_Adjust_Speed (Gnattest_T : in out Test_Controller); - -- procedure Test_Adjust_Speed_d4219e (Gnattest_T : in out Test_Controller) renames Test_Adjust_Speed; --- id:2.2/d4219e26c5c99d2c/Adjust_Speed/0/1/ - -- procedure Test_Adjust_Speed (Gnattest_T : in out Test_Controller) is --- end read only --- --- pragma Unreferenced (Gnattest_T); --- --- begin --- --- AUnit.Assertions.Assert --- (Gnattest_Generated.Default_Assert_Value, --- "Test not implemented."); --- --- begin read only - -- end Test_Adjust_Speed; --- end read only - --- begin read only --- id:2.2/02/ --- --- This section can be used to add elaboration code for the global state. --- -begin --- end read only - null; --- begin read only --- end read only -end Speed1.Controller_Test_Data.Controller_Tests; diff --git a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed1-controller_test_data-controller_tests.ads b/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed1-controller_test_data-controller_tests.ads deleted file mode 100644 index c2bc6ade5..000000000 --- a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed1-controller_test_data-controller_tests.ads +++ /dev/null @@ -1,19 +0,0 @@ --- This package has been generated automatically by GNATtest. --- Do not edit any part of it, see GNATtest documentation for more details. - --- begin read only -with GNATtest_Generated; - -package Speed1.Controller_Test_Data.Controller_Tests is - - type Test_Controller is new - GNATtest_Generated.GNATtest_Standard.Speed1.Controller_Test_Data.Test_Controller with null record; - - procedure Test_Speed_bdc804 (Gnattest_T : in out Test_Controller); - -- speed1.ads:12:4:Speed - - procedure Test_Adjust_Speed_6fd48f (Gnattest_T : in out Test_Controller); - -- speed1.ads:13:4:Adjust_Speed - -end Speed1.Controller_Test_Data.Controller_Tests; --- end read only diff --git a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed1-controller_test_data.adb b/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed1-controller_test_data.adb deleted file mode 100644 index 0b49e2ec5..000000000 --- a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed1-controller_test_data.adb +++ /dev/null @@ -1,19 +0,0 @@ --- This package is intended to set up and tear down the test environment. --- Once created by GNATtest, this package will never be overwritten --- automatically. Contents of this package can be modified in any way --- except for sections surrounded by a 'read only' marker. - -package body Speed1.Controller_Test_Data is - - Local_Controller : aliased GNATtest_Generated.GNATtest_Standard.Speed1.Controller; - procedure Set_Up (Gnattest_T : in out Test_Controller) is - begin - Gnattest_T.Fixture := Local_Controller'Access; - end Set_Up; - - procedure Tear_Down (Gnattest_T : in out Test_Controller) is - begin - null; - end Tear_Down; - -end Speed1.Controller_Test_Data; diff --git a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed1-controller_test_data.ads b/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed1-controller_test_data.ads deleted file mode 100644 index 4fe8f9161..000000000 --- a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed1-controller_test_data.ads +++ /dev/null @@ -1,25 +0,0 @@ --- This package is intended to set up and tear down the test environment. --- Once created by GNATtest, this package will never be overwritten --- automatically. Contents of this package can be modified in any way --- except for sections surrounded by a 'read only' marker. - - -with AUnit.Test_Fixtures; - -with GNATtest_Generated; - -package Speed1.Controller_Test_Data is - - type Controller_Access is access all GNATtest_Generated.GNATtest_Standard.Speed1.Controller'Class; - --- begin read only - type Test_Controller is new AUnit.Test_Fixtures.Test_Fixture --- end read only - with record - Fixture : Controller_Access; - end record; - - procedure Set_Up (Gnattest_T : in out Test_Controller); - procedure Tear_Down (Gnattest_T : in out Test_Controller); - -end Speed1.Controller_Test_Data; diff --git a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed1-test_data-tests.ads b/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed1-test_data-tests.ads deleted file mode 100644 index c0bfa5a9d..000000000 --- a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed1-test_data-tests.ads +++ /dev/null @@ -1,2 +0,0 @@ -package Speed1.Test_Data.Tests is -end Speed1.Test_Data.Tests; diff --git a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed1-test_data.ads b/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed1-test_data.ads deleted file mode 100644 index 15dbe63ad..000000000 --- a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed1-test_data.ads +++ /dev/null @@ -1,2 +0,0 @@ -package Speed1.Test_Data is -end Speed1.Test_Data; diff --git a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed2-auto_controller_test_data-auto_controller_tests.adb b/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed2-auto_controller_test_data-auto_controller_tests.adb deleted file mode 100644 index 59158f8cc..000000000 --- a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed2-auto_controller_test_data-auto_controller_tests.adb +++ /dev/null @@ -1,109 +0,0 @@ --- This package has been generated automatically by GNATtest. --- You are allowed to add your code to the bodies of test routines. --- Such changes will be kept during further regeneration of this file. --- All code placed outside of test routine bodies will be lost. The --- code intended to set up and tear down the test environment should be --- placed into Speed2.Auto_Controller_Test_Data. - -with AUnit.Assertions; use AUnit.Assertions; -with System.Assertions; - --- begin read only --- id:2.2/00/ --- --- This section can be used to add with clauses if necessary. --- --- end read only - --- begin read only --- end read only -package body Speed2.Auto_Controller_Test_Data.Auto_Controller_Tests is - --- begin read only --- id:2.2/01/ --- --- This section can be used to add global variables and other elements. --- --- end read only - --- begin read only --- end read only - --- begin read only - procedure Test_Desired_Speed (Gnattest_T : in out Test_Auto_Controller); - procedure Test_Desired_Speed_3a9813 (Gnattest_T : in out Test_Auto_Controller) renames Test_Desired_Speed; --- id:2.2/3a98136ae8d1ca89/Desired_Speed/1/0/ - procedure Test_Desired_Speed (Gnattest_T : in out Test_Auto_Controller) is - -- speed2.ads:10:4:Desired_Speed --- end read only - - pragma Unreferenced (Gnattest_T); - - begin - - null; - -- delay 10.0; - AUnit.Assertions.Assert - (Gnattest_Generated.Default_Assert_Value, - "Test not implemented."); - --- begin read only - end Test_Desired_Speed; --- end read only - - --- begin read only - procedure Test_Set_Desired_Speed (Gnattest_T : in out Test_Auto_Controller); - procedure Test_Set_Desired_Speed_42cd33 (Gnattest_T : in out Test_Auto_Controller) renames Test_Set_Desired_Speed; --- id:2.2/42cd33c8ea29e2bf/Set_Desired_Speed/1/0/ - procedure Test_Set_Desired_Speed (Gnattest_T : in out Test_Auto_Controller) is - -- speed2.ads:12:4:Set_Desired_Speed --- end read only - - pragma Unreferenced (Gnattest_T); - - begin - - null; - -- delay 4.0; - -- AUnit.Assertions.Assert - -- (Gnattest_Generated.Default_Assert_Value, - -- "Test not implemented."); - --- begin read only - end Test_Set_Desired_Speed; --- end read only - - --- begin read only - procedure Test_Adjust_Speed (Gnattest_T : in out Test_Auto_Controller); - procedure Test_Adjust_Speed_6fd48f (Gnattest_T : in out Test_Auto_Controller) renames Test_Adjust_Speed; --- id:2.2/6fd48ff933c1edff/Adjust_Speed/1/0/ - procedure Test_Adjust_Speed (Gnattest_T : in out Test_Auto_Controller) is - -- speed2.ads:16:4:Adjust_Speed --- end read only - - pragma Unreferenced (Gnattest_T); - - begin - - null; - -- AUnit.Assertions.Assert - -- (Gnattest_Generated.Default_Assert_Value, - -- "Test not implemented."); - --- begin read only - end Test_Adjust_Speed; --- end read only - --- begin read only --- id:2.2/02/ --- --- This section can be used to add elaboration code for the global state. --- -begin --- end read only - null; --- begin read only --- end read only -end Speed2.Auto_Controller_Test_Data.Auto_Controller_Tests; diff --git a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed2-auto_controller_test_data-auto_controller_tests.ads b/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed2-auto_controller_test_data-auto_controller_tests.ads deleted file mode 100644 index b5ce17f1e..000000000 --- a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed2-auto_controller_test_data-auto_controller_tests.ads +++ /dev/null @@ -1,22 +0,0 @@ --- This package has been generated automatically by GNATtest. --- Do not edit any part of it, see GNATtest documentation for more details. - --- begin read only -with GNATtest_Generated; - -package Speed2.Auto_Controller_Test_Data.Auto_Controller_Tests is - - type Test_Auto_Controller is new - GNATtest_Generated.GNATtest_Standard.Speed2.Auto_Controller_Test_Data.Test_Auto_Controller with null record; - - procedure Test_Desired_Speed_3a9813 (Gnattest_T : in out Test_Auto_Controller); - -- speed2.ads:10:4:Desired_Speed - - procedure Test_Set_Desired_Speed_42cd33 (Gnattest_T : in out Test_Auto_Controller); - -- speed2.ads:12:4:Set_Desired_Speed - - procedure Test_Adjust_Speed_6fd48f (Gnattest_T : in out Test_Auto_Controller); - -- speed2.ads:16:4:Adjust_Speed - -end Speed2.Auto_Controller_Test_Data.Auto_Controller_Tests; --- end read only diff --git a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed2-auto_controller_test_data.adb b/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed2-auto_controller_test_data.adb deleted file mode 100644 index 44bbc8de7..000000000 --- a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed2-auto_controller_test_data.adb +++ /dev/null @@ -1,22 +0,0 @@ --- This package is intended to set up and tear down the test environment. --- Once created by GNATtest, this package will never be overwritten --- automatically. Contents of this package can be modified in any way --- except for sections surrounded by a 'read only' marker. - -package body Speed2.Auto_Controller_Test_Data is - - Local_Auto_Controller : aliased GNATtest_Generated.GNATtest_Standard.Speed2.Auto_Controller; - procedure Set_Up (Gnattest_T : in out Test_Auto_Controller) is - begin - GNATtest_Generated.GNATtest_Standard.Speed1.Controller_Test_Data.Controller_Tests.Set_Up - (GNATtest_Generated.GNATtest_Standard.Speed1.Controller_Test_Data.Controller_Tests.Test_Controller (Gnattest_T)); - Gnattest_T.Fixture := Local_Auto_Controller'Access; - end Set_Up; - - procedure Tear_Down (Gnattest_T : in out Test_Auto_Controller) is - begin - GNATtest_Generated.GNATtest_Standard.Speed1.Controller_Test_Data.Controller_Tests.Tear_Down - (GNATtest_Generated.GNATtest_Standard.Speed1.Controller_Test_Data.Controller_Tests.Test_Controller (Gnattest_T)); - end Tear_Down; - -end Speed2.Auto_Controller_Test_Data; diff --git a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed2-auto_controller_test_data.ads b/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed2-auto_controller_test_data.ads deleted file mode 100644 index 36e7ef615..000000000 --- a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed2-auto_controller_test_data.ads +++ /dev/null @@ -1,21 +0,0 @@ --- This package is intended to set up and tear down the test environment. --- Once created by GNATtest, this package will never be overwritten --- automatically. Contents of this package can be modified in any way --- except for sections surrounded by a 'read only' marker. - -with Speed1.Controller_Test_Data.Controller_Tests; - -with GNATtest_Generated; - -package Speed2.Auto_Controller_Test_Data is - --- begin read only - type Test_Auto_Controller is new - GNATtest_Generated.GNATtest_Standard.Speed1.Controller_Test_Data.Controller_Tests.Test_Controller --- end read only - with null record; - - procedure Set_Up (Gnattest_T : in out Test_Auto_Controller); - procedure Tear_Down (Gnattest_T : in out Test_Auto_Controller); - -end Speed2.Auto_Controller_Test_Data; diff --git a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed2-test_data-tests.ads b/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed2-test_data-tests.ads deleted file mode 100644 index 309580fbe..000000000 --- a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed2-test_data-tests.ads +++ /dev/null @@ -1,2 +0,0 @@ -package Speed2.Test_Data.Tests is -end Speed2.Test_Data.Tests; diff --git a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed2-test_data.ads b/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed2-test_data.ads deleted file mode 100644 index ac09bb4ef..000000000 --- a/integration/vscode/ada/test/workspaces/gnattest/obj/gnattest/tests/speed2-test_data.ads +++ /dev/null @@ -1,2 +0,0 @@ -package Speed2.Test_Data is -end Speed2.Test_Data; diff --git a/integration/vscode/ada/test/workspaces/gnattest/src/speed1.adb b/integration/vscode/ada/test/workspaces/gnattest/src/speed1.adb deleted file mode 100644 index 078b5e67b..000000000 --- a/integration/vscode/ada/test/workspaces/gnattest/src/speed1.adb +++ /dev/null @@ -1,17 +0,0 @@ --- --- Copyright (C) 2011-2012, AdaCore --- - -package body Speed1 is - function Speed (This : Controller) return Speed_Type is - begin - return This.Actual_Speed; - end Speed; - - procedure Adjust_Speed - (This : in out Controller; Increment : Speed_Delta) is - begin - This.Actual_Speed := This.Actual_Speed + Increment; - end Adjust_Speed; - -end Speed1; diff --git a/integration/vscode/ada/test/workspaces/gnattest/src/speed1.ads b/integration/vscode/ada/test/workspaces/gnattest/src/speed1.ads deleted file mode 100644 index 895efdd58..000000000 --- a/integration/vscode/ada/test/workspaces/gnattest/src/speed1.ads +++ /dev/null @@ -1,20 +0,0 @@ --- --- Copyright (C) 2011-2012, AdaCore --- - --- manual speed controller definition - -package Speed1 is - subtype Speed_Type is Integer range 0 .. 200; - subtype Speed_Delta is Integer range -5 .. +5; - - type Controller is tagged private; - function Speed (This : Controller) return Speed_Type; - procedure Adjust_Speed (This : in out Controller; Increment : Speed_Delta); - -private - type Controller is tagged record - Actual_Speed : Speed_Type := 0; - end record; - -end Speed1; diff --git a/integration/vscode/ada/test/workspaces/gnattest/src/speed2.adb b/integration/vscode/ada/test/workspaces/gnattest/src/speed2.adb deleted file mode 100644 index d24a39e4b..000000000 --- a/integration/vscode/ada/test/workspaces/gnattest/src/speed2.adb +++ /dev/null @@ -1,24 +0,0 @@ --- --- Copyright (C) 2011-2012, AdaCore --- - -package body Speed2 is - - function Desired_Speed (This : Auto_Controller) return Speed_Type is - begin - return This.Desired_Speed; - end Desired_Speed; - - procedure Set_Desired_Speed - (This : in out Auto_Controller; Val : Speed_Type) is - begin - This.Desired_Speed := Val; - end Set_Desired_Speed; - - procedure Adjust_Speed - (This : in out Auto_Controller; Increment : Speed_Delta) is - begin - null; - end Adjust_Speed; - -end Speed2; diff --git a/integration/vscode/ada/test/workspaces/gnattest/src/speed2.ads b/integration/vscode/ada/test/workspaces/gnattest/src/speed2.ads deleted file mode 100644 index cdfbe414e..000000000 --- a/integration/vscode/ada/test/workspaces/gnattest/src/speed2.ads +++ /dev/null @@ -1,23 +0,0 @@ --- --- Copyright (C) 2011-2012, AdaCore --- - --- derived class violating LSP on adjustSpeed -with Speed1; use Speed1; -package Speed2 is - type Auto_Controller is new Controller with private; - - function Desired_Speed (This : Auto_Controller) return Speed_Type; - - procedure Set_Desired_Speed - (This : in out Auto_Controller; Val : Speed_Type); - --- overriding - procedure Adjust_Speed - (This : in out Auto_Controller; Increment : Speed_Delta); - -private - type Auto_Controller is new Controller with record - Desired_Speed : Speed_Type := 0; - end record; -end Speed2; diff --git a/integration/vscode/ada/test/workspaces/workspace_missing_dirs/workspace.gpr b/integration/vscode/ada/test/workspaces/workspace_missing_dirs/workspace.gpr index 031cd99d9..1233c1c03 100644 --- a/integration/vscode/ada/test/workspaces/workspace_missing_dirs/workspace.gpr +++ b/integration/vscode/ada/test/workspaces/workspace_missing_dirs/workspace.gpr @@ -1,5 +1,5 @@ -- This .gpr file is used to test the command that displays a popup -- to adds source directories not located under the workspace's root folder. project Workspace is - for Source_Dirs use (".", "../general/src/", "../gnattest/"); + for Source_Dirs use (".", "../general/src/highlighting/hello", "../general/src/highlighting/nesting"); end Workspace; diff --git a/source/ada/lsp-ada_configurations.adb b/source/ada/lsp-ada_configurations.adb index b72cfb4ed..dc5dace34 100644 --- a/source/ada/lsp-ada_configurations.adb +++ b/source/ada/lsp-ada_configurations.adb @@ -20,6 +20,9 @@ pragma Ada_2022; with Ada.Containers.Generic_Anonymous_Array_Sort; with GNATCOLL.Traces; +with GNATCOLL.VFS; + +with LSP.Utils; with VSS.JSON.Pull_Readers.Simple; with VSS.JSON.Streams; @@ -71,6 +74,43 @@ package body LSP.Ada_Configurations is From : Positive; Reload : out Boolean); + ---------------- + -- Build_Path -- + ---------------- + + function Build_Path + (Self : Configuration'Class; + File : GPR2.Path_Name.Object) + return GPR2.Path_Name.Object + is + Result : GPR2.Path_Name.Object; + + Relocate_Build_Tree : constant GNATCOLL.VFS.Virtual_File := + LSP.Utils.To_Virtual_File + (Self.Relocate_Build_Tree); + + Root_Dir : constant GNATCOLL.VFS.Virtual_File := + LSP.Utils.To_Virtual_File (Self.Relocate_Root); + + begin + if not Self.Relocate_Build_Tree.Is_Empty then + Result := GPR2.Path_Name.Create (Relocate_Build_Tree); + + if not Self.Relocate_Root.Is_Empty and then File.Is_Defined + then + if not Root_Dir.Is_Absolute_Path then + Result := + GPR2.Path_Name.Create_Directory + (File.Relative_Path + (GPR2.Path_Name.Create (Root_Dir)), + GPR2.Filename_Type + (Result.Value)); + end if; + end if; + end if; + return Result; + end Build_Path; + --------------------------- -- Completion_Formatting -- --------------------------- @@ -217,6 +257,17 @@ package body LSP.Ada_Configurations is Self.Variables_Names := Variables_Names; Self.Variables_Values := Variables_Values; + -- Replace Context with user provided values + Self.Context.Clear; + for J in 1 .. Variables_Names.Length loop + Self.Context.Insert + (GPR2.Optional_Name_Type + (VSS.Strings.Conversions.To_UTF_8_String + (Variables_Names (J))), + VSS.Strings.Conversions.To_UTF_8_String + (Variables_Values (J))); + end loop; + elsif Name = "defaultCharset" and then JSON (Index).Kind = String_Value then diff --git a/source/ada/lsp-ada_configurations.ads b/source/ada/lsp-ada_configurations.ads index d9c142d9b..066a8c3a9 100644 --- a/source/ada/lsp-ada_configurations.ads +++ b/source/ada/lsp-ada_configurations.ads @@ -24,6 +24,9 @@ with VSS.String_Vectors; with LSP.Enumerations; with LSP.Structures; +with GPR2.Context; +with GPR2.Path_Name; + package LSP.Ada_Configurations is type Configuration is tagged private; @@ -93,18 +96,19 @@ package LSP.Ada_Configurations is function Documentation_Style (Self : Configuration'Class) return GNATdoc.Comments.Options.Documentation_Style; - type Variable_List is record - Names : VSS.String_Vectors.Virtual_String_Vector; - Values : VSS.String_Vectors.Virtual_String_Vector; - end record; - - function Scenario_Variables - (Self : Configuration'Class) return Variable_List; - -- Scenario variables, if provided by the user on Configuration/Init - function Display_Method_Ancestry_Policy (Self : Configuration'Class) return LSP.Enumerations.AlsDisplayMethodAncestryOnNavigationPolicy; + function Build_Path + (Self : Configuration'Class; File : GPR2.Path_Name.Object) + return GPR2.Path_Name.Object; + -- Convert Self.Relocate_Build_Tree, Self.Relocate_Root & File to + -- GPR2.Project.Tree.Load procedures Build_Path parameter. + + function Context (Self : Configuration'Class) return GPR2.Context.Object; + -- Convert Configuration scenario variables to + -- GPR2.Project.Tree.Load procedures Context parameter. + function Completion_Formatting return Boolean; -- Used in LSP.Ada_Completions.Pretty_Print_Snippet @@ -146,6 +150,8 @@ private Variables_Names : VSS.String_Vectors.Virtual_String_Vector; Variables_Values : VSS.String_Vectors.Virtual_String_Vector; + + Context : GPR2.Context.Object; end record; function Project_File @@ -164,10 +170,6 @@ private (Self : Configuration'Class) return VSS.Strings.Virtual_String is (Self.Relocate_Root); - function Scenario_Variables - (Self : Configuration'Class) return Variable_List is - ((Self.Variables_Names, Self.Variables_Values)); - function Diagnostics_Enabled (Self : Configuration'Class) return Boolean is (Self.Diagnostics_Enabled); @@ -216,4 +218,7 @@ private (firstTriggerCharacter => 1 * VSS.Characters.Latin.Line_Feed, moreTriggerCharacter => <>); + function Context (Self : Configuration'Class) return GPR2.Context.Object + is (Self.Context); + end LSP.Ada_Configurations; diff --git a/source/ada/lsp-ada_handlers-alire.adb b/source/ada/lsp-ada_handlers-alire.adb index d81c8e7f5..f42a12c40 100644 --- a/source/ada/lsp-ada_handlers-alire.adb +++ b/source/ada/lsp-ada_handlers-alire.adb @@ -77,12 +77,11 @@ package body LSP.Ada_Handlers.Alire is -- Run_Alire -- --------------- - procedure Run_Alire + procedure Determine_Alire_Project (Root : String; Has_Alire : out Boolean; Error : out VSS.Strings.Virtual_String; - Project : out VSS.Strings.Virtual_String; - Environment : in out GPR2.Environment.Object) + Project : out VSS.Strings.Virtual_String) is use type GNAT.OS_Lib.String_Access; @@ -96,10 +95,6 @@ package body LSP.Ada_Handlers.Alire is VSS.Regular_Expressions.To_Regular_Expression (" +Project_File: ([^\n]+)"); - Export_Pattern : constant VSS.Regular_Expressions.Regular_Expression := - VSS.Regular_Expressions.To_Regular_Expression - ("export ([^=]+)=""([^\n]+)"""); - Lines : VSS.String_Vectors.Virtual_String_Vector; begin Project.Clear; @@ -117,66 +112,64 @@ package body LSP.Ada_Handlers.Alire is return; end if; - -- Find project file in `alr show` output - - declare - First : constant VSS.Strings.Virtual_String := Lines (1); - -- We should keep copy of regexp subject string while we have a match - Match : constant VSS.Regular_Expressions.Regular_Expression_Match := - Crate_Pattern.Match (First); - begin - if Match.Has_Match then - Project := Match.Captured (1); - Project.Append (".gpr"); - end if; - end; - + -- Find project file in `alr show` output. There are several cases to cover. + -- + -- If alire.toml contains a "project-files" entry providing a list of + -- projects, `alr show` prints one line per project of the pattern: + -- Project_File: ... + -- + -- Otherwise `alr show` doesn't print a Project_File line and we can expect + -- there to be a project named identically to the crate name. + -- + -- So the strategy below is to first use the crate name as a project + -- name, and then override it if a Project_File line is found (the first + -- one is taken). + + -- When `alr show` is called in a directory where /alire/ and /config/ + -- don't exist, the command auto-generates those directories and prints a + -- few lines of output before the actual crate information. So we can't + -- assume that the crate name will be on the first line. for Line of Lines loop declare - Match : constant VSS.Regular_Expressions.Regular_Expression_Match - := Project_Pattern.Match (Line, Anchored); + -- We should keep copy of regexp subject string while we have a match + Match : constant VSS.Regular_Expressions.Regular_Expression_Match := + Crate_Pattern.Match (Line); begin if Match.Has_Match then Project := Match.Captured (1); + Project.Append (".gpr"); exit; end if; end; end loop; - if Project.Is_Empty then - Error.Append ("No project file is found by alire"); - end if; - - -- Find variables in `alr printenv` output - - Start_Alire - (ALR.all, "--non-interactive", "printenv", Root, Error, Lines); - - GNAT.OS_Lib.Free (ALR); - - -- Find variables in `alr printenv` output - + -- Next check if there is a Project_File line, take the first one. for Line of Lines loop declare Match : constant VSS.Regular_Expressions.Regular_Expression_Match - := Export_Pattern.Match (Line, Anchored); + := Project_Pattern.Match (Line, Anchored); begin if Match.Has_Match then - Environment.Insert - (Key => VSS.Strings.Conversions.To_UTF_8_String - (Match.Captured (1)), - Value => VSS.Strings.Conversions.To_UTF_8_String - (Match.Captured (2))); + Project := Match.Captured (1); + exit; end if; end; end loop; - end Run_Alire; + + if Project.Is_Empty then + Error.Append ("No project file could be determined from the output of `alr show`:"); + for Line of Lines loop + Error.Append (Line); + end loop; + end if; + + end Determine_Alire_Project; --------------- -- Run_Alire -- --------------- - procedure Run_Alire + procedure Setup_Alire_Env (Root : String; Has_Alire : out Boolean; Error : out VSS.Strings.Virtual_String; @@ -221,7 +214,7 @@ package body LSP.Ada_Handlers.Alire is end if; end; end loop; - end Run_Alire; + end Setup_Alire_Env; ----------------- -- Start_Alire -- diff --git a/source/ada/lsp-ada_handlers-alire.ads b/source/ada/lsp-ada_handlers-alire.ads index b3b6b3403..a4455cc10 100644 --- a/source/ada/lsp-ada_handlers-alire.ads +++ b/source/ada/lsp-ada_handlers-alire.ads @@ -24,20 +24,19 @@ with VSS.Strings; private package LSP.Ada_Handlers.Alire is - procedure Run_Alire + procedure Determine_Alire_Project (Root : String; Has_Alire : out Boolean; Error : out VSS.Strings.Virtual_String; - Project : out VSS.Strings.Virtual_String; - Environment : in out GPR2.Environment.Object); + Project : out VSS.Strings.Virtual_String); -- if Root directory contains `alire.toml` file, then run - -- `alr printenv` and fetch the first project from `alire.toml`. + -- `alr show` and determine the project from the output. - procedure Run_Alire + procedure Setup_Alire_Env (Root : String; Has_Alire : out Boolean; Error : out VSS.Strings.Virtual_String; Environment : in out GPR2.Environment.Object); - -- The same as above, but without fetching the project file + -- Run `alr printenv` and set up the obtained environment variables end LSP.Ada_Handlers.Alire; diff --git a/source/ada/lsp-ada_handlers-project_loading.adb b/source/ada/lsp-ada_handlers-project_loading.adb index 459e6ca14..3be60215a 100644 --- a/source/ada/lsp-ada_handlers-project_loading.adb +++ b/source/ada/lsp-ada_handlers-project_loading.adb @@ -18,7 +18,6 @@ with GNATCOLL.Traces; with GNATCOLL.VFS; -with GPR2.Context; with GPR2.Path_Name; with GPR2.Project.View; with GPR2.Containers; @@ -29,7 +28,6 @@ with GPR2.Project.Tree.View_Builder; with Libadalang.Preprocessing; with VSS.Strings.Conversions; -with VSS.String_Vectors; with Spawn.Environments; @@ -40,6 +38,7 @@ with LSP.Ada_Handlers.File_Readers; with LSP.Ada_Indexing; with LSP.Enumerations; with LSP.Structures; +with LSP.Utils; with URIs; with LSP.Ada_Documents; use LSP.Ada_Documents; @@ -54,7 +53,7 @@ package body LSP.Ada_Handlers.Project_Loading is procedure Load_Project_With_Alire (Self : in out Message_Handler'Class; Project_File : VSS.Strings.Virtual_String := ""; - Scenario_Variables : LSP.Ada_Configurations.Variable_List; + Context : GPR2.Context.Object; Charset : VSS.Strings.Virtual_String); -- Core procedure to find project, search path, scenario and load the -- project. @@ -86,26 +85,10 @@ package body LSP.Ada_Handlers.Project_Loading is -- Mark all sources in all projects for indexing. This factorizes code -- between Load_Project and Load_Implicit_Project. - function To_Virtual_String - (Value : GNATCOLL.VFS.Virtual_File) return VSS.Strings.Virtual_String is - (VSS.Strings.Conversions.To_Virtual_String (Value.Display_Full_Name)); - -- Cast Virtual_File to Virtual_String - - function To_Virtual_File - (Value : VSS.Strings.Virtual_String) return GNATCOLL.VFS.Virtual_File is - (GNATCOLL.VFS.Create_From_UTF8 - (VSS.Strings.Conversions.To_UTF_8_String (Value))); - -- Cast Virtual_String to Virtual_File - function Root (Self : Message_Handler'Class) return GNATCOLL.VFS.Virtual_File; -- Return the root directory of the client workspace - type Environment is record - Context : GPR2.Context.Object := GPR2.Context.Empty; - Build_Path : GPR2.Path_Name.Object := GPR2.Path_Name.Undefined; - end record; - --------------------------- -- Ensure_Project_Loaded -- --------------------------- @@ -127,7 +110,7 @@ package body LSP.Ada_Handlers.Project_Loading is Load_Project_With_Alire (Self => Self, Project_File => VSS.Strings.Empty_Virtual_String, - Scenario_Variables => Self.Configuration.Scenario_Variables, + Context => Self.Configuration.Context, Charset => Self.Configuration.Charset); if not Self.Contexts.Is_Empty then @@ -148,7 +131,7 @@ package body LSP.Ada_Handlers.Project_Loading is if X.Has_Suffix (".gpr") then GPRs_Found := GPRs_Found + 1; exit when GPRs_Found > 1; - Project_File := To_Virtual_String (X); + Project_File := LSP.Utils.To_Virtual_String (X); end if; end loop; @@ -169,7 +152,7 @@ package body LSP.Ada_Handlers.Project_Loading is Load_Project (Self => Self, Project_Path => Project_File, - Scenario => Self.Configuration.Scenario_Variables, + Context => Self.Configuration.Context, Environment => GPR2.Environment.Process_Environment, Charset => "iso-8859-1", Status => Single_Project_Found); @@ -243,23 +226,13 @@ package body LSP.Ada_Handlers.Project_Loading is procedure Load_Project (Self : in out Message_Handler'Class; Project_Path : VSS.Strings.Virtual_String; - Scenario : LSP.Ada_Configurations.Variable_List; + Context : GPR2.Context.Object; Environment : GPR2.Environment.Object; Charset : VSS.Strings.Virtual_String; Status : Load_Project_Status) is - use type GNATCOLL.VFS.Virtual_File; - Project_File : GNATCOLL.VFS.Virtual_File := - To_Virtual_File (Project_Path); - - Project_Environment : Project_Loading.Environment; - - Relocate_Build_Tree : constant GNATCOLL.VFS.Virtual_File := - To_Virtual_File (Self.Configuration.Relocate_Build_Tree); - - Root_Dir : constant GNATCOLL.VFS.Virtual_File := - To_Virtual_File (Self.Configuration.Relocate_Root); + LSP.Utils.To_Virtual_File (Project_Path); procedure Create_Context_For_Non_Aggregate (View : GPR2.Project.View.Object); @@ -276,7 +249,7 @@ package body LSP.Ada_Handlers.Project_Loading is use LSP.Ada_Contexts; C : constant Context_Access := - new Context (Self.Tracer); + new LSP.Ada_Contexts.Context (Self.Tracer); Reader : LSP.Ada_Handlers.File_Readers.LSP_File_Reader (Self'Unchecked_Access); @@ -350,37 +323,12 @@ package body LSP.Ada_Handlers.Project_Loading is -- Now load the new project Self.Project_Status.Load_Status := Status; - if not Self.Configuration.Relocate_Build_Tree.Is_Empty then - Project_Environment.Build_Path := - GPR2.Path_Name.Create (Relocate_Build_Tree); - - if not Self.Configuration.Relocate_Root.Is_Empty - and then Project_File /= GNATCOLL.VFS.No_File - then - if not Root_Dir.Is_Absolute_Path then - Project_Environment.Build_Path := - GPR2.Path_Name.Create_Directory - (GPR2.Path_Name.Create (Project_File).Relative_Path - (GPR2.Path_Name.Create (Root_Dir)), - GPR2.Filename_Type - (Project_Environment.Build_Path.Value)); - end if; - end if; - end if; - - -- Update scenario variables with user provided values - for J in 1 .. Scenario.Names.Length loop - Project_Environment.Context.Insert - (GPR2.Optional_Name_Type - (VSS.Strings.Conversions.To_UTF_8_String (Scenario.Names (J))), - VSS.Strings.Conversions.To_UTF_8_String (Scenario.Values (J))); - end loop; - begin Self.Project_Tree.Load_Autoconf (Filename => GPR2.Path_Name.Create (Project_File), - Context => Project_Environment.Context, - Build_Path => Project_Environment.Build_Path, + Context => Context, + Build_Path => LSP.Ada_Configurations.Build_Path + (Self.Configuration, GPR2.Path_Name.Create (Project_File)), Environment => Environment); if Self.Project_Tree.Are_Sources_Loaded then @@ -402,6 +350,15 @@ package body LSP.Ada_Handlers.Project_Loading is -- project. Self.Project_Status.GPR2_Messages := Self.Project_Tree.Log_Messages.all; Self.Project_Status.Project_File := Project_File; + Self.Tracer.Trace ("GPR2 Log Messages:"); + for Msg of Self.Project_Status.GPR2_Messages loop + declare + Location : constant String := Msg.Sloc.Format (Full_Path_Name => True); + Message : constant String := Msg.Message; + begin + Self.Tracer.Trace (Location & " " & Message); + end; + end loop; if Self.Project_Status.Load_Status /= Status or else not Self.Project_Tree.Is_Defined @@ -454,7 +411,7 @@ package body LSP.Ada_Handlers.Project_Loading is procedure Load_Project_With_Alire (Self : in out Message_Handler'Class; Project_File : VSS.Strings.Virtual_String := ""; - Scenario_Variables : LSP.Ada_Configurations.Variable_List; + Context : GPR2.Context.Object; Charset : VSS.Strings.Virtual_String) is @@ -480,24 +437,22 @@ package body LSP.Ada_Handlers.Project_Loading is if Project.Is_Empty then - LSP.Ada_Handlers.Alire.Run_Alire + LSP.Ada_Handlers.Alire.Determine_Alire_Project (Root => Root (Self).Display_Full_Name, Has_Alire => Has_Alire, Error => Errors, - Project => Project, - Environment => Environment); + Project => Project); Status := Alire_Project; - else + end if; - LSP.Ada_Handlers.Alire.Run_Alire - (Root => Root (Self).Display_Full_Name, - Has_Alire => Has_Alire, - Error => Errors, - Environment => Environment); + LSP.Ada_Handlers.Alire.Setup_Alire_Env + (Root => Root (Self).Display_Full_Name, + Has_Alire => Has_Alire, + Error => Errors, + Environment => Environment); - Status := Valid_Project_Configured; - end if; + Status := Valid_Project_Configured; if Has_Alire and then not Errors.Is_Empty then @@ -527,7 +482,7 @@ package body LSP.Ada_Handlers.Project_Loading is Load_Project (Self => Self, Project_Path => Project, - Scenario => Scenario_Variables, + Context => Context, Environment => Environment, Charset => (if Charset.Is_Empty then UTF_8 else Charset), Status => Status); @@ -545,7 +500,7 @@ package body LSP.Ada_Handlers.Project_Loading is Load_Project (Self => Self, Project_Path => Project, - Scenario => Scenario_Variables, + Context => Context, Environment => Environment, Charset => Charset, Status => Valid_Project_Configured); @@ -668,7 +623,7 @@ package body LSP.Ada_Handlers.Project_Loading is Load_Project_With_Alire (Self, Project_File, - Self.Configuration.Scenario_Variables, + Self.Configuration.Context, Self.Configuration.Charset); end if; end Reload_Project; diff --git a/source/ada/lsp-ada_handlers-project_loading.ads b/source/ada/lsp-ada_handlers-project_loading.ads index e767094a0..2c607aada 100644 --- a/source/ada/lsp-ada_handlers-project_loading.ads +++ b/source/ada/lsp-ada_handlers-project_loading.ads @@ -17,8 +17,7 @@ with VSS.Strings; with GPR2.Environment; - -with LSP.Ada_Configurations; +with GPR2.Context; private @@ -27,7 +26,7 @@ package LSP.Ada_Handlers.Project_Loading is procedure Load_Project (Self : in out Message_Handler'Class; Project_Path : VSS.Strings.Virtual_String; - Scenario : LSP.Ada_Configurations.Variable_List; + Context : GPR2.Context.Object; Environment : GPR2.Environment.Object; Charset : VSS.Strings.Virtual_String; Status : Load_Project_Status); diff --git a/source/ada/lsp-utils.adb b/source/ada/lsp-utils.adb index 56a5b080e..7943add03 100644 --- a/source/ada/lsp-utils.adb +++ b/source/ada/lsp-utils.adb @@ -18,8 +18,6 @@ with Ada.Strings.Unbounded; with System; -with GNATCOLL.VFS; - with Libadalang.Common; with Libadalang.Lexer; with Libadalang.Sources; @@ -29,7 +27,6 @@ with Langkit_Support.Token_Data_Handlers; with Pp.Actions; with VSS.Strings.Character_Iterators; -with VSS.Strings.Conversions; with VSS.Strings.Formatters.Generic_Modulars; with VSS.Strings.Formatters.Integers; with VSS.Strings.Templates; diff --git a/source/ada/lsp-utils.ads b/source/ada/lsp-utils.ads index ffdfd2ddb..cf4653e14 100644 --- a/source/ada/lsp-utils.ads +++ b/source/ada/lsp-utils.ads @@ -17,8 +17,9 @@ -- This package provides some utility subprograms. -with VSS.Strings; +with VSS.Strings.Conversions; +with GNATCOLL.VFS; with GPR2.Path_Name; with GPR2.Message; with GPR2.Source_Reference; @@ -119,4 +120,15 @@ package LSP.Utils is -- Convert a GPR2 message into a proper LSP diagnostic, with the right -- severity level and the location reported by GPR2. + function To_Virtual_File + (Value : VSS.Strings.Virtual_String) return GNATCOLL.VFS.Virtual_File is + (GNATCOLL.VFS.Create_From_UTF8 + (VSS.Strings.Conversions.To_UTF_8_String (Value))); + -- Cast Virtual_String to Virtual_File + + function To_Virtual_String + (Value : GNATCOLL.VFS.Virtual_File) return VSS.Strings.Virtual_String is + (VSS.Strings.Conversions.To_Virtual_String (Value.Display_Full_Name)); + -- Cast Virtual_File to Virtual_String + end LSP.Utils; diff --git a/source/gpr/lsp-gpr_did_change_document.adb b/source/gpr/lsp-gpr_did_change_document.adb index 65911bd8a..f74741e6c 100644 --- a/source/gpr/lsp-gpr_did_change_document.adb +++ b/source/gpr/lsp-gpr_did_change_document.adb @@ -104,7 +104,7 @@ package body LSP.GPR_Did_Change_Document is -- Load gpr tree & prepare diagnostics - Self.Document.Load; + Self.Document.Load (Self.Parent.Context.Get_Configuration); -- Build GPR file for LSP needs. diff --git a/source/gpr/lsp-gpr_documents.adb b/source/gpr/lsp-gpr_documents.adb index 2288eadc2..eb4b6c3f4 100644 --- a/source/gpr/lsp-gpr_documents.adb +++ b/source/gpr/lsp-gpr_documents.adb @@ -148,7 +148,9 @@ package body LSP.GPR_Documents is -- Load -- ---------- - procedure Load (Self : in out Document) is + procedure Load + (Self : in out Document; + Configuration : LSP.Ada_Configurations.Configuration) is procedure Update_Diagnostics; -- Update Self.Messages, Self.Errors_Changed, Self.Has_Diagnostics @@ -181,7 +183,8 @@ package body LSP.GPR_Documents is Self.Tree.Load_Autoconf (Filename => Self.File, - Context => Self.Context, + Context => Configuration.Context, + Build_Path => Configuration.Build_Path (Self.File), File_Reader => Self.File_Provider.Get_File_Reader, Environment => Self.Environment); diff --git a/source/gpr/lsp-gpr_documents.ads b/source/gpr/lsp-gpr_documents.ads index d02bc4584..cdb7b0893 100644 --- a/source/gpr/lsp-gpr_documents.ads +++ b/source/gpr/lsp-gpr_documents.ads @@ -25,7 +25,6 @@ with Langkit_Support.Slocs; with GNATCOLL.VFS; -with GPR2.Context; with GPR2.Environment; with GPR2.Log; with GPR2.Path_Name; @@ -35,6 +34,7 @@ with GPR2.Project.Typ; with GPR2.Project.Attribute; with GPR2.Project.Variable; +with LSP.Ada_Configurations; with LSP.Text_Documents; with LSP.GPR_Files; with LSP.GPR_Files.References; @@ -73,7 +73,9 @@ package LSP.GPR_Documents is -- Create a new document from a TextDocumentItem. Use Diagnostic as -- project status diagnostic source. - procedure Load (Self : in out Document); + procedure Load + (Self : in out Document; + Configuration : LSP.Ada_Configurations.Configuration); -- Load associated GPR tree. procedure Cleanup (Self : in out Document); @@ -158,6 +160,8 @@ package LSP.GPR_Documents is -- if Document contains a valid Tree & Reference is a type reference -- returns corresponding value otherwise returns 'Undefined' + function File (Self : Document'Class) return GPR2.Path_Name.Object; + private type Name_Information is record @@ -182,9 +186,6 @@ private Tree : GPR2.Project.Tree.Object; -- The loaded tree - Context : GPR2.Context.Object; - -- GPR scenario variables - File_Provider : LSP.GPR_Files.File_Provider_Access; -- Reader used by GPR2 to access opened documents contents @@ -205,4 +206,8 @@ private -- This set records files with diags previously published. end record; + function File (Self : Document'Class) return GPR2.Path_Name.Object + is (Self.File); + -- 'Self' document's file path + end LSP.GPR_Documents; diff --git a/source/gpr/lsp-gpr_handlers.adb b/source/gpr/lsp-gpr_handlers.adb index ec16db028..a2e8b2c8f 100644 --- a/source/gpr/lsp-gpr_handlers.adb +++ b/source/gpr/lsp-gpr_handlers.adb @@ -221,7 +221,12 @@ package body LSP.GPR_Handlers is -- Load gpr tree & prepare diagnostics - Object.Load; + begin + Object.Load (Self.Get_Configuration); + exception + when E : others => + Self.Tracer.Trace_Exception (E, "On_DidOpen_Notification"); + end; -- Build GPR file for LSP needs. @@ -652,6 +657,22 @@ package body LSP.GPR_Handlers is pragma Warnings (Off, Reload); begin Self.Configuration.Read_JSON (Value.settings, Reload); + + for Document of Self.Open_Documents loop + begin + -- reload gpr tree + Document.Load (Self.Configuration); + + exception + when E : others => + Self.Tracer.Trace_Exception + (E, + VSS.Strings.Conversions.To_Virtual_String + ("On_DidChangeConfiguration_Notification for " & + Document.File.Value)); + + end; + end loop; end On_DidChangeConfiguration_Notification; ------------------------- @@ -711,6 +732,18 @@ package body LSP.GPR_Handlers is end if; end Publish_Diagnostics; + ----------------------- + -- Set_Configuration -- + ----------------------- + + overriding procedure Set_Configuration + (Self : in out Message_Handler; + Value : LSP.Ada_Configurations.Configuration) + is + begin + Self.Configuration := Value; + end Set_Configuration; + -------------- -- To_Range -- -------------- diff --git a/source/gpr/lsp-gpr_handlers.ads b/source/gpr/lsp-gpr_handlers.ads index 5a7e0e9c9..13762b2de 100644 --- a/source/gpr/lsp-gpr_handlers.ads +++ b/source/gpr/lsp-gpr_handlers.ads @@ -118,7 +118,7 @@ private Is_Canceled : Has_Been_Canceled_Function; -- Is request has been canceled - Configuration : LSP.Ada_Configurations.Configuration; + Configuration : LSP.Ada_Configurations.Configuration; -- Ada/GPR configuration settings end record; @@ -200,6 +200,18 @@ private -- If the document is not opened, then it returns a -- VersionedTextDocumentIdentifier with a null version. + ------------------------------------------ + -- LSP.GPR_Job_Contexts.GPR_Job_Context -- + ------------------------------------------ + + overriding function Get_Configuration + (Self : Message_Handler) + return LSP.Ada_Configurations.Configuration is (Self.Configuration); + + overriding procedure Set_Configuration + (Self : in out Message_Handler; + Value : LSP.Ada_Configurations.Configuration); + --------------------------------- -- LSP.GPR_Files.File_Provider -- --------------------------------- diff --git a/source/gpr/lsp-gpr_job_contexts.ads b/source/gpr/lsp-gpr_job_contexts.ads index df36f8ed5..b52d468be 100644 --- a/source/gpr/lsp-gpr_job_contexts.ads +++ b/source/gpr/lsp-gpr_job_contexts.ads @@ -22,6 +22,7 @@ with Ada.Exceptions; with VSS.Strings; +with LSP.Ada_Configurations; with LSP.GPR_Documents; with LSP.GPR_Files; with LSP.Structures; @@ -46,4 +47,12 @@ package LSP.GPR_Job_Contexts is Message : VSS.Strings.Virtual_String := VSS.Strings.Empty_Virtual_String) is abstract; + function Get_Configuration + (Self : GPR_Job_Context) + return LSP.Ada_Configurations.Configuration is abstract; + + procedure Set_Configuration + (Self : in out GPR_Job_Context; + Value : LSP.Ada_Configurations.Configuration) is abstract; + end LSP.GPR_Job_Contexts; diff --git a/testsuite/gpr_lsp/configuration/prj.gpr b/testsuite/gpr_lsp/configuration/prj.gpr new file mode 100644 index 000000000..15d1d00ab --- /dev/null +++ b/testsuite/gpr_lsp/configuration/prj.gpr @@ -0,0 +1,5 @@ +project Prj is +-- leading documentation style +Var := external("GPR_LSP_CONFIGURATION", "default"); +-- gnat documentation style +end Prj; \ No newline at end of file diff --git a/testsuite/gpr_lsp/configuration/test.json b/testsuite/gpr_lsp/configuration/test.json new file mode 100644 index 000000000..3b61bb739 --- /dev/null +++ b/testsuite/gpr_lsp/configuration/test.json @@ -0,0 +1,295 @@ +[ + { + "comment": [ + "test workspace/didChangeConfiguration request works" + ] + }, + { + "start": { + "cmd": [ + "${ALS}", + "--language-gpr" + ] + } + }, + { + "send": { + "request": { + "jsonrpc": "2.0", + "id": "init", + "method": "initialize", + "params": { + "processId": 441587, + "rootUri": "$URI{.}", + "capabilities": { + "workspace": { + "applyEdit": true, + "workspaceEdit": {}, + "didChangeConfiguration": {}, + "didChangeWatchedFiles": {}, + "executeCommand": {} + }, + "textDocument": { + "synchronization": {}, + "completion": { + "dynamicRegistration": true, + "completionItem": { + "snippetSupport": true, + "documentationFormat": [ + "plaintext", + "markdown" + ] + } + }, + "hover": {}, + "signatureHelp": {}, + "declaration": {}, + "definition": {}, + "typeDefinition": {}, + "implementation": {}, + "references": {}, + "documentHighlight": {}, + "documentSymbol": { + "hierarchicalDocumentSymbolSupport": true + }, + "codeLens": {}, + "colorProvider": {}, + "formatting": { + "dynamicRegistration": false + }, + "rangeFormatting": { + "dynamicRegistration": false + }, + "onTypeFormatting": { + "dynamicRegistration": false + }, + "foldingRange": { + "lineFoldingOnly": true + }, + "selectionRange": {}, + "linkedEditingRange": {}, + "callHierarchy": {}, + "moniker": {} + } + } + } + }, + "wait": [ + { + "jsonrpc": "2.0", + "id": "init", + "result": { + "capabilities": { + "textDocumentSync": { + "openClose": true, + "change": 1 + } + } + } + } + ] + } + }, + { + "send": { + "request": { + "jsonrpc": "2.0", + "method": "initialized" + }, + "wait": [] + } + }, + { + "send": { + "request": { + "jsonrpc": "2.0", + "method": "workspace/didChangeConfiguration", + "params": { + "settings": { + "ada": { + "projectFile": "", + "scenarioVariables": {}, + "defaultCharset": "iso-8859-1", + "relocateBuildTree": "", + "rootDir": "", + "onTypeFormatting": { + "indentOnly": true + }, + "documentationStyle": "gnat", + "displayMethodAncestryOnNavigation": "usage_and_abstract_only", + "enableDiagnostics": true, + "foldComments": true, + "namedNotationThreshold": 3, + "useCompletionSnippets": false, + "renameInComments": false, + "enableIndexing": true, + "followSymlinks": true, + "trace": { + "server": "off" + } + } + } + } + }, + "wait": [] + } + }, + { + "send": { + "request": { + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "$URI{prj.gpr}", + "languageId": "Gpr", + "version": 1, + "text": "project Prj is\n-- leading documentation style\nVar := external(\"GPR_LSP_CONFIGURATION\", \"default\");\n-- gnat documentation style\nend Prj;" + } + } + }, + "wait": [] + } + }, + { + "send": { + "request": { + "params": { + "position": { + "line": 2, + "character": 1 + }, + "textDocument": { + "uri": "$URI{prj.gpr}" + } + }, + "jsonrpc": "2.0", + "id": 1, + "method": "textDocument/hover" + }, + "wait": [ + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "contents": [ + { + "language": "gpr", + "value": "Var := \"default\";" + }, + "prj.gpr:3:01", + { + "language": "plaintext", + "value": "-- gnat documentation style" + } + ] + } + } + ] + } + }, + { + "send": { + "request": { + "jsonrpc": "2.0", + "method": "workspace/didChangeConfiguration", + "params": { + "settings": { + "ada": { + "projectFile": "", + "scenarioVariables": { + "GPR_LSP_CONFIGURATION": "from configuration" + }, + "defaultCharset": "iso-8859-1", + "relocateBuildTree": "", + "rootDir": "", + "onTypeFormatting": { + "indentOnly": true + }, + "documentationStyle": "leading", + "displayMethodAncestryOnNavigation": "usage_and_abstract_only", + "enableDiagnostics": true, + "foldComments": true, + "namedNotationThreshold": 3, + "useCompletionSnippets": false, + "renameInComments": false, + "enableIndexing": true, + "followSymlinks": true, + "trace": { + "server": "off" + } + } + } + } + }, + "wait": [] + } + }, + { + "send": { + "request": { + "params": { + "position": { + "line": 2, + "character": 1 + }, + "textDocument": { + "uri": "$URI{prj.gpr}" + } + }, + "jsonrpc": "2.0", + "id": 2, + "method": "textDocument/hover" + }, + "wait": [ + { + "jsonrpc": "2.0", + "id": 2, + "result": { + "contents": [ + { + "language": "gpr", + "value": "Var := \"from configuration\";" + }, + "prj.gpr:3:01", + { + "language": "plaintext", + "value": "-- leading documentation style" + } + ] + } + } + ] + } + }, + { + "send": { + "request": { + "jsonrpc": "2.0", + "id": "shutdown", + "method": "shutdown", + "params": null + }, + "wait": [ + { + "id": "shutdown", + "result": null + } + ] + } + }, + { + "send": { + "request": { + "jsonrpc": "2.0", + "method": "exit" + }, + "wait": [] + } + }, + { + "stop": { + "exit_code": 0 + } + } +] \ No newline at end of file diff --git a/testsuite/gpr_lsp/configuration/test.yaml b/testsuite/gpr_lsp/configuration/test.yaml new file mode 100644 index 000000000..2b3599579 --- /dev/null +++ b/testsuite/gpr_lsp/configuration/test.yaml @@ -0,0 +1,3 @@ +title: 'workspace/didChangeConfiguration request' +skip: + - ['SKIP', 'env.build.os.name not in ("linux","windows")']