diff --git a/integration/vscode/ada/package.json b/integration/vscode/ada/package.json index 0ff7fbd6b..7a3a70557 100644 --- a/integration/vscode/ada/package.json +++ b/integration/vscode/ada/package.json @@ -476,7 +476,10 @@ }, "args": { "type": "array", - "description": "Extra command line arguments" + "items": { + "type": "string" + }, + "description": "Extra build command line arguments" } }, "oneOf": [ @@ -535,6 +538,13 @@ "executable": { "type": "string", "description": "Path to main executable file (if it cannot be computed automatically)" + }, + "mainArgs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Arguments passed to the main executable invocation" } }, "additionalProperties": false @@ -574,6 +584,9 @@ }, "args": { "type": "array", + "items": { + "type": "string" + }, "description": "Extra command line arguments" } }, @@ -630,7 +643,8 @@ }, { "command": "als-reload-project", - "title": "Ada: Reload project" + "title": "Ada: Reload project", + "icon": "$(refresh)" }, { "command": "ada.subprogramBox", @@ -638,17 +652,29 @@ }, { "command": "ada.showExtensionOutput", - "title": "Ada: Show Output", + "title": "Ada: Show extension output", "when": "ADA_PROJECT_CONTEXT" }, { "command": "ada.showAdaLSOutput", - "title": "Ada: Show Ada Language Server Output", + "title": "Ada: Show Ada Language Server output", "when": "ADA_PROJECT_CONTEXT" }, { "command": "ada.showGprLSOutput", - "title": "Ada: Show GPR Language Server Output", + "title": "Ada: Show GPR Language Server output", + "when": "ADA_PROJECT_CONTEXT" + }, + { + "command": "ada.runMainAsk", + "title": "Ada: Build and run project main...", + "icon": "$(run-all)", + "when": "ADA_PROJECT_CONTEXT" + }, + { + "command": "ada.runMainLast", + "title": "Ada: Build and run last used main", + "icon": "$(run)", "when": "ADA_PROJECT_CONTEXT" } ], @@ -674,6 +700,18 @@ "command": "ada.subprogramBox", "when": "ADA_PROJECT_CONTEXT" } + ], + "editor/title/run": [ + { + "command": "ada.runMainLast", + "when": "editorLangId == ada", + "group": "navigation@0" + }, + { + "command": "ada.runMainAsk", + "when": "editorLangId == ada", + "group": "navigation@1" + } ] }, "walkthroughs": [ diff --git a/integration/vscode/ada/src/clients.ts b/integration/vscode/ada/src/clients.ts index 75eccb5c3..19b6714a5 100644 --- a/integration/vscode/ada/src/clients.ts +++ b/integration/vscode/ada/src/clients.ts @@ -10,7 +10,7 @@ import { import { logger } from './extension'; import GnatTaskProvider from './gnatTaskProvider'; import GprTaskProvider from './gprTaskProvider'; -import { logErrorAndThrow } from './helpers'; +import { logErrorAndThrow, setCustomEnvironment } from './helpers'; import { registerTaskProviders } from './taskProviders'; export class ContextClients { @@ -145,15 +145,15 @@ function createClient( logger.debug(`Using ALS at: ${serverExecPath}`); - // The debug options for the server - // let debugOptions = { execArgv: [] }; - // If the extension is launched in debug mode then the debug server options are used - // Otherwise the run options are used + // Copy this process's environment + const serverEnv: NodeJS.ProcessEnv = { ...process.env }; + // Set custom environment + setCustomEnvironment(serverEnv); // Options to control the server const serverOptions: ServerOptions = { - run: { command: serverExecPath, args: extra }, - debug: { command: serverExecPath, args: extra }, + run: { command: serverExecPath, args: extra, options: { env: serverEnv } }, + debug: { command: serverExecPath, args: extra, options: { env: serverEnv } }, }; // Options to control the language client diff --git a/integration/vscode/ada/src/commands.ts b/integration/vscode/ada/src/commands.ts index 20e4b93f1..b6dff6944 100644 --- a/integration/vscode/ada/src/commands.ts +++ b/integration/vscode/ada/src/commands.ts @@ -1,9 +1,13 @@ +import assert from 'assert'; +import { existsSync } from 'fs'; import * as vscode from 'vscode'; import { SymbolKind } from 'vscode'; +import { Disposable } from 'vscode-jsonrpc'; import { ContextClients } from './clients'; import { getOrAskForProgram } from './debugConfigProvider'; import { mainOutputChannel } from './extension'; -import { getEnclosingSymbol } from './taskProviders'; +import { getProjectFileRelPath } from './helpers'; +import { CustomTaskDefinition, getEnclosingSymbol } from './taskProviders'; export function registerCommands(context: vscode.ExtensionContext, clients: ContextClients) { context.subscriptions.push( @@ -26,6 +30,14 @@ export function registerCommands(context: vscode.ExtensionContext, clients: Cont ) ); + context.subscriptions.push( + vscode.commands.registerCommand('ada.runMainLast', () => runMainLast()) + ); + + context.subscriptions.push( + vscode.commands.registerCommand('ada.runMainAsk', () => runMainAsk()) + ); + // This is a hidden command that gets called in the default debug // configuration snippet that gets offered in the launch.json file. context.subscriptions.push( @@ -89,3 +101,222 @@ async function addSupbrogramBoxCommand() { } }); } + +let lastUsedTaskInfo: { source: string; name: string } | undefined; + +/** + * If a task was previously run through the commands `ada.runMainAsk` or + * `ada.runMainLast`, re-run the same task. If not, defer to {@link runMainAsk} + * to ask the User to select a task to run. + * + * @returns the TaskExecution corresponding to the task. + */ +async function runMainLast() { + const buildAndRunTasks = await getBuildAndRunTasks(); + if (lastUsedTaskInfo) { + const matchingTasks = buildAndRunTasks.filter(matchesLastUsedTask); + assert(matchingTasks.length <= 1); + const lastTask = matchingTasks.length == 1 ? matchingTasks[0] : undefined; + if (lastTask) { + return await vscode.tasks.executeTask(lastTask); + } + } + + // No task was run so far, or the last one run no longer exists + return runMainAsk(); +} + +/** + * + * @param t - a task + * @returns `true` if the given task matches the last executed task + */ +function matchesLastUsedTask(t: vscode.Task): boolean { + return t.source == lastUsedTaskInfo?.source && t.name == lastUsedTaskInfo?.name; +} + +/** + * + * @param task - a task + * @returns the label to be displayed to the user in the quick picker for that task + */ +function getTaskLabel(task: vscode.Task): string { + return isFromWorkspace(task) ? `(From Workspace) ${task.name}` : getConventionalTaskLabel(task); +} + +/** + * + * @param task - a task + * @returns the label typically generated for that task by vscode. For tasks not + * defined explicitely in the workspace, this is `ada: `. For tasks + * defined in the workspace simply return the name which should already include + * the convention. + */ +function getConventionalTaskLabel(task: vscode.Task): string { + return isFromWorkspace(task) ? task.name : `${task.source}: ${task.name}`; +} + +/** + * + * @param task - a task + * @returns `true` if the task is defined explicitely in the workspace's tasks.json + */ +function isFromWorkspace(task: vscode.Task) { + return task.source == 'Workspace'; +} + +interface TaskQuickPickItem extends vscode.QuickPickItem { + task: vscode.Task; +} + +/** + * Propose to the User a list of build and run tasks, one for each main defined + * in the project. + * + * Tasks defined explicitely in the workspace are identified as such in the + * offered list and proposed first. + * + * The User can choose either to run the task as is, or click the secondary + * button to add the task to tasks.json (if not already there) and configure it + * there. + */ +async function runMainAsk() { + function createQuickPickItem(task: vscode.Task): TaskQuickPickItem { + return { + // Mark the last used task with a leading star + label: (matchesLastUsedTask(task) ? '$(star) ' : '') + getTaskLabel(task), + // Add a description to the last used task + description: matchesLastUsedTask(task) ? 'last used' : undefined, + task: task, + // Add a button allowing to configure the task in tasks.json + buttons: [ + { + iconPath: new vscode.ThemeIcon('gear'), + tooltip: 'Configure task in tasks.json, e.g. to add main arguments', + }, + ], + }; + } + const adaTasksMain = await getBuildAndRunTasks(); + + if (adaTasksMain.length > 0) { + const tasksFromWorkspace = adaTasksMain.filter(isFromWorkspace); + const tasksFromExtension = adaTasksMain.filter((v) => !isFromWorkspace(v)); + + // Propose workspace-configured tasks first + const quickPickItems: TaskQuickPickItem[] = tasksFromWorkspace.map(createQuickPickItem); + + if (tasksFromWorkspace.length > 0) { + // Use a separator between workspace tasks and implicit tasks provided by the extension + quickPickItems.push({ + kind: vscode.QuickPickItemKind.Separator, + label: '', + // Use any valid task to avoid allowing 'undefined' in the type declaration + task: adaTasksMain[0], + }); + } + + quickPickItems.push(...tasksFromExtension.map(createQuickPickItem)); + + // Create the quick picker + const qp = vscode.window.createQuickPick(); + qp.items = qp.items.concat(quickPickItems); + + // Array for event handlers to be disposed after the quick picker is disposed + const disposables: Disposable[] = []; + try { + const choice: TaskQuickPickItem | undefined = await new Promise((resolve) => { + // Add event handlers to the quick picker + disposables.push( + qp.onDidChangeSelection((items) => { + // When the User selects an option, resolve the Promise + // and hide the quick picker + const item = items[0]; + if (item) { + resolve(item); + qp.hide(); + } + }), + qp.onDidHide(() => { + resolve(undefined); + }), + qp.onDidTriggerItemButton(async (e) => { + // When the User selects the secondary button, find or + // create the task in the tasks.json file + + // There's only one button, so let's assert that + assert(e.item.buttons && e.item.buttons[0]); + assert(e.button == e.item.buttons[0]); + + const tasks: vscode.TaskDefinition[] = + vscode.workspace.getConfiguration('tasks').get('tasks') ?? []; + + // Check if the task is already defined in tasks.json + if (!tasks.find((t) => t?.label == getConventionalTaskLabel(e.item.task))) { + // If the task doesn't exist, create it + + // Copy the definition and add a label + const def: CustomTaskDefinition = { + ...(e.item.task.definition as CustomTaskDefinition), + label: getConventionalTaskLabel(e.item.task), + }; + tasks.push(def); + await vscode.workspace.getConfiguration().update('tasks.tasks', tasks); + } + + // Then open tasks.json in an editor + if (vscode.workspace.workspaceFolders) { + const tasksUri = vscode.workspace.workspaceFolders + .map((ws) => vscode.Uri.joinPath(ws.uri, '.vscode', 'tasks.json')) + .find((v) => existsSync(v.fsPath)); + if (tasksUri) { + await vscode.window.showTextDocument(tasksUri); + } + } + resolve(undefined); + qp.hide(); + }) + ); + + // Show the quick picker + qp.show(); + }); + + if (choice) { + // If a task was selected, mark it as the last executed task and + // run it + lastUsedTaskInfo = { + source: choice.task.source, + name: choice.task.name, + }; + return await vscode.tasks.executeTask(choice.task); + } else { + return undefined; + } + } finally { + disposables.forEach((d) => d.dispose()); + } + } else { + void vscode.window.showWarningMessage( + `There are no Mains defined in the workspace project ${await getProjectFileRelPath()}` + ); + return undefined; + } +} + +/** + * + * @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. + */ +async function getBuildAndRunTasks() { + return await vscode.tasks + .fetchTasks({ type: 'ada' }) + .then((tasks) => + tasks.filter( + (t) => + (t.definition as CustomTaskDefinition).configuration.kind == 'buildAndRunMain' + ) + ); +} diff --git a/integration/vscode/ada/src/extension.ts b/integration/vscode/ada/src/extension.ts index 7dac29b36..2f3926326 100644 --- a/integration/vscode/ada/src/extension.ts +++ b/integration/vscode/ada/src/extension.ts @@ -33,7 +33,6 @@ import { assertSupportedEnvironments, getCustomEnvSettingName, getEvaluatedCustomEnv, - setCustomEnvironment, startedInDebugMode, } from './helpers'; @@ -124,9 +123,8 @@ async function activateExtension(context: vscode.ExtensionContext) { // Log the environment that the extension (and all VS Code) will be using const customEnv = getEvaluatedCustomEnv(); - if (customEnv && Object.keys(customEnv).length > 0) { - logger.info('Setting environment variables:'); + logger.info('Custom environment variables:'); for (const varName in customEnv) { const varValue: string = customEnv[varName]; logger.info(`${varName}=${varValue}`); @@ -135,13 +133,6 @@ async function activateExtension(context: vscode.ExtensionContext) { logger.debug('No custom environment variables set in %s', getCustomEnvSettingName()); } - // Set the custom environment into the current node process. This must be - // done before calling assertSupportedEnvironments in order to set the ALS - // environment variable if provided. - setCustomEnvironment(); - - assertSupportedEnvironments(logger); - // Create the Ada and GPR clients. contextClients = new ContextClients(context); diff --git a/integration/vscode/ada/src/helpers.ts b/integration/vscode/ada/src/helpers.ts index 01e1a08d5..2801702f6 100644 --- a/integration/vscode/ada/src/helpers.ts +++ b/integration/vscode/ada/src/helpers.ts @@ -158,10 +158,15 @@ export function getEvaluatedCustomEnv() { /** * Read the environment variables specified in the vscode setting - * `terminal.integrated.env.` and set them in the current node process so - * that they become inherited by any child processes. + * `terminal.integrated.env.` and set them in the given ProcessEnv object. + * + * If no targetEnv is given, `process.env` is used as a target environment. */ -export function setCustomEnvironment() { +export function setCustomEnvironment(targetEnv?: NodeJS.ProcessEnv) { + if (!targetEnv) { + targetEnv = process.env; + } + // Retrieve the user's custom environment variables if specified in their // settings/workspace: we'll then launch any child process with this custom // environment @@ -170,13 +175,17 @@ export function setCustomEnvironment() { if (custom_env) { for (const var_name in custom_env) { const var_value: string = custom_env[var_name]; - process.env[var_name] = var_value; + targetEnv[var_name] = var_value; } } } export function assertSupportedEnvironments(mainChannel: winston.Logger) { - if (process.env.ALS) { + // Get the ALS environment variable from the custom environment, or from the + // process environment + const customEnv = getEvaluatedCustomEnv(); + const als = customEnv?.ALS ?? process.env.ALS; + if (als) { // The User provided an external ALS executable. Do not perform any // platform support checks because we may be on an unsupported platform // where the User built and provided ALS. @@ -224,16 +233,28 @@ export function logErrorAndThrow(msg: string, logger: winston.Logger) { * Get the project file from the workspace configuration if available, or from * the ALS if not. * - * @param client - the client to send the request to - * @returns a string contains the path of the project file + * @param client - the client to send the request to. If not provided, the main + * Ada client of the extension is used. + * @returns the full path of the currently loaded project file */ -export async function getProjectFile(client: LanguageClient): Promise { +export async function getProjectFile(client?: LanguageClient): Promise { + if (!client) { + client = contextClients.adaClient; + } const result: string = (await client.sendRequest(ExecuteCommandRequest.type, { command: 'als-project-file', })) as string; return result; } +/** + * + * @returns The path of the project file loaded by the ALS relative to the workspace + */ +export async function getProjectFileRelPath(): Promise { + return vscode.workspace.asRelativePath(await getProjectFile()); +} + /** * Get the Object Directory path * @param client - the client to send the request to diff --git a/integration/vscode/ada/src/taskProviders.ts b/integration/vscode/ada/src/taskProviders.ts index f435d2e82..7ff014b69 100644 --- a/integration/vscode/ada/src/taskProviders.ts +++ b/integration/vscode/ada/src/taskProviders.ts @@ -127,6 +127,7 @@ export interface CustomTaskDefinition extends vscode.TaskDefinition { args?: string[]; main?: string; executable?: string; + mainArgs?: string[]; }; } @@ -621,6 +622,9 @@ async function buildFullCommandLine( // The "executable" property is either set explicitly by the // User, or automatically by querying the ALS in previous code. cmd = cmd.concat('&&', taskDef.configuration.executable); + if (taskDef.configuration.mainArgs) { + cmd = cmd.concat(taskDef.configuration.mainArgs); + } } else { if (taskProjectIsALSProject) { // The task project is the same as the ALS project, and apparently we were