Skip to content

Commit 942cceb

Browse files
committed
Implement a CodeLens provider of run and debug actions
1 parent 4710ba4 commit 942cceb

File tree

3 files changed

+265
-3
lines changed

3 files changed

+265
-3
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import {
2+
CancellationError,
3+
CancellationToken,
4+
CodeLens,
5+
CodeLensProvider,
6+
DocumentSymbol,
7+
Event,
8+
ProviderResult,
9+
SymbolKind,
10+
TextDocument,
11+
commands,
12+
} from 'vscode';
13+
import { getMains } from './helpers';
14+
import { CMD_BUILD_AND_DEBUG_MAIN, CMD_BUILD_AND_RUN_MAIN } from './commands';
15+
16+
export class AdaCodeLensProvider implements CodeLensProvider {
17+
static readonly ENABLE_SPARK_CODELENS = false;
18+
19+
onDidChangeCodeLenses?: Event<void> | undefined;
20+
provideCodeLenses(
21+
document: TextDocument,
22+
_token?: CancellationToken
23+
): ProviderResult<CodeLens[]> {
24+
const symbols = commands.executeCommand<DocumentSymbol[]>(
25+
'vscode.executeDocumentSymbolProvider',
26+
document.uri
27+
);
28+
29+
/**
30+
* For main procedures, provide Run and Debug CodeLenses.
31+
*/
32+
const res1 = getMains().then((mains) => {
33+
if (mains.some((m) => m == document.fileName)) {
34+
// It's a main file, so let's offer Run and Debug actions on the main subprogram
35+
return symbols.then((symbols) => {
36+
const functions = symbols.filter((s) => s.kind == SymbolKind.Function);
37+
if (functions.length > 0) {
38+
/**
39+
* We choose to provide the CodeLenses on the first
40+
* subprogram of the file. It may be possible that the
41+
* main subprogram is not the first one, but that's an
42+
* unlikely scenario that we choose not to handle for
43+
* the moment.
44+
*/
45+
return [
46+
new CodeLens(functions[0].range, {
47+
command: CMD_BUILD_AND_RUN_MAIN,
48+
title: '$(run) Run',
49+
arguments: [document.uri],
50+
}),
51+
// TODO implement this command
52+
new CodeLens(functions[0].range, {
53+
command: CMD_BUILD_AND_DEBUG_MAIN,
54+
title: '$(debug-alt-small) Debug',
55+
arguments: [document.uri],
56+
}),
57+
];
58+
} else {
59+
return [];
60+
}
61+
});
62+
} else {
63+
return [];
64+
}
65+
});
66+
67+
let res2;
68+
if (AdaCodeLensProvider.ENABLE_SPARK_CODELENS) {
69+
/**
70+
* This is tentative deactivated code in preparation of SPARK support.
71+
*/
72+
res2 = symbols.then<CodeLens[]>((symbols) => {
73+
// Create a named reduce function to implement a recursive visit of symbols
74+
const reduce = (acc: DocumentSymbol[], cur: DocumentSymbol) => {
75+
if (_token?.isCancellationRequested) {
76+
throw new CancellationError();
77+
}
78+
if (cur.kind == SymbolKind.Function) {
79+
// Include functions in the accumulated result
80+
acc.push(cur);
81+
}
82+
83+
// Search for Functions among the children of these symbol kinds
84+
if (
85+
[SymbolKind.Module, SymbolKind.Package, SymbolKind.Function].includes(
86+
cur.kind
87+
)
88+
) {
89+
cur.children.reduce(reduce, acc);
90+
}
91+
92+
return acc;
93+
};
94+
95+
// Collect functions recursively
96+
const functions = symbols.reduce(reduce, []);
97+
98+
return functions.map((f) => {
99+
if (_token?.isCancellationRequested) {
100+
throw new CancellationError();
101+
}
102+
// TODO make SPARK codelenses conditional to the availability of SPARK on PATH
103+
return new CodeLens(f.range, {
104+
title: '$(play-circle) Prove',
105+
command: 'TODO',
106+
});
107+
});
108+
});
109+
} else {
110+
res2 = Promise.resolve([]);
111+
}
112+
113+
return Promise.all([res1, res2]).then((results) => {
114+
return results[0].concat(results[1]);
115+
});
116+
}
117+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
118+
resolveCodeLens?(codeLens: CodeLens, _token: CancellationToken): ProviderResult<CodeLens> {
119+
if (codeLens.command) {
120+
return codeLens;
121+
} else {
122+
throw new Error(`Cannot resolve CodeLens`);
123+
}
124+
}
125+
}

