Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ authors = ["Shuhei Kadowaki <aviatesk@gmail.com>"]
projects = ["test"]

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

[compat]
Configurations = "0.17.6"
JET = "0.10.6"
JSONRPC = "0.1"
JuliaLowering = "1"
Expand Down
2 changes: 1 addition & 1 deletion src/analysis/full-analysis.jl
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ function request_analysis!(
end
should_queue || @goto wait_or_return # Request saved as pending

debounce = get_config(server.state.config_manager, "full_analysis", "debounce")
debounce = get_config(server.state.config_manager, :full_analysis, :debounce)
if onsave && debounce isa Float64 && debounce > 0
local delay::Float64 = debounce
store!(manager.debounced) do debounced
Expand Down
274 changes: 120 additions & 154 deletions src/config.jl
Original file line number Diff line number Diff line change
@@ -1,92 +1,3 @@
# TODO (later): move this definition to external files
const DEFAULT_CONFIG = ConfigDict(
"full_analysis" => ConfigDict(
"debounce" => 1.0
),
"testrunner" => ConfigDict(
"executable" => @static Sys.iswindows() ? "testrunner.bat" : "testrunner"
),
"formatter" => ConfigDict(
"runic" => ConfigDict(
"executable" => @static Sys.iswindows() ? "runic.bat" : "runic"
)
),
"internal" => ConfigDict(
"static_setting" => 0
),
)

const STATIC_CONFIG = ConfigDict(
"full_analysis" => ConfigDict(
"debounce" => false
),
"testrunner" => ConfigDict(
"executable" => false
),
"formatter" => ConfigDict(
"runic" => ConfigDict(
"executable" => false
)
),
"internal" => ConfigDict(
"static_setting" => true
),
)

function access_nested_dict(dict::ConfigDict, path::String, rest_path::String...)
nextobj = @something get(dict, path, nothing) return nothing
if !(nextobj isa ConfigDict)
if isempty(rest_path)
return nextobj
else
return nothing
end
end
return access_nested_dict(nextobj, rest_path...)
end

"""
traverse_merge(on_leaf, base::ConfigDict, overlay::ConfigDict) -> merged::ConfigDict

Return a new `ConfigDict` whose key value pairs are merged from `base` and `overlay`.

If a key in `overlay` is a dictionary, it will recursively merge it into the corresponding
key in `base`, creating new `ConfigDict` instances along the way.

When a value in `overlay` is not a dictionary, the `on_leaf` function is called with:
- `current_path`: the current path as a vector of strings
- `v`: the value from `overlay`
The `on_leaf(current_path, v) -> newv` function should return the value to be stored
in the result, or `nothing` to skip storing the key.
"""
function traverse_merge(
on_leaf, base::ConfigDict, overlay::ConfigDict,
key_path::Vector{String} = String[]
)
result = base
for (k, v) in overlay
current_path = [key_path; k]
if v isa ConfigDict
base_v = get(base, k, nothing)
if base_v isa ConfigDict
merged_v = traverse_merge(on_leaf, base_v, v, current_path)
result = ConfigDict(result, k => merged_v)
else
merged_v = traverse_merge(on_leaf, ConfigDict(), v, current_path)
result = ConfigDict(result, k => merged_v)
end
else
on_leaf_result = on_leaf(current_path, v)
if on_leaf_result !== nothing
result = ConfigDict(result, k => on_leaf_result)
end
end
end
return result
end

is_static_setting(key_path::String...) = access_nested_dict(STATIC_CONFIG, key_path...) === true

is_config_file(filepath::AbstractString) = filepath == "__DEFAULT_CONFIG__" || basename(filepath) == ".JETLSConfig.toml"

"""
Expand All @@ -97,7 +8,7 @@ The order is determined by the following rule:

1. "__DEFAULT_CONFIG__" has lower priority than any other path.

This rules defines a total order. (See `is_config_file`)
This rule defines a total order. (See `is_config_file`)
"""
function Base.lt(::ConfigFileOrder, path1, path2)
path1 == "__DEFAULT_CONFIG__" && return false
Expand All @@ -106,49 +17,96 @@ function Base.lt(::ConfigFileOrder, path1, path2)
return false # unreachable
end

function cleanup_empty_dicts(dict::ConfigDict)
result = dict
for (k, v) in dict
if v isa ConfigDict
cleaned_v = cleanup_empty_dicts(v)
if isempty(cleaned_v)
result = Base.delete(result, k)
elseif cleaned_v != v
result = ConfigDict(result, k => cleaned_v)
end
end
@generated function on_difference(
Comment thread
aviatesk marked this conversation as resolved.
callback,
old_config::T,
new_config::T,
path::NTuple{N,Symbol}=()
) where {T<:ConfigSection,N}
entries = (
:(on_difference(
callback,
getfield(old_config, $(QuoteNode(fname))),
getfield(new_config, $(QuoteNode(fname))),
(path..., $(QuoteNode(fname)))
))
for fname in fieldnames(T)
)

quote
$T($(entries...))
end
return result
end

function merge_settings(base::ConfigDict, overlay::ConfigDict)
return traverse_merge(base, overlay) do _, v
v
end |> cleanup_empty_dicts
end
@generated function on_difference(
callback,
old_val::T,
new_val::Nothing,
path::Tuple
) where T <: ConfigSection
entries = (
:(on_difference(
callback,
getfield(old_val, $(QuoteNode(fname))),
nothing,
(path..., $(QuoteNode(fname)))
))
for fname in fieldnames(T)
)

function get_settings(data::ConfigManagerData)
result = ConfigDict()
for config in Iterators.reverse(values(data.watched_files))
result = merge_settings(result, config)
quote
$T($(entries...))
end
return result
end

function merge_static_settings(base::ConfigDict, overlay::ConfigDict)
return traverse_merge(base, overlay) do path, v
is_static_setting(path...) ? v : nothing
end |> cleanup_empty_dicts
@generated function on_difference(
callback,
old_val::Nothing,
new_val::T,
path::Tuple
) where T <: ConfigSection
entries = (
:(on_difference(
callback,
nothing,
getfield(new_val, $(QuoteNode(fname))),
(path..., $(QuoteNode(fname)))
))
for fname in fieldnames(T)
)

quote
$T($(entries...))
end
end

function get_static_settings(data::ConfigManagerData)
result = ConfigDict()
for config in Iterators.reverse(values(data.watched_files))
result = merge_static_settings(result, config)
on_difference(callback, old_val, new_val, path::Tuple) =
old_val !== new_val ? callback(old_val, new_val, path) : old_val

"""
merge_setting(base::T, overlay::T) where {T<:ConfigSection} -> T

Merges two configuration objects, with `overlay` taking precedence over `base`.
If a field in `overlay` is `nothing`, the corresponding field from `base` is retained.
"""
merge_setting(base::T, overlay::T) where {T<:ConfigSection} =
on_difference((base_val, overlay_val, path) -> overlay_val === nothing ? base_val : overlay_val, base, overlay)

function get_current_settings(watched_files::WatchedConfigFiles)
result = DEFAULT_CONFIG
for config in Iterators.reverse(values(watched_files))
result = merge_setting(result, config)
end
return result
end

# TODO: remove this.
# (now this is used for `collect_unmatched_keys` only. see that's comment)
const ConfigDict = Base.PersistentDict{String, Any}
to_config_dict(dict::AbstractDict) = ConfigDict((k => (v isa AbstractDict ? to_config_dict(v) : v) for (k, v) in dict)...)

const DEFAULT_CONFIG_DICT = to_config_dict(Configurations.to_dict(DEFAULT_CONFIG))

"""
collect_unmatched_keys(this::ConfigDict, ref::ConfigDict) -> Vector{Vector{String}}

Expand Down Expand Up @@ -178,8 +136,12 @@ julia> collect_unmatched_keys(
1-element Vector{Vector{String}}:
["key1"]
```


TODO: remove this. This is a temporary workaround to report unknown keys in the config file
until Configurations.jl supports reporting full path of unknown keys.
"""
function collect_unmatched_keys(this::ConfigDict, ref::ConfigDict=DEFAULT_CONFIG)
function collect_unmatched_keys(this::ConfigDict, ref::ConfigDict=DEFAULT_CONFIG_DICT)
unknown_keys = Vector{String}[]
collect_unmatched_keys!(unknown_keys, this, ref, String[])
return unknown_keys
Expand Down Expand Up @@ -211,19 +173,20 @@ Retrieves the current configuration value.
Among the registered configuration files, fetches the value in order of priority (see `Base.lt(::ConfigFileOrder, path1, path2)`).
If the key path does not exist in any of the configurations, returns `nothing`.
"""
function get_config(manager::ConfigManager, key_path::String...)
is_static_setting(key_path...) &&
return access_nested_dict(load(manager).static_settings, key_path...)
for config in values(load(manager).watched_files)
return @something access_nested_dict(config, key_path...) continue
Base.@constprop :aggressive function get_config(manager::ConfigManager, key_path::Symbol...)
try
is_static_setting(key_path...) &&
return getobjpath(load(manager).static_settings, key_path...)
return getobjpath(load(manager).current_settings, key_path...)
catch e
e isa FieldError ? nothing : rethrow()
end
return nothing
end

function fix_static_settings!(manager::ConfigManager)
store!(manager) do old_data
new_static = get_static_settings(old_data)
new_data = ConfigManagerData(new_static, old_data.watched_files)
new_static = get_current_settings(old_data.watched_files)
new_data = ConfigManagerData(old_data.current_settings, new_static, old_data.watched_files)
return new_data, new_static
end
end
Expand All @@ -235,7 +198,7 @@ If the file does not exist or cannot be parsed, just return leaving the current
configuration unchanged. When there are unknown keys in the config file,
send error message while leaving current configuration unchanged.
"""
function load_config!(on_leaf, server::Server, filepath::AbstractString;
function load_config!(callback, server::Server, filepath::AbstractString;
reload::Bool = false)
store!(server.state.config_manager) do old_data
if reload
Expand All @@ -247,43 +210,46 @@ function load_config!(on_leaf, server::Server, filepath::AbstractString;
parsed = TOML.tryparsefile(filepath)
parsed isa TOML.ParserError && return old_data, nothing

new_config = to_config_dict(parsed)

unknown_keys = collect_unmatched_keys(new_config)
if !isempty(unknown_keys)
new_config = try
Configurations.from_dict(JETLSConfig, parsed)
catch e
# TODO: remove this when Configurations.jl support to report
# full path of unknown key.
if e isa Configurations.InvalidKeyError
config_dict = to_config_dict(parsed)
unknown_keys = collect_unmatched_keys(config_dict)
if !isempty(unknown_keys)
show_error_message(server, """
Configuration file at $filepath contains unknown keys:
$(join(map(x -> string('`', join(x, "."), '`'), unknown_keys), ", "))
""")
return old_data, nothing
end
end
show_error_message(server, """
Configuration file at $filepath contains unknown keys:
$(join(map(x -> string('`', join(x, "."), '`'), unknown_keys), ", "))
Failed to load configuration file at $filepath:
$(e)
""")
return old_data, nothing
end

current_config = get(old_data.watched_files, filepath, DEFAULT_CONFIG)
merged_config = traverse_merge(current_config, new_config) do filepath, v
on_leaf(current_config, filepath, v)
v
end
new_watched_files = copy(old_data.watched_files)
new_watched_files[filepath] = merged_config
new_data = ConfigManagerData(old_data.static_settings, new_watched_files)
new_watched_files[filepath] = new_config
new_current_settings = get_current_settings(new_watched_files)
on_difference(callback, old_data.current_settings, new_current_settings)
new_data = ConfigManagerData(new_current_settings, old_data.static_settings, new_watched_files)
return new_data, nothing
end
end

to_config_dict(dict::Dict{String,Any}) = ConfigDict(
(k => (v isa Dict{String,Any} ? to_config_dict(v) : v) for (k, v) in dict)...)

function delete_config!(on_leaf, manager::ConfigManager, filepath::AbstractString)
function delete_config!(callback, manager::ConfigManager, filepath::AbstractString)
store!(manager) do old_data
old_settings = get_settings(old_data)
haskey(old_data.watched_files, filepath) || return old_data, nothing
new_watched_files = copy(old_data.watched_files)
delete!(new_watched_files, filepath)
new_data = ConfigManagerData(old_data.static_settings, new_watched_files)
new_settings = get_settings(new_data)
traverse_merge(old_settings, new_settings) do path, v
on_leaf(old_settings, path, v)
nothing
end
new_current_settings = get_current_settings(new_watched_files)
new_data = ConfigManagerData(new_current_settings, old_data.static_settings, new_watched_files)
on_difference(callback, old_data.current_settings, new_current_settings)
return new_data, nothing
end
end
Loading
Loading