Skip to content

Commit b19d2bd

Browse files
authored
Merge pull request #289 from aviatesk/avi/prioritize-workspace-config
config: hierarchical configuration experiment (not enabled for now)
2 parents ccbb696 + 529137b commit b19d2bd

File tree

6 files changed

+68
-54
lines changed

6 files changed

+68
-54
lines changed

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
- Additionally, prioritize simple text style and limit unnecessary decorations
77
(e.g. `**`) to only truly necessary locations. This is a style that should
88
generally be aimed for, but pay particular attention when writing Markdown.
9+
- Headers should use sentence case (only the first word capitalized), not
10+
title case. For example:
11+
- Good: `## Conclusion and alternative approaches`
12+
- Bad: `## Conclusion And Alternative Approaches`
913
- When writing commit messages, follow the format `component: Brief summary` for
1014
the title. In the body of the commit message, provide a brief prose summary of
1115
the purpose of the changes made.

docs/src/configuration.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ See [TestRunner integration](@ref) for setup instructions.
6767

6868
## How to configure JETLS
6969

70-
### Method 1: Project-specific configuration file
70+
### Method 1: File-based configuration
7171

7272
Create a `.JETLSConfig.toml` file in your project root.
7373
This configuration method works client-agnostically, thus allows projects to
@@ -154,10 +154,13 @@ section:
154154
When multiple configuration sources are present, they are merged in priority
155155
order (highest first):
156156

157-
1. Project-specific `.JETLSConfig.toml`
158-
2. Editor configuration via LSP
157+
1. File-based configuration (`.JETLSConfig.toml`)
158+
2. Editor configuration via LSP (`workspace/configuration`)
159159
3. Built-in defaults
160160

161-
The `.JETLSConfig.toml` file takes precedence, since it provides a
162-
**client-agnostic** way to configure JETLS that works consistently across
163-
all editors.
161+
File-based configuration (`.JETLSConfig.toml`) takes precedence as it provides
162+
a **client-agnostic** way to configure JETLS that works consistently across all
163+
editors. While the LSP specification defines `scopeUri` for hierarchical
164+
configuration, current LSP clients (including Zed and VS Code as of 2025-10-27)
165+
do not properly support scoped configuration requests, making `.JETLSConfig.toml`
166+
the more useful configuration method for project-specific settings.

