Skip to content

Commit a8e83cb

Browse files
Merge branch 'topic/gnatmetric-int' into 'master'
Add GNATmetric integration using CodeLenses See merge request eng/ide/ada_language_server!2236
2 parents 5f2b672 + 0fa3b7c commit a8e83cb

15 files changed

Lines changed: 619 additions & 99 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ section below it for the last release. -->
1111
* Highlight `finally` keyword (from Ada extension) with semantic tokens
1212
* Prompt the user to restart the Ada Language Server when the workspace `.als.json` configuration file is modified
1313
* Two color themes **GNAT Studio Light** and **GNAT Studio Dark**
14+
* Add a predefined task to compute file metrics and display them automatically via CodeLenses
1415

1516
## 2026.1.202601121
1617

392 KB
Loading

doc/media/gnatmetric-vscode.gif

228 KB
Loading

doc/settings.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ Settings taken into account only from the Ada & SPARK VS Code extension:
9696

9797
* [showNotificationsOnErrors](#shownotificationsonerrors)
9898
* [trace.server](#traceserver)
99+
* [metricThresholds](#metricthresholds)
99100

100101
Settings understood by the Ada Language Server itself, independently from the LSP client:
101102

@@ -147,6 +148,28 @@ On the server side this option does not trigger any additional logging.
147148

148149
An equivalent setting `gpr.trace.server` exists for tracing the communcation between VS Code and the GPR language server.
149150

151+
### metricThresholds
152+
153+
Configurable thresholds for metrics provided by `gnatmetric`. Each key is a metric name (e.g., `cyclomatic_complexity`, `code_lines`), and the value is an object with optional `warn` and `error` numeric thresholds.
154+
Metric names can be retrieved from the XML metric files generated by `gnatmetric`, which by default are located under the project's object directory (`.metrics.xml` files).
155+
156+
Below is an example configuration for setting metric thresholds to highlight subprograms with high cyclomatic complexity:
157+
158+
```json
159+
{
160+
"ada.metricThresholds":
161+
{
162+
"cyclomatic_complexity":
163+
{
164+
"warn": 10,
165+
"error": 20
166+
}
167+
}
168+
}
169+
```
170+
171+
If not set, no thresholds will be applied.
172+
150173
### projectFile
151174

152175
You can configure the GNAT Project File via the `projectFile` key.

doc/vscode-ug.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ This extension **does not include a compiler nor additional tools**. Nonetheless
2727
| | Code Coverage ||
2828
| **GNAT SAS** | | |
2929
| | Static Analysis ||
30+
| | Metrics ||
3031
| **SPARK** | | |
3132
| | Formal Proof ||
3233

@@ -314,6 +315,18 @@ The GNATtest integration in VS Code also supports running tests in coverage mode
314315

315316
Integrating the steps of source instrumentation and test harness build into the test execution workflow allows for a quick feedback loop: run a test, observe results and coverage, edit the test or the tested code, repeat... In this context invoking the VS Code commands `Test: Rerun Last Run` and `Test: Rerun Last Run with Coverage` with their respective keyboard shortcuts can be valuable.
316317

318+
## GNAT Metrics Support
319+
320+
The extension provides a predefined task called `Compute metrics for current file`, which runs `gnatmetric` and displays file metrics directly in the editor using CodeLenses.
321+
322+
![GNAT Metrics CodeLenses](media/gnatmetric-vscode.gif)
323+
324+
By default, the displayed metrics include code complexity and lines of code. You can customize which metrics are shown by adjusting the command-line options for `gnatmetric` in the task configuration.
325+
326+
You can configure thresholds for specific metrics to highlight when they are exceeded via the `ada.metricThresholds` [VS Code setting](./settings.md#metricthresholds). The extension will display warning or error diagnostics for each violation, and the corresponding CodeLenses will show warning or error icons as appropriate.
327+
328+
![GNAT Metrics Thresholds](media/gnatmetric-thresholds.png)
329+
317330
## Cross and Embedded Support
318331

319332
This section provides some guidance to work on cross or embedded projects. It assumes
@@ -489,15 +502,15 @@ The VS Code extension has a few limitations and some differences compared to [GN
489502
* **Indentation/formatting**: it does not support automatic indentation when adding a newline and range/document
490503
formatting might no succeed on incomplete/illegal code.
491504

492-
* **Tooling support**: we currently provide support for some _SPARK_, _GNATtest_, _GNATcoverage_, _GNAT SAS_ and _GNATemulator_ [Tasks](#tasks), but some workflows may not be supported yet.
505+
* **Tooling support**: we currently provide support for some _SPARK_, _GNATtest_, _GNATcoverage_, _GNAT SAS_, _GNATmetric_ and _GNATemulator_ [Tasks](#tasks), but some workflows may not be supported yet.
493506

494507
* **Alire support**: if the root folder contains an `alire.toml` file and
495508
there is `alr` executable in the `PATH`, then the language server fetches
496509
the project's search path, environment variables and the project's file
497510
name from the crate description. [Tasks](#tasks) are also automatically
498511
invoked with Alire in this case.
499512

500-
* **Project support**: there is no `Scenario` view: users should configure scenarios via the _ada.scenarioVariables* setting (see the settings list available [here](./settings.md)). Saving the settings file after changing the values will automatically reload the project and update the
513+
* **Project support**: there is no `Scenario` view: users should configure scenarios via the `ada.scenarioVariables` setting (see the settings list available [here](./settings.md)). Saving the settings file after changing the values will automatically reload the project and update the
501514
predefined tasks to take into account the new scenario values.
502515

503516
Source directories from imported projects should be added in a [workspace file](https://code.visualstudio.com/docs/editor/workspaces#_multiroot-workspaces). If you already have a workspace file, the extension will propose you to automatically add all the source directories coming from imported projects to your workspace automatically at startup.

integration/vscode/ada/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,12 @@
564564
],
565565
"default": null,
566566
"markdownDescription": "Enable editing Ada comments to update references to an entity when it is being renamed.\n\nIf not set in VS Code, this setting takes its value from the [`.als.json`](https://github.com/AdaCore/ada_language_server/blob/master/doc/settings.md) file at the root of the workspace, if that file exists. Otherwise it defaults to `false`."
567+
},
568+
"ada.metricThresholds": {
569+
"scope": "window",
570+
"type": "object",
571+
"default": null,
572+
"markdownDescription": "Configurable thresholds for metrics provided by `gnatmetric`. Each key is a metric name (e.g., `cyclomatic_complexity`, `code_lines`), and the value is an object with optional 'warn' and 'error' numeric thresholds.\nMetric names can be retrieved from the XML metric files generated by `gnatmetric`, which by default are located under the project's object directory (`.metrics.xml` files).\n\nIf not set, no thresholds will be applied."
567573
}
568574
}
569575
},
@@ -1616,4 +1622,4 @@
16161622
"ws": "^8.18.0",
16171623
"yaml": "^2.8.0"
16181624
}
1619-
}
1625+
}

integration/vscode/ada/schemas/als-settings-schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,12 @@
139139
"type": "integer",
140140
"default": 10,
141141
"description": "Controls the maximum number of trace files preserved in the ALS log directory (which defaults to `~/.als`). When this threshold is reached, old trace files get deleted automatically. The default number of preserved trace files is `10`."
142+
},
143+
"metricThresholds": {
144+
"scope": "window",
145+
"type": "object",
146+
"default": null,
147+
"markdownDescription": "Configurable thresholds for metrics provided by `gnatmetric`. Each key is a metric name (e.g., `cyclomatic_complexity`, `code_lines`), and the value is an object with optional 'warn' and 'error' numeric thresholds.\nMetric names can be retrieved from the XML metric files generated by `gnatmetric`, which by default are located under the project's object directory (`.metrics.xml` files).\n\nIf not set, no thresholds will be applied."
142148
}
143149
},
144150
"additionalProperties": false

