Skip to content

Commit ec2d71e

Browse files
committed
Merge branch 'topic/vscode-config' into 'master'
Update VS Code settings and project loading policies Closes #1621 and #1631 See merge request eng/ide/ada_language_server!1973
2 parents 20c7867 + e0ea9cf commit ec2d71e

File tree

11 files changed

+400
-91
lines changed

11 files changed

+400
-91
lines changed

integration/vscode/ada/package.json

Lines changed: 229 additions & 48 deletions
Large diffs are not rendered by default.

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@
2525
"projectDiagnostics": {
2626
"type": "boolean",
2727
"default": true,
28-
"description": "Controls whether or not the Ada Language Server should emit project diagnostics into the VS Code Problems view."
28+
"description": "Controls whether or not the Ada Language Server should emit diagnostics about project loading."
2929
},
3030
"alireDiagnostics": {
3131
"type": "boolean",
3232
"default": true,
33-
"description": "Controls whether or not the Ada Language Server should emit diagnostics related to alire into the VS Code Problems view."
33+
"description": "Controls whether or not the Ada Language Server should emit diagnostics related to Alire."
3434
},
3535
"defaultCharset": {
3636
"type": "string",
@@ -88,12 +88,12 @@
8888
"adaFileDiagnostics": {
8989
"type": "boolean",
9090
"default": true,
91-
"description": "Controls whether or not the Ada Language Server should emit diagnostics related to the edition of Ada Files into the VS Code Problems view."
91+
"description": "Controls whether or not the Ada Language Server should emit diagnostics related to the edition of Ada files."
9292
},
9393
"gprFileDiagnostics": {
9494
"type": "boolean",
9595
"default": true,
96-
"description": "Controls whether or not the Ada Language Server should emit diagnostics related to the edition of GPR Files into the VS Code Problems view."
96+
"description": "Controls whether or not the Ada Language Server should emit diagnostics related to the edition of GPR files."
9797
},
9898
"foldComments": {
9999
"type": "boolean",

integration/vscode/ada/src/clients.ts

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { existsSync } from 'fs';
22
import * as vscode from 'vscode';
33
import {
4+
DidChangeConfigurationNotification,
45
LanguageClient,
56
LanguageClientOptions,
67
MessageSignature,
78
ServerOptions,
9+
State,
810
} from 'vscode-languageclient/node';
911
import { logger } from './extension';
1012
import { logErrorAndThrow, setTerminalEnvironment } from './helpers';
@@ -124,16 +126,69 @@ export function createClient(
124126
// Register the server for ada sources documents
125127
documentSelector: [{ scheme: 'file', language: id }],
126128
synchronize: {
127-
// Synchronize the setting section 'ada' to the server
128-
configurationSection: 'ada',
129129
// Notify the server about file changes to Ada files contain in the workspace
130130
fileEvents: vscode.workspace.createFileSystemWatcher(pattern),
131131
},
132-
// Include the ada.* settings in the initialize request sent to the server
133-
initializationOptions: () => ({ ada: vscode.workspace.getConfiguration('ada') }),
132+
initializationOptions: () =>
133+
/**
134+
* Only include the settings that are explicitly set in one of the
135+
* VS Code scopes to avoid unintentionally overriding ALS settings
136+
* from .als.json or other applicable configuration files.
137+
*/
138+
({ ada: getExplicitlySetConfiguration() }),
134139
};
140+
135141
// Create the language client
136142
const client = new AdaLanguageClient(id, name, serverOptions, clientOptions);
143+
144+
context.subscriptions.push(
145+
vscode.workspace.onDidChangeConfiguration(async (e) => {
146+
if (e.affectsConfiguration('ada') && client.state === State.Running) {
147+
await client.sendNotification(DidChangeConfigurationNotification.type, {
148+
settings: {
149+
ada: getExplicitlySetConfiguration(),
150+
},
151+
});
152+
}
153+
}),
154+
);
155+
137156
client.serverEnv = serverEnv;
138157
return client;
139158
}
159+
160+
/**
161+
* ALS settings can come from configuration files (e.g. .als.json) and from VS
162+
* Code.
163+
*
164+
* Settings from config files are loaded first. To avoid overwriting them
165+
* unintentionally, settings from VS Code should only be sent if they were
166+
* explicitly set in one of the VS Code configuration scopes.
167+
*
168+
* This function returns that set of settings.
169+
*
170+
* @returns a dictionary of ada.* settings that have been explicitly set in one
171+
* of the VS Code configuration scopes.
172+
*/
173+
function getExplicitlySetConfiguration(): { [k: string]: unknown } {
174+
// Get all ada.* settings
175+
const adaConfig = vscode.workspace.getConfiguration('ada');
176+
const explicitSettings = Object.fromEntries(
177+
Object.entries(adaConfig).filter(([key]) => {
178+
// Filter to settings that are explicitly set
179+
const info = adaConfig.inspect(key);
180+
return (
181+
info &&
182+
[
183+
info.globalValue,
184+
info.workspaceValue,
185+
info.workspaceFolderValue,
186+
info.globalLanguageValue,
187+
info.workspaceLanguageValue,
188+
info.workspaceFolderLanguageValue,
189+
].some((v) => v !== undefined)
190+
);
191+
}),
192+
);
193+
return explicitSettings;
194+
}

source/ada/lsp-ada_handlers-project_loading.adb

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ with URIs;
5050

5151
with VSS.Characters;
5252
with VSS.Characters.Latin;
53+
with VSS.String_Vectors;
5354
with VSS.Strings;
5455
with VSS.Strings.Conversions;
5556

@@ -118,8 +119,9 @@ package body LSP.Ada_Handlers.Project_Loading is
118119

119120
procedure Ensure_Project_Loaded (Self : in out Message_Handler'Class) is
120121
use type VSS.Strings.Virtual_String;
122+
use VSS.Strings.Conversions;
121123

122-
GPRs_Found : Natural := 0;
124+
Candidates : VSS.String_Vectors.Virtual_String_Vector;
123125
Project_File : VSS.Strings.Virtual_String :=
124126
Self.Configuration.Project_File;
125127
GPR_Configuration_File : VSS.Strings.Virtual_String :=
@@ -188,29 +190,30 @@ package body LSP.Ada_Handlers.Project_Loading is
188190
declare
189191
Files : GNATCOLL.VFS.File_Array_Access :=
190192
Self.Client.Root_Directory.Read_Dir (GNATCOLL.VFS.Files_Only);
191-
Found : GNATCOLL.VFS.Virtual_File;
192193
begin
193194
for X of Files.all loop
194195
if X.Has_Suffix (".gpr") then
195-
GPRs_Found := GPRs_Found + 1;
196-
exit when GPRs_Found > 1;
197-
Found := X;
196+
Candidates.Append (LSP.Utils.To_Virtual_String (X));
198197
end if;
199198
end loop;
200199

201200
GNATCOLL.VFS.Unchecked_Free (Files);
202201

203-
if GPRs_Found = 1 then
204-
Project_File := LSP.Utils.To_Virtual_String (Found);
202+
if Candidates.Length = 1 then
203+
Project_File := Candidates.First_Element;
205204

206205
-- Report how we found the project
207206
Self.Project_Status.Set_Project_Type
208207
(LSP.Ada_Project_Loading.Single_Project_Found);
209208

210209
Tracer.Trace_Text ("Found unique project: " & Project_File);
211210
else
212-
Tracer.Trace
213-
("Found " & GPRs_Found'Image & " projects at the root");
211+
Tracer.Trace_Text
212+
("Found "
213+
& To_Virtual_String (Candidates.Length'Image)
214+
& " projects at the root:"
215+
& VSS.Characters.Latin.Line_Feed
216+
& Candidates.Join (VSS.Characters.Latin.Line_Feed));
214217
end if;
215218
end;
216219
end if;
@@ -276,12 +279,12 @@ package body LSP.Ada_Handlers.Project_Loading is
276279
-- We didn't find a project file. Let's load an implicit project. We
277280
-- reach this point either because there are no GPR projects at the
278281
-- root, or there are more than one.
279-
pragma Assert (GPRs_Found = 0 or GPRs_Found > 1);
282+
pragma Assert (Candidates.Length = 0 or Candidates.Length > 1);
280283

281284
Load_Implicit_Project
282285
(Self,
283-
(if GPRs_Found = 0 then LSP.Ada_Project_Loading.No_Project
284-
elsif GPRs_Found > 1
286+
(if Candidates.Length = 0 then LSP.Ada_Project_Loading.No_Project
287+
elsif Candidates.Length > 1
285288
then LSP.Ada_Project_Loading.Multiple_Projects
286289
else LSP.Ada_Project_Loading.Project_Not_Found));
287290
end if;

source/ada/lsp-ada_handlers.adb

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ with LSP.Utils;
9494

9595
package body LSP.Ada_Handlers is
9696

97+
Tracer_Config : constant LSP.GNATCOLL_Tracers.Tracer :=
98+
LSP.GNATCOLL_Tracers.Create ("ALS.CONFIG", GNATCOLL.Traces.On);
99+
97100
pragma Style_Checks ("o"); -- check subprogram bodies in alphabetical ordr
98101

99102
subtype AlsReferenceKind_Array is LSP.Structures.AlsReferenceKind_Set;
@@ -2486,6 +2489,8 @@ package body LSP.Ada_Handlers is
24862489
if not Value.initializationOptions.Is_Empty then
24872490
Self.Tracer.Trace
24882491
("Processing initializationOptions from initialize request");
2492+
Tracer_Config.Trace
2493+
("initializationOptions = " & Value.initializationOptions'Image);
24892494
-- The expected structure is this:
24902495
-- "initializationOptions": {
24912496
-- "ada": {
@@ -2495,7 +2500,12 @@ package body LSP.Ada_Handlers is
24952500
-- }
24962501
-- }
24972502
declare
2498-
New_Configuration : LSP.Ada_Configurations.Configuration;
2503+
New_Configuration : LSP.Ada_Configurations.Configuration :=
2504+
Self.Configuration;
2505+
-- Start from the existing configuration so that settings parsed
2506+
-- from configuration files are preserved, and the settings from
2507+
-- initialize request are applied on top.
2508+
24992509
Messages : VSS.String_Vectors.Virtual_String_Vector;
25002510
begin
25012511
-- Parse the configuration.
@@ -2537,30 +2547,27 @@ package body LSP.Ada_Handlers is
25372547
(Self : in out Message_Handler;
25382548
Value : LSP.Structures.InitializedParams) is
25392549
begin
2540-
-- The client is notifying us that it has initialized. If a project was
2541-
-- provided with the initialize request, we can load it and start
2542-
-- communicating with the client.
2543-
--
2544-
-- We only perform loading in case ada.projectFile was provided with the
2545-
-- initialize request. That's because some clients don't provide
2546-
-- settings in the initialize request, and instead send a
2547-
-- onDidChangeConfiguration notification immediately after
2548-
-- initialization. In this scenario, loading a project unconditionally
2549-
-- here would result in automatically searching for a project (unique
2550-
-- project at the root, or implicit fallback) and loading it, only to
2551-
-- load another project shortly after, upon receiving
2552-
-- onDidChangeConfiguration. To avoid the first useless load, we only
2553-
-- load a project here if it was specified in the initialize request.
2550+
-- The client is notifying us that it has initialized. It is good at
2551+
-- this stage to load a project so that subsequent requests, e.g.
2552+
-- project attribute requests to support VS Code integration, work on a
2553+
-- valid project.
25542554
--
2555-
-- Moreover, there is an impact on legacy tests. The prior project
2555+
-- However, there is an impact on legacy tests. The prior project
25562556
-- loading policy was to wait for the first onDidChangeConfiguration
25572557
-- notification to obtain settings and load a project. Many existing
25582558
-- tests do not set the project in the initialize request and don't
25592559
-- expect messages pertaining to project loading after the initialize
2560-
-- request. If we were to always load the project here, tests would
2561-
-- receive different messages and potentially fail. So the conditional
2562-
-- project loading here is backwards compatible with existing tests.
2563-
if not Self.Configuration.Project_File.Is_Empty then
2560+
-- request. If we were to always load the project here, tests would
2561+
-- receive different messages and potentially fail.
2562+
--
2563+
-- To mitigate that, we perform project loading conditionally to
2564+
-- Base_Configuration_Received. If Base_Configuration_Received is True,
2565+
-- it means that we received non-empty initializationOptions which is
2566+
-- the typical case in IDEs like GNAT Studio and VS Code.
2567+
--
2568+
-- This conditional load allows us to load a project early while
2569+
-- remaining backwards compatible with legacy tests.
2570+
if Self.Base_Configuration_Received then
25642571
LSP.Ada_Handlers.Project_Loading.Ensure_Project_Loaded (Self);
25652572
end if;
25662573
end On_Initialized_Notification;
@@ -3935,6 +3942,8 @@ package body LSP.Ada_Handlers is
39353942
(Self : in out Message_Handler;
39363943
Value : LSP.Ada_Configurations.Configuration'Class) is
39373944
begin
3945+
Tracer_Config.Trace ("Setting configuration: " & Value'Image);
3946+
39383947
Self.Configuration := LSP.Ada_Configurations.Configuration (Value);
39393948

39403949
-- The base configuration is still the default one, meaning that

testsuite/ada_lsp/config_local/test.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,16 @@ async def func(lsp: ALSLanguageClient) -> None:
1919
response = await lsp.getCurrentProject()
2020
assert response
2121
assertEqual(response, URI("non-root/p2.gpr"))
22+
23+
24+
@test(als_settings={})
25+
async def test_init_override(lsp: ALSLanguageClient) -> None:
26+
# In this test we want to check that settings obtained in the initializationOptions
27+
# of the 'initialize' request are interpreted on top of the file-based
28+
# configuration.
29+
#
30+
# To check that we send an empty dictionary of settings in the initialize request
31+
# (the 'als_settings' argument above), and we check that the applicable project is
32+
# still the one specified in .als.json, i.e. the initialize request did not
33+
# overwrite the file settings.
34+
assertEqual(await lsp.getCurrentProject(), URI("non-root/p2.gpr"))

testsuite/ada_lsp/config_priority/test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""
2-
The goal of this test is to check that when --config CONFIG_FILE is given, that
3-
configuration is loaded.
2+
The goal of the tests below is to check the priority with which the ALS considers
3+
configuration files and the initialize request.
44
55
To test that we create a unique p1.gpr at the root. If there was no configuration,
66
p1.gpr would be loaded automatically. It's a failure sentinel.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
project Prj is
2+
package Gnattest is
3+
for Harness_Dir use "myharness";
4+
end Gnattest;
5+
end Prj;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""
2+
The goal of this test is to check that the ALS loads the project early in the session,
3+
such that immediately after the initialization sequence, it is possible to inspect
4+
project attributes without opening any Ada sources or sending a configuration change to
5+
force a project load.
6+
"""
7+
8+
from drivers.pylsp import ALSLanguageClient, test
9+
10+
11+
@test(
12+
# Sending initializationOptions should cause the ALS to load a project early,
13+
# when handling the `initialized` notification. So we give an empty settings
14+
# dictionary to force the sending of initializationOptions.
15+
als_settings={}
16+
)
17+
async def t1(lsp: ALSLanguageClient):
18+
# Query a project attribute
19+
result = await lsp.getProjectAttributeValue("harness_dir", "gnattest")
20+
21+
lsp.assertEqual(result, "myharness")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
driver: pylsp

0 commit comments

Comments
 (0)