src/did-change-watched-files.jl

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ function handle_config_file_change!(
2828
tracker = ConfigChangeTracker()
2929

3030
if change_type == FileChangeType.Created
31-
load_config!(tracker, server, changed_path)
31+
load_file_config!(tracker, server, changed_path)
3232
kind = "created"
3333
show_info_message(server, config_file_created_msg(changed_path))
3434
elseif change_type == FileChangeType.Changed
35-
load_config!(tracker, server, changed_path; reload=true)
35+
load_file_config!(tracker, server, changed_path; reload=true)
3636
kind = "updated"
3737
elseif change_type == FileChangeType.Deleted
38-
delete_config!(tracker, server.state.config_manager, changed_path)
38+
delete_file_config!(tracker, server.state.config_manager, changed_path)
3939
kind = "deleted"
4040
show_info_message(server, config_file_deleted_msg(changed_path))
4141
else error("Unknown FileChangeType") end
@@ -45,24 +45,24 @@ function handle_config_file_change!(
4545
end
4646

4747
"""
48-
Loads the project configuration from the specified path into the server's config manager.
48+
Loads the file-based configuration from the specified path into the server's config manager.
4949
5050
If the file does not exist or cannot be parsed, just return leaving the current
5151
configuration unchanged. When there are unknown keys in the config file,
5252
send error message while leaving current configuration unchanged.
5353
"""
54-
function load_config!(callback, server::Server, filepath::AbstractString;
55-
reload::Bool = false)
54+
function load_file_config!(callback, server::Server, filepath::AbstractString;
55+
reload::Bool = false)
5656
store!(server.state.config_manager) do old_data::ConfigManagerData
57-
if reload && old_data.project_config_path != filepath
57+
if reload && old_data.file_config_path != filepath
5858
show_warning_message(server, "Loading unregistered configuration file: $filepath")
5959
end
6060

6161
isfile(filepath) || return old_data, nothing
6262
parsed = TOML.tryparsefile(filepath)
6363
parsed isa TOML.ParserError && return old_data, nothing
6464

65-
new_project_config = try
65+
new_file_config = try
6666
Configurations.from_dict(JETLSConfig, parsed)
6767
catch e
6868
# TODO: remove this when Configurations.jl support to report
@@ -83,8 +83,8 @@ function load_config!(callback, server::Server, filepath::AbstractString;
8383
end
8484

8585
new_data = ConfigManagerData(old_data;
86-
project_config=new_project_config,
87-
project_config_path=filepath
86+
file_config=new_file_config,
87+
file_config_path=filepath
8888
)
8989
on_difference(callback, get_settings(old_data), get_settings(new_data))
9090
return new_data, nothing
@@ -94,12 +94,12 @@ end
9494
unmatched_keys_in_config_file_msg(filepath::AbstractString, unmatched_keys) =
9595
unmatched_keys_msg("Configuration file at $filepath contains unknown keys:", unmatched_keys)
9696

97-
function delete_config!(callback, manager::ConfigManager, filepath::AbstractString)
97+
function delete_file_config!(callback, manager::ConfigManager, filepath::AbstractString)
9898
store!(manager) do old_data::ConfigManagerData
99-
old_data.project_config_path == filepath || return old_data, nothing
99+
old_data.file_config_path == filepath || return old_data, nothing
100100
new_data = ConfigManagerData(old_data;
101-
project_config=EMPTY_CONFIG,
102-
project_config_path=nothing
101+
file_config=EMPTY_CONFIG,
102+
file_config_path=nothing
103103
)
104104
on_difference(callback, get_settings(old_data), get_settings(new_data))
105105
return new_data, nothing

src/lifecycle.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ function handle_InitializeRequest(server::Server, msg::InitializeRequest)
5454
else
5555
config_path = joinpath(state.root_path, ".JETLSConfig.toml")
5656
if isfile(config_path)
57-
load_config!(Returns(nothing), server, config_path)
57+
load_file_config!(Returns(nothing), server, config_path)
5858
end
5959
end
6060

src/types.jl

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -392,24 +392,28 @@ const EMPTY_CONFIG = JETLSConfig()
392392

393393
struct ConfigManagerData
394394
static_settings::JETLSConfig
395-
project_config::JETLSConfig
395+
file_config::JETLSConfig
396396
lsp_config::JETLSConfig
397-
project_config_path::Union{Nothing,String}
397+
file_config_path::Union{Nothing,String}
398398
__settings__::JETLSConfig
399399
function ConfigManagerData(
400400
static_settings::JETLSConfig,
401-
project_config::JETLSConfig,
401+
file_config::JETLSConfig,
402402
lsp_config::JETLSConfig,
403-
project_config_path::Union{Nothing,String}
403+
file_config_path::Union{Nothing,String}
404404
)
405405
# Configuration priority:
406406
# 1. DEFAULT_CONFIG (base layer)
407-
# 2. LSP config (middle layer)
408-
# 3. Project config (highest priority)
407+
# 2. LSP config via `workspace/configuration` (middle layer)
408+
# 3. File config from `.JETLSConfig.toml` (highest priority)
409+
# - Allows client-agnostic configuration
410+
# - Limited to project root scope only
411+
# - Takes precedence since clients don't properly support
412+
# hierarchical configuration via scopeUri
409413
settings = DEFAULT_CONFIG
410414
settings = merge_setting(settings, lsp_config)
411-
settings = merge_setting(settings, project_config)
412-
return new(static_settings, project_config, lsp_config, project_config_path, settings)
415+
settings = merge_setting(settings, file_config)
416+
return new(static_settings, file_config, lsp_config, file_config_path, settings)
413417
end
414418
end
415419

@@ -418,11 +422,11 @@ ConfigManagerData() = ConfigManagerData(DEFAULT_CONFIG, EMPTY_CONFIG, EMPTY_CONF
418422
function ConfigManagerData(
419423
data::ConfigManagerData;
420424
static_settings::JETLSConfig = data.static_settings,
421-
project_config::JETLSConfig = data.project_config,
425+
file_config::JETLSConfig = data.file_config,
422426
lsp_config::JETLSConfig = data.lsp_config,
423-
project_config_path::Union{Nothing,String} = data.project_config_path
427+
file_config_path::Union{Nothing,String} = data.file_config_path
424428
)
425-
return ConfigManagerData(static_settings, project_config, lsp_config, project_config_path)
429+
return ConfigManagerData(static_settings, file_config, lsp_config, file_config_path)
426430
end
427431

428432
get_settings(data::ConfigManagerData) = data.__settings__

test/test_config.jl

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,11 @@ end
113113
end
114114
end
115115

116-
function store_project_config!(manager::JETLS.ConfigManager, filepath::AbstractString, new_config::JETLS.JETLSConfig)
116+
function store_file_config!(manager::JETLS.ConfigManager, filepath::AbstractString, new_config::JETLS.JETLSConfig)
117117
JETLS.store!(manager) do old_data
118118
new_data = JETLS.ConfigManagerData(old_data;
119-
project_config=new_config,
120-
project_config_path=filepath
119+
file_config=new_config,
120+
file_config_path=filepath
121121
)
122122
return new_data, nothing
123123
end
@@ -139,7 +139,7 @@ end
139139
internal=JETLS.InternalConfig(5, nothing)
140140
)
141141

142-
store_project_config!(manager, "/foo/bar/.JETLSConfig.toml", test_config)
142+
store_file_config!(manager, "/foo/bar/.JETLSConfig.toml", test_config)
143143
JETLS.fix_static_settings!(manager)
144144

145145
@test JETLS.get_config(manager, :full_analysis, :debounce) === 2.0
@@ -154,33 +154,34 @@ end
154154
JETLS.get_config(manager, :internal, :static_setting)
155155
end == Union{Nothing, Int}
156156

157-
# Test priority: LSP config has lower priority than project config
157+
# Test priority: file config has higher priority than LSP config
158158
lsp_config = JETLS.JETLSConfig(;
159159
full_analysis=JETLS.FullAnalysisConfig(999.0),
160160
testrunner=JETLS.TestRunnerConfig("lsp_runner")
161161
)
162162
store_lsp_config!(manager, lsp_config)
163-
# High priority project config should still win
163+
# High priority file config should win
164164
@test JETLS.get_config(manager, :full_analysis, :debounce) === 2.0
165165
@test JETLS.get_config(manager, :testrunner, :executable) === "test_runner"
166166

167167
# Test updating config
168+
store_lsp_config!(manager, JETLS.EMPTY_CONFIG)
168169
changed_static_keys = Set{String}()
169170
updated_config = JETLS.JETLSConfig(;
170171
full_analysis=JETLS.FullAnalysisConfig(3.0),
171172
testrunner=JETLS.TestRunnerConfig("new_runner"),
172173
internal=JETLS.InternalConfig(10, nothing)
173174
)
174175
let data = JETLS.load(manager)
175-
current_config = data.project_config
176+
current_config = data.file_config
176177
JETLS.on_difference(current_config, updated_config) do _, new_val, path
177178
if JETLS.is_static_setting(JETLS.JETLSConfig, path...)
178179
push!(changed_static_keys, join(path, "."))
179180
end
180181
return new_val
181182
end
182183
end
183-
store_project_config!(manager, "/foo/bar/.JETLSConfig.toml", updated_config)
184+
store_file_config!(manager, "/foo/bar/.JETLSConfig.toml", updated_config)
184185

185186
# `on_static_setting` should be called for static keys
186187
@test changed_static_keys == Set(["internal.static_setting"])
@@ -193,19 +194,19 @@ end
193194

194195
@testset "`fix_static_settings!`" begin
195196
manager = JETLS.ConfigManager(JETLS.ConfigManagerData())
196-
high_priority_config = JETLS.JETLSConfig(;
197+
file_config = JETLS.JETLSConfig(;
197198
internal=JETLS.InternalConfig(2, nothing)
198199
)
199-
low_priority_config = JETLS.JETLSConfig(;
200+
lsp_config = JETLS.JETLSConfig(;
200201
internal=JETLS.InternalConfig(999, nothing),
201202
testrunner=JETLS.TestRunnerConfig("custom")
202203
)
203204

204-
store_lsp_config!(manager, low_priority_config)
205-
store_project_config!(manager, "/path/.JETLSConfig.toml", high_priority_config)
205+
store_file_config!(manager, "/path/.JETLSConfig.toml", file_config)
206+
store_lsp_config!(manager, lsp_config)
206207
JETLS.fix_static_settings!(manager)
207208

208-
# high priority should win for the static keys
209+
# File config (higher priority) should win for the static keys
209210
data = JETLS.load(manager)
210211
@test JETLS.getobjpath(data.static_settings, :internal, :static_setting) == 2
211212
end
@@ -217,19 +218,21 @@ end
217218
full_analysis=JETLS.FullAnalysisConfig(2.0),
218219
testrunner=nothing
219220
)
220-
project_config = JETLS.JETLSConfig(;
221-
testrunner=JETLS.TestRunnerConfig("project_runner"),
221+
file_config = JETLS.JETLSConfig(;
222+
testrunner=JETLS.TestRunnerConfig("file_runner"),
222223
full_analysis=nothing
223224
)
224225

225226
store_lsp_config!(manager, lsp_config)
226-
store_project_config!(manager, "/project/.JETLSConfig.toml", project_config)
227+
store_file_config!(manager, "/project/.JETLSConfig.toml", file_config)
227228

228-
@test JETLS.get_config(manager, :testrunner, :executable) == "project_runner"
229+
# File config has higher priority, so it wins when both are set
230+
@test JETLS.get_config(manager, :testrunner, :executable) == "file_runner"
231+
# When file config doesn't set a value, LSP config is used
229232
@test JETLS.get_config(manager, :full_analysis, :debounce) == 2.0
230233
end
231234

232-
@testset "LSP configuration merging without project config" begin
235+
@testset "LSP configuration merging without file config" begin
233236
manager = JETLS.ConfigManager(JETLS.ConfigManagerData())
234237

235238
lsp_config = JETLS.JETLSConfig(;
@@ -247,22 +250,22 @@ end
247250
@testset "preset formatter: Runic" begin
248251
manager = JETLS.ConfigManager(JETLS.ConfigManagerData())
249252
config = JETLS.JETLSConfig(; formatter="Runic")
250-
store_project_config!(manager, "/path/.JETLSConfig.toml", config)
253+
store_file_config!(manager, "/path/.JETLSConfig.toml", config)
251254
@test JETLS.get_config(manager, :formatter) == "Runic"
252255
end
253256

254257
@testset "preset formatter: JuliaFormatter" begin
255258
manager = JETLS.ConfigManager(JETLS.ConfigManagerData())
256259
config = JETLS.JETLSConfig(; formatter="JuliaFormatter")
257-
store_project_config!(manager, "/path/.JETLSConfig.toml", config)
260+
store_file_config!(manager, "/path/.JETLSConfig.toml", config)
258261
@test JETLS.get_config(manager, :formatter) == "JuliaFormatter"
259262
end
260263

261264
@testset "custom formatter" begin
262265
manager = JETLS.ConfigManager(JETLS.ConfigManagerData())
263266
custom = JETLS.CustomFormatterConfig("my-formatter", "my-range-formatter")
264267
config = JETLS.JETLSConfig(; formatter=custom)
265-
store_project_config!(manager, "/path/.JETLSConfig.toml", config)
268+
store_file_config!(manager, "/path/.JETLSConfig.toml", config)
266269
formatter = JETLS.get_config(manager, :formatter)
267270
@test formatter isa JETLS.CustomFormatterConfig
268271
@test formatter.executable == "my-formatter"
@@ -273,7 +276,7 @@ end
273276
manager = JETLS.ConfigManager(JETLS.ConfigManagerData())
274277
custom = JETLS.CustomFormatterConfig("my-formatter", nothing)
275278
config = JETLS.JETLSConfig(; formatter=custom)
276-
store_project_config!(manager, "/path/.JETLSConfig.toml", config)
279+
store_file_config!(manager, "/path/.JETLSConfig.toml", config)
277280
formatter = JETLS.get_config(manager, :formatter)
278281
@test formatter isa JETLS.CustomFormatterConfig
279282
@test formatter.executable == "my-formatter"

0 commit comments

Comments
 (0)