integration/vscode/ada/src/ExtensionState.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export class ExtensionState {
3030

3131
private registeredTaskProviders: Disposable[];
3232

33+
public readonly codelensProvider = new AdaCodeLensProvider();
34+
3335
constructor(context: vscode.ExtensionContext) {
3436
this.context = context;
3537
this.gprClient = createClient(
@@ -55,6 +57,9 @@ export class ExtensionState {
5557
public start = async () => {
5658
await Promise.all([this.gprClient.start(), this.adaClient.start()]);
5759
this.registerTaskProviders();
60+
this.context.subscriptions.push(
61+
vscode.languages.registerCodeLensProvider('ada', this.codelensProvider)
62+
);
5863
};
5964

6065
public dispose = () => {

integration/vscode/ada/src/commands.ts

Lines changed: 135 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,36 @@ import { SymbolKind } from 'vscode';
55
import { Disposable } from 'vscode-jsonrpc';
66
import { ExecuteCommandRequest } from 'vscode-languageclient';
77
import { ExtensionState } from './ExtensionState';
8-
import { getOrAskForProgram } from './debugConfigProvider';
9-
import { adaExtState, mainOutputChannel } from './extension';
10-
import { getProjectFileRelPath } from './helpers';
8+
import { AdaConfig, getOrAskForProgram, initializeConfig } from './debugConfigProvider';
9+
import { adaExtState, logger, mainOutputChannel } from './extension';
10+
import { findAdaMain, getProjectFileRelPath } from './helpers';
1111
import {
1212
CustomTaskDefinition,
13+
findBuildAndRunTask,
1314
getBuildAndRunTasks,
1415
getConventionalTaskLabel,
1516
getEnclosingSymbol,
1617
isFromWorkspace,
1718
} from './taskProviders';
1819

20+
/**
21+
* Identifier for a hidden command used for building and running a project main.
22+
* The command accepts a parameter which is the URI of the main source file.
23+
* It is triggered by CodeLenses provided by the extension.
24+
*
25+
* @see {@link buildAndRunSpecifiedMain}
26+
*/
27+
export const CMD_BUILD_AND_RUN_MAIN = 'ada.buildAndRunMain';
28+
29+
/**
30+
* Identifier for a hidden command used for building and debugging a project main.
31+
* The command accepts a parameter which is the URI of the main source file.
32+
* It is triggered by CodeLenses provided by the extension.
33+
*
34+
* @see {@link buildAndDebugSpecifiedMain}
35+
*/
36+
export const CMD_BUILD_AND_DEBUG_MAIN = 'ada.buildAndDebugMain';
37+
1938
export function registerCommands(context: vscode.ExtensionContext, clients: ExtensionState) {
2039
context.subscriptions.push(vscode.commands.registerCommand('ada.otherFile', otherFileHandler));
2140
context.subscriptions.push(
@@ -66,6 +85,17 @@ export function registerCommands(context: vscode.ExtensionContext, clients: Exte
6685
}
6786
)
6887
);
88+
89+
/**
90+
* The following commands are not defined in package.json and hence not
91+
* exposed through the command palette but are called from CodeLenses.
92+
*/
93+
context.subscriptions.push(
94+
vscode.commands.registerCommand(CMD_BUILD_AND_RUN_MAIN, buildAndRunSpecifiedMain)
95+
);
96+
context.subscriptions.push(
97+
vscode.commands.registerCommand(CMD_BUILD_AND_DEBUG_MAIN, buildAndDebugSpecifiedMain)
98+
);
6999
}
70100
/**
71101
* Add a subprogram box above the subprogram enclosing the cursor's position, if any.
@@ -439,3 +469,105 @@ export async function checkSrcDirectories(atStartup = false, displayYesNoPopup =
439469
}
440470
}
441471
}
472+
473+
/*
474+
* This is a command handler that builds and runs the main given as parameter.
475+
* If the given URI does not match one of the project Mains an error is
476+
* displayed.
477+
*
478+
* @param main - a URI of a document
479+
*/
480+
async function buildAndRunSpecifiedMain(main: vscode.Uri): Promise<void> {
481+
const adaMain = await findAdaMain(main.fsPath);
482+
if (adaMain) {
483+
const task = await findBuildAndRunTask(adaMain);
484+
if (task) {
485+
lastUsedTaskInfo = { source: task.source, name: task.name };
486+
await vscode.tasks.executeTask(task);
487+
} else {
488+
void vscode.window.showErrorMessage(
489+
`Could not find the 'Build and Run' task for the project main ` +
490+
`${adaMain.mainRelPath()}`,
491+
{ modal: true }
492+
);
493+
}
494+
} else {
495+
void vscode.window.showErrorMessage(
496+
`The document ${vscode.workspace.asRelativePath(main)} does not match ` +
497+
`any of the Mains of the project ${await getProjectFileRelPath()}`,
498+
{ modal: true }
499+
);
500+
}
501+
}
502+
503+
/**
504+
* This is a command handler that builds the main given as parameter and starts
505+
* a debug session on that main. If the given URI does not match one of the
506+
* project Mains an error is displayed.
507+
*
508+
* @param main - a URI of a document
509+
*/
510+
async function buildAndDebugSpecifiedMain(main: vscode.Uri): Promise<void> {
511+
function isMatchingConfig(cfg: vscode.DebugConfiguration, configToMatch: AdaConfig): boolean {
512+
return cfg.type == configToMatch.type && cfg.name == configToMatch.name;
513+
}
514+
515+
const wsFolder = vscode.workspace.getWorkspaceFolder(main);
516+
const adaMain = await findAdaMain(main.fsPath);
517+
if (adaMain) {
518+
/**
519+
* The vscode API doesn't provide a way to list both automatically
520+
* provided and User-defined debug configurations. So instead, we
521+
* inspect the launch.json data if it exists, and the dynamic configs
522+
* provided by the exctension. We look for a debug config that matches
523+
* the given main URI.
524+
*/
525+
// Create a launch config for this main to help with matching
526+
const configToMatch = initializeConfig(adaMain);
527+
logger.debug('Debug config to match:\n' + JSON.stringify(configToMatch, null, 2));
528+
529+
let matchingConfig = undefined;
530+
531+
{
532+
// Find matching config in the list of workspace-defined launch configs
533+
const configs: vscode.DebugConfiguration[] =
534+
vscode.workspace.getConfiguration('launch').get('configurations') ?? [];
535+
logger.debug(`Workspace debug configurations:\n${JSON.stringify(configs, null, 2)}`);
536+
matchingConfig = configs.find((cfg) => isMatchingConfig(cfg, configToMatch));
537+
}
538+
539+
if (!matchingConfig) {
540+
logger.debug('Could not find matching config in workspace configs.');
541+
// Look for a matching config among the configs dynamically provided by the extension
542+
const dynamicConfigs =
543+
await adaExtState.dynamicDebugConfigProvider.provideDebugConfigurations();
544+
logger.debug(
545+
`Dynamic debug configurations:\n${JSON.stringify(dynamicConfigs, null, 2)}`
546+
);
547+
matchingConfig = dynamicConfigs.find((cfg) => isMatchingConfig(cfg, configToMatch));
548+
}
549+
550+
if (matchingConfig) {
551+
logger.debug('Found matching config: ' + JSON.stringify(matchingConfig, null, 2));
552+
const success = await vscode.debug.startDebugging(wsFolder, matchingConfig);
553+
if (!success) {
554+
void vscode.window.showErrorMessage(
555+
`Failed to start debug configuration: ${matchingConfig.name}`
556+
);
557+
}
558+
} else {
559+
logger.error('Could not find matching config');
560+
void vscode.window.showErrorMessage(
561+
`Could not find a debug configuration for the project main ` +
562+
`${adaMain.mainRelPath()}`,
563+
{ modal: true }
564+
);
565+
}
566+
} else {
567+
void vscode.window.showErrorMessage(
568+
`The document ${vscode.workspace.asRelativePath(main)} does not match ` +
569+
`any of the Mains of the project ${await getProjectFileRelPath()}`,
570+
{ modal: true }
571+
);
572+
}
573+
}

0 commit comments

Comments
 (0)