integration/vscode/ada/src/AdaCodeLensProvider.ts

Lines changed: 79 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -19,123 +19,110 @@ import {
1919
CMD_SPARK_PROVE_SUBP,
2020
} from './constants';
2121
import { envHasExec, getSymbols } from './helpers';
22+
import { findMetricsXmlForSource, formatMetric, parseMetricsXml } from './metricsUtils';
23+
import * as vscode from 'vscode';
2224
import { adaExtState } from './extension';
2325

2426
export class AdaCodeLensProvider implements CodeLensProvider {
25-
onDidChangeCodeLenses?: Event<void> | undefined;
26-
provideCodeLenses(
27+
private emitter = new vscode.EventEmitter<void>();
28+
readonly onDidChangeCodeLenses = this.emitter.event;
29+
30+
async provideCodeLenses(
2731
document: TextDocument,
2832
token?: CancellationToken,
29-
): ProviderResult<CodeLens[]> {
30-
const symbols = commands.executeCommand<DocumentSymbol[] | undefined>(
33+
): Promise<CodeLens[]> {
34+
const symbols = await commands.executeCommand<DocumentSymbol[] | undefined>(
3135
'vscode.executeDocumentSymbolProvider',
3236
document.uri,
3337
);
3438

35-
/**
36-
* For main procedures, provide Run and Debug CodeLenses.
37-
*/
38-
const res1 = adaExtState.getTargetPrefix().then((targetPrefix) => {
39-
return adaExtState.getMains().then((mains) => {
40-
if (
41-
mains.some(
42-
(m) =>
43-
// Here we go through the Uri class to benefit from the normalization
44-
// of path casing on Windows. See Uri.fsPath documentation.
45-
Uri.file(m).fsPath == document.uri.fsPath,
46-
)
47-
) {
48-
// It's a main file, so let's offer Run and Debug actions on the main subprogram
49-
return symbols.then((symbols) => {
50-
if (!symbols) {
51-
return [];
52-
}
39+
// For main procedures, provide Run and Debug CodeLenses.
40+
const targetPrefix = await adaExtState.getTargetPrefix();
41+
const mains = await adaExtState.getMains();
42+
let codeLenses: CodeLens[] = [];
43+
if (mains.some((m) => Uri.file(m).fsPath === document.uri.fsPath)) {
44+
if (symbols) {
45+
const functions = symbols.filter((s) => s.kind === SymbolKind.Function);
46+
if (functions.length > 0) {
47+
codeLenses = [
48+
new CodeLens(functions[0].range, {
49+
command: CMD_BUILD_AND_RUN_MAIN,
50+
title: '$(run) Run',
51+
arguments: [document.uri],
52+
}),
53+
new CodeLens(functions[0].range, {
54+
command: CMD_BUILD_AND_DEBUG_MAIN,
55+
title: '$(debug-alt-small) Debug',
56+
arguments: [document.uri],
57+
}),
58+
];
59+
if (targetPrefix && envHasExec(targetPrefix + '-gnatemu')) {
60+
codeLenses = codeLenses.concat([
61+
new CodeLens(functions[0].range, {
62+
command: CMD_BUILD_AND_RUN_GNATEMULATOR,
63+
title: '$(run) Run with GNATemulator',
64+
arguments: [document.uri],
65+
}),
66+
new CodeLens(functions[0].range, {
67+
command: CMD_BUILD_AND_DEBUG_GNATEMULATOR,
68+
title: '$(debug-alt-small) Debug with GNATemulator',
69+
arguments: [document.uri],
70+
}),
71+
]);
72+
}
73+
}
74+
}
75+
}
5376

54-
const functions = symbols.filter((s) => s.kind == SymbolKind.Function);
55-
if (functions.length > 0) {
56-
/**
57-
* We choose to provide the CodeLenses on the first
58-
* subprogram of the file. It may be possible that the
59-
* main subprogram is not the first one, but that's an
60-
* unlikely scenario that we choose not to handle for
61-
* the moment.
62-
*/
63-
let codeLenses = [
64-
new CodeLens(functions[0].range, {
65-
command: CMD_BUILD_AND_RUN_MAIN,
66-
title: '$(run) Run',
67-
arguments: [document.uri],
68-
}),
69-
new CodeLens(functions[0].range, {
70-
command: CMD_BUILD_AND_DEBUG_MAIN,
71-
title: '$(debug-alt-small) Debug',
72-
arguments: [document.uri],
77+
// --- Metrics CodeLens ---
78+
const objectDir = await adaExtState.getObjectDir();
79+
const metricsXml = findMetricsXmlForSource(document.uri.fsPath, objectDir);
80+
if (metricsXml) {
81+
const parsed = await parseMetricsXml(metricsXml);
82+
if (parsed && Array.isArray(parsed.units)) {
83+
const { units, displayNames } = parsed;
84+
for (const unit of units) {
85+
if (unit.sloc) {
86+
const metrics: string[] = [];
87+
for (const [key, value] of Object.entries(unit.metrics)) {
88+
const label = displayNames[key] || key;
89+
metrics.push(formatMetric(key, label, Number(value)));
90+
}
91+
if (metrics.length > 0) {
92+
const summary = `$(graph) Metrics: ${metrics.join(', ')}`;
93+
codeLenses.push(
94+
new CodeLens(new vscode.Range(unit.sloc, unit.sloc), {
95+
title: summary,
96+
command: '',
7397
}),
74-
];
75-
76-
// It's not a native project: provide a 'Run with GNATemulator' CodeLens
77-
// if GNATemulator for the given target is present in the user's env.
78-
if (targetPrefix && envHasExec(targetPrefix + '-gnatemu')) {
79-
codeLenses = codeLenses.concat([
80-
new CodeLens(functions[0].range, {
81-
command: CMD_BUILD_AND_RUN_GNATEMULATOR,
82-
title: '$(run) Run with GNATemulator',
83-
arguments: [document.uri],
84-
}),
85-
new CodeLens(functions[0].range, {
86-
command: CMD_BUILD_AND_DEBUG_GNATEMULATOR,
87-
title: '$(debug-alt-small) Debug with GNATemulator',
88-
arguments: [document.uri],
89-
}),
90-
]);
91-
}
92-
93-
return codeLenses;
94-
} else {
95-
return [];
98+
);
9699
}
97-
});
98-
} else {
99-
return [];
100+
}
100101
}
101-
});
102-
});
102+
}
103+
}
103104

104-
let res2;
105+
// SPARK CodeLenses (if gnatprove is available)
106+
let sparkLenses: CodeLens[] = [];
105107
if (envHasExec('gnatprove')) {
106-
/**
107-
* This is tentative deactivated code in preparation of SPARK support.
108-
*/
109-
res2 = symbols.then<CodeLens[]>((symbols) => {
110-
if (!symbols) {
111-
return [];
112-
}
113-
108+
if (symbols) {
114109
const symbolKinds = [SymbolKind.Function];
115110
const recurseInto = [SymbolKind.Module, SymbolKind.Package, SymbolKind.Function];
116-
117-
// Create a named reduce function to implement a recursive visit of symbols
118111
const functions = getSymbols(symbols, symbolKinds, recurseInto, token);
119-
120-
return functions.map((f) => {
112+
sparkLenses = functions.map((f) => {
121113
if (token?.isCancellationRequested) {
122114
throw new CancellationError();
123115
}
124-
125116
return new CodeLens(f.selectionRange, {
126117
title: '$(check) Prove',
127118
command: CMD_SPARK_PROVE_SUBP,
128119
arguments: [document.uri, f.selectionRange],
129120
});
130121
});
131-
});
132-
} else {
133-
res2 = Promise.resolve([]);
122+
}
134123
}
135124

136-
return Promise.all([res1, res2]).then((results) => {
137-
return results[0].concat(results[1]);
138-
});
125+
return codeLenses.concat(sparkLenses);
139126
}
140127
// eslint-disable-next-line @typescript-eslint/no-unused-vars
141128
resolveCodeLens?(codeLens: CodeLens, _token: CancellationToken): ProviderResult<CodeLens> {
@@ -145,4 +132,8 @@ export class AdaCodeLensProvider implements CodeLensProvider {
145132
throw new Error(`Cannot resolve CodeLens`);
146133
}
147134
}
135+
136+
refresh() {
137+
this.emitter.fire();
138+
}
148139
}

0 commit comments

Comments
 (0)