Skip to content

Commit 4c3d84b

Browse files
abap34aviatesk
andauthored
config: use struct instead of Dict (#282)
Co-Authored-By: Shuhei Kadowaki <40514306+aviatesk@users.noreply.github.com>
1 parent 741f327 commit 4c3d84b

File tree

10 files changed

+388
-450
lines changed

10 files changed

+388
-450
lines changed

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ authors = ["Shuhei Kadowaki <aviatesk@gmail.com>"]
77
projects = ["test"]
88

99
[deps]
10+
Configurations = "5218b696-f38b-4ac9-8b61-a12ec717816d"
1011
JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
1112
JSONRPC = "a2756949-8476-49a1-a294-231eace0f283"
1213
JuliaLowering = "f3c80556-a63f-4383-b822-37d64f81a311"
@@ -27,6 +28,7 @@ JuliaSyntax = {rev = "jetls-hacking-2", url = "https://github.com/JuliaLang/Juli
2728
LSP = {path = "LSP"}
2829

2930
[compat]
31+
Configurations = "0.17.6"
3032
JET = "0.10.6"
3133
JSONRPC = "0.1"
3234
JuliaLowering = "1"

src/analysis/full-analysis.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ function request_analysis!(
109109
end
110110
should_queue || @goto wait_or_return # Request saved as pending
111111

112-
debounce = get_config(server.state.config_manager, "full_analysis", "debounce")
112+
debounce = get_config(server.state.config_manager, :full_analysis, :debounce)
113113
if onsave && debounce isa Float64 && debounce > 0
114114
local delay::Float64 = debounce
115115
store!(manager.debounced) do debounced

src/config.jl

Lines changed: 120 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,3 @@
1-
# TODO (later): move this definition to external files
2-
const DEFAULT_CONFIG = ConfigDict(
3-
"full_analysis" => ConfigDict(
4-
"debounce" => 1.0
5-
),
6-
"testrunner" => ConfigDict(
7-
"executable" => @static Sys.iswindows() ? "testrunner.bat" : "testrunner"
8-
),
9-
"formatter" => ConfigDict(
10-
"runic" => ConfigDict(
11-
"executable" => @static Sys.iswindows() ? "runic.bat" : "runic"
12-
)
13-
),
14-
"internal" => ConfigDict(
15-
"static_setting" => 0
16-
),
17-
)
18-
19-
const STATIC_CONFIG = ConfigDict(
20-
"full_analysis" => ConfigDict(
21-
"debounce" => false
22-
),
23-
"testrunner" => ConfigDict(
24-
"executable" => false
25-
),
26-
"formatter" => ConfigDict(
27-
"runic" => ConfigDict(
28-
"executable" => false
29-
)
30-
),
31-
"internal" => ConfigDict(
32-
"static_setting" => true
33-
),
34-
)
35-
36-
function access_nested_dict(dict::ConfigDict, path::String, rest_path::String...)
37-
nextobj = @something get(dict, path, nothing) return nothing
38-
if !(nextobj isa ConfigDict)
39-
if isempty(rest_path)
40-
return nextobj
41-
else
42-
return nothing
43-
end
44-
end
45-
return access_nested_dict(nextobj, rest_path...)
46-
end
47-
48-
"""
49-
traverse_merge(on_leaf, base::ConfigDict, overlay::ConfigDict) -> merged::ConfigDict
50-
51-
Return a new `ConfigDict` whose key value pairs are merged from `base` and `overlay`.
52-
53-
If a key in `overlay` is a dictionary, it will recursively merge it into the corresponding
54-
key in `base`, creating new `ConfigDict` instances along the way.
55-
56-
When a value in `overlay` is not a dictionary, the `on_leaf` function is called with:
57-
- `current_path`: the current path as a vector of strings
58-
- `v`: the value from `overlay`
59-
The `on_leaf(current_path, v) -> newv` function should return the value to be stored
60-
in the result, or `nothing` to skip storing the key.
61-
"""
62-
function traverse_merge(
63-
on_leaf, base::ConfigDict, overlay::ConfigDict,
64-
key_path::Vector{String} = String[]
65-
)
66-
result = base
67-
for (k, v) in overlay
68-
current_path = [key_path; k]
69-
if v isa ConfigDict
70-
base_v = get(base, k, nothing)
71-
if base_v isa ConfigDict
72-
merged_v = traverse_merge(on_leaf, base_v, v, current_path)
73-
result = ConfigDict(result, k => merged_v)
74-
else
75-
merged_v = traverse_merge(on_leaf, ConfigDict(), v, current_path)
76-
result = ConfigDict(result, k => merged_v)
77-
end
78-
else
79-
on_leaf_result = on_leaf(current_path, v)
80-
if on_leaf_result !== nothing
81-
result = ConfigDict(result, k => on_leaf_result)
82-
end
83-
end
84-
end
85-
return result
86-
end
87-
88-
is_static_setting(key_path::String...) = access_nested_dict(STATIC_CONFIG, key_path...) === true
89-
901
is_config_file(filepath::AbstractString) = filepath == "__DEFAULT_CONFIG__" || basename(filepath) == ".JETLSConfig.toml"
912

923
"""
@@ -97,7 +8,7 @@ The order is determined by the following rule:
978
989
1. "__DEFAULT_CONFIG__" has lower priority than any other path.
9910
100-
This rules defines a total order. (See `is_config_file`)
11+
This rule defines a total order. (See `is_config_file`)
10112
"""
10213
function Base.lt(::ConfigFileOrder, path1, path2)
10314
path1 == "__DEFAULT_CONFIG__" && return false
@@ -106,49 +17,96 @@ function Base.lt(::ConfigFileOrder, path1, path2)
10617
return false # unreachable
10718
end
10819

109-
function cleanup_empty_dicts(dict::ConfigDict)
110-
result = dict
111-
for (k, v) in dict
112-
if v isa ConfigDict
113-
cleaned_v = cleanup_empty_dicts(v)
114-
if isempty(cleaned_v)
115-
result = Base.delete(result, k)
116-
elseif cleaned_v != v
117-
result = ConfigDict(result, k => cleaned_v)
118-
end
119-
end
20+
@generated function on_difference(
21+
callback,
22+
old_config::T,
23+
new_config::T,
24+
path::NTuple{N,Symbol}=()
25+
) where {T<:ConfigSection,N}
26+
entries = (
27+
:(on_difference(
28+
callback,
29+
getfield(old_config, $(QuoteNode(fname))),
30+
getfield(new_config, $(QuoteNode(fname))),
31+
(path..., $(QuoteNode(fname)))
32+
))
33+
for fname in fieldnames(T)
34+
)
35+
36+
quote
37+
$T($(entries...))
12038
end
121-
return result
12239
end
12340

124-
function merge_settings(base::ConfigDict, overlay::ConfigDict)
125-
return traverse_merge(base, overlay) do _, v
126-
v
127-
end |> cleanup_empty_dicts
128-
end
41+
@generated function on_difference(
42+
callback,
43+
old_val::T,
44+
new_val::Nothing,
45+
path::Tuple
46+
) where T <: ConfigSection
47+
entries = (
48+
:(on_difference(
49+
callback,
50+
getfield(old_val, $(QuoteNode(fname))),
51+
nothing,
52+
(path..., $(QuoteNode(fname)))
53+
))
54+
for fname in fieldnames(T)
55+
)
12956

130-
function get_settings(data::ConfigManagerData)
131-
result = ConfigDict()
132-
for config in Iterators.reverse(values(data.watched_files))
133-
result = merge_settings(result, config)
57+
quote
58+
$T($(entries...))
13459
end
135-
return result
13660
end
13761

138-
function merge_static_settings(base::ConfigDict, overlay::ConfigDict)
139-
return traverse_merge(base, overlay) do path, v
140-
is_static_setting(path...) ? v : nothing
141-
end |> cleanup_empty_dicts
62+
@generated function on_difference(
63+
callback,
64+
old_val::Nothing,
65+
new_val::T,
66+
path::Tuple
67+
) where T <: ConfigSection
68+
entries = (
69+
:(on_difference(
70+
callback,
71+
nothing,
72+
getfield(new_val, $(QuoteNode(fname))),
73+
(path..., $(QuoteNode(fname)))
74+
))
75+
for fname in fieldnames(T)
76+
)
77+
78+
quote
79+
$T($(entries...))
80+
end
14281
end
14382

144-
function get_static_settings(data::ConfigManagerData)
145-
result = ConfigDict()
146-
for config in Iterators.reverse(values(data.watched_files))
147-
result = merge_static_settings(result, config)
83+
on_difference(callback, old_val, new_val, path::Tuple) =
84+
old_val !== new_val ? callback(old_val, new_val, path) : old_val
85+
86+
"""
87+
merge_setting(base::T, overlay::T) where {T<:ConfigSection} -> T
88+
89+
Merges two configuration objects, with `overlay` taking precedence over `base`.
90+
If a field in `overlay` is `nothing`, the corresponding field from `base` is retained.
91+
"""
92+
merge_setting(base::T, overlay::T) where {T<:ConfigSection} =
93+
on_difference((base_val, overlay_val, path) -> overlay_val === nothing ? base_val : overlay_val, base, overlay)
94+
95+
function get_current_settings(watched_files::WatchedConfigFiles)
96+
result = DEFAULT_CONFIG
97+
for config in Iterators.reverse(values(watched_files))
98+
result = merge_setting(result, config)
14899
end
149100
return result
150101
end
151102

103+
# TODO: remove this.
104+
# (now this is used for `collect_unmatched_keys` only. see that's comment)
105+
const ConfigDict = Base.PersistentDict{String, Any}
106+
to_config_dict(dict::AbstractDict) = ConfigDict((k => (v isa AbstractDict ? to_config_dict(v) : v) for (k, v) in dict)...)
107+
108+
const DEFAULT_CONFIG_DICT = to_config_dict(Configurations.to_dict(DEFAULT_CONFIG))
109+
152110
"""
153111
collect_unmatched_keys(this::ConfigDict, ref::ConfigDict) -> Vector{Vector{String}}
154112
@@ -178,8 +136,12 @@ julia> collect_unmatched_keys(
178136
1-element Vector{Vector{String}}:
179137
["key1"]
180138
```
139+
140+
141+
TODO: remove this. This is a temporary workaround to report unknown keys in the config file
142+
until Configurations.jl supports reporting full path of unknown keys.
181143
"""
182-
function collect_unmatched_keys(this::ConfigDict, ref::ConfigDict=DEFAULT_CONFIG)
144+
function collect_unmatched_keys(this::ConfigDict, ref::ConfigDict=DEFAULT_CONFIG_DICT)
183145
unknown_keys = Vector{String}[]
184146
collect_unmatched_keys!(unknown_keys, this, ref, String[])
185147
return unknown_keys
@@ -211,19 +173,20 @@ Retrieves the current configuration value.
211173
Among the registered configuration files, fetches the value in order of priority (see `Base.lt(::ConfigFileOrder, path1, path2)`).
212174
If the key path does not exist in any of the configurations, returns `nothing`.
213175
"""
214-
function get_config(manager::ConfigManager, key_path::String...)
215-
is_static_setting(key_path...) &&
216-
return access_nested_dict(load(manager).static_settings, key_path...)
217-
for config in values(load(manager).watched_files)
218-
return @something access_nested_dict(config, key_path...) continue
176+
Base.@constprop :aggressive function get_config(manager::ConfigManager, key_path::Symbol...)
177+
try
178+
is_static_setting(key_path...) &&
179+
return getobjpath(load(manager).static_settings, key_path...)
180+
return getobjpath(load(manager).current_settings, key_path...)
181+
catch e
182+
e isa FieldError ? nothing : rethrow()
219183
end
220-
return nothing
221184
end
222185

223186
function fix_static_settings!(manager::ConfigManager)
224187
store!(manager) do old_data
225-
new_static = get_static_settings(old_data)
226-
new_data = ConfigManagerData(new_static, old_data.watched_files)
188+
new_static = get_current_settings(old_data.watched_files)
189+
new_data = ConfigManagerData(old_data.current_settings, new_static, old_data.watched_files)
227190
return new_data, new_static
228191
end
229192
end
@@ -235,7 +198,7 @@ If the file does not exist or cannot be parsed, just return leaving the current
235198
configuration unchanged. When there are unknown keys in the config file,
236199
send error message while leaving current configuration unchanged.
237200
"""
238-
function load_config!(on_leaf, server::Server, filepath::AbstractString;
201+
function load_config!(callback, server::Server, filepath::AbstractString;
239202
reload::Bool = false)
240203
store!(server.state.config_manager) do old_data
241204
if reload
@@ -247,43 +210,46 @@ function load_config!(on_leaf, server::Server, filepath::AbstractString;
247210
parsed = TOML.tryparsefile(filepath)
248211
parsed isa TOML.ParserError && return old_data, nothing
249212

250-
new_config = to_config_dict(parsed)
251-
252-
unknown_keys = collect_unmatched_keys(new_config)
253-
if !isempty(unknown_keys)
213+
new_config = try
214+
Configurations.from_dict(JETLSConfig, parsed)
215+
catch e
216+
# TODO: remove this when Configurations.jl support to report
217+
# full path of unknown key.
218+
if e isa Configurations.InvalidKeyError
219+
config_dict = to_config_dict(parsed)
220+
unknown_keys = collect_unmatched_keys(config_dict)
221+
if !isempty(unknown_keys)
222+
show_error_message(server, """
223+
Configuration file at $filepath contains unknown keys:
224+
$(join(map(x -> string('`', join(x, "."), '`'), unknown_keys), ", "))
225+
""")
226+
return old_data, nothing
227+
end
228+
end
254229
show_error_message(server, """
255-
Configuration file at $filepath contains unknown keys:
256-
$(join(map(x -> string('`', join(x, "."), '`'), unknown_keys), ", "))
230+
Failed to load configuration file at $filepath:
231+
$(e)
257232
""")
258233
return old_data, nothing
259234
end
260235

261-
current_config = get(old_data.watched_files, filepath, DEFAULT_CONFIG)
262-
merged_config = traverse_merge(current_config, new_config) do filepath, v
263-
on_leaf(current_config, filepath, v)
264-
v
265-
end
266236
new_watched_files = copy(old_data.watched_files)
267-
new_watched_files[filepath] = merged_config
268-
new_data = ConfigManagerData(old_data.static_settings, new_watched_files)
237+
new_watched_files[filepath] = new_config
238+
new_current_settings = get_current_settings(new_watched_files)
239+
on_difference(callback, old_data.current_settings, new_current_settings)
240+
new_data = ConfigManagerData(new_current_settings, old_data.static_settings, new_watched_files)
269241
return new_data, nothing
270242
end
271243
end
272244

273-
to_config_dict(dict::Dict{String,Any}) = ConfigDict(
274-
(k => (v isa Dict{String,Any} ? to_config_dict(v) : v) for (k, v) in dict)...)
275-
276-
function delete_config!(on_leaf, manager::ConfigManager, filepath::AbstractString)
245+
function delete_config!(callback, manager::ConfigManager, filepath::AbstractString)
277246
store!(manager) do old_data
278-
old_settings = get_settings(old_data)
247+
haskey(old_data.watched_files, filepath) || return old_data, nothing
279248
new_watched_files = copy(old_data.watched_files)
280249
delete!(new_watched_files, filepath)
281-
new_data = ConfigManagerData(old_data.static_settings, new_watched_files)
282-
new_settings = get_settings(new_data)
283-
traverse_merge(old_settings, new_settings) do path, v
284-
on_leaf(old_settings, path, v)
285-
nothing
286-
end
250+
new_current_settings = get_current_settings(new_watched_files)
251+
new_data = ConfigManagerData(new_current_settings, old_data.static_settings, new_watched_files)
252+
on_difference(callback, old_data.current_settings, new_current_settings)
287253
return new_data, nothing
288254
end
289255
end

0 commit comments

Comments
 (0)