Skip to content

Commit bdc2303

Browse files
committed
Fix merge bug in Config::load
Currently `Config::load` merges editor configs with `merge_toml_values(global, local, 3)` Besides the puzzling use of the magic number `3`, this is a bug, because it concatenates arrays from the global config file with those from the local config file. For example `editor.shell=["bash", "-c"]` and `editor.shell=["fish", "-c"]` would become `editor.shell=["bash", "-c", "fish", "-c"]`. This commit adds a new function `merge_toml_values_with_strategy` and changes `Config::load` to call: ``` merge_toml_values_with_strategy( global, local, &MergeStrategy { array: MergeMode::Never, table: MergeMode::Always, }, ) ``` For backward compatability, the original function `merge_toml_values` calls: ``` merge_toml_values_with_strategy( left, right, &MergeStrategy { array: MergeMode::MaxDepth(max_merge_depth), table: MergeMode::MaxDepth(max_merge_depth), }, ) ``` , so there should be no change in the way themes and languages.toml are merged. Also update doc string and add tests.
1 parent 8b952bb commit bdc2303

File tree

2 files changed

+122
-18
lines changed

2 files changed

+122
-18
lines changed

helix-loader/src/lib.rs

+76-10
Original file line numberDiff line numberDiff line change
@@ -152,39 +152,102 @@ pub fn default_log_file() -> PathBuf {
152152
cache_dir().join("helix.log")
153153
}
154154

155+
pub struct MergeStrategy {
156+
pub array: MergeMode,
157+
pub table: MergeMode,
158+
}
159+
160+
pub enum MergeMode {
161+
Never,
162+
Always,
163+
MaxDepth(usize),
164+
}
165+
166+
impl MergeMode {
167+
pub fn should_merge(&self, depth: usize) -> bool {
168+
match self {
169+
MergeMode::Always => true,
170+
MergeMode::MaxDepth(max_depth) => depth < *max_depth,
171+
MergeMode::Never => false,
172+
}
173+
}
174+
}
175+
155176
/// Merge two TOML documents, merging values from `right` onto `left`
156177
///
157-
/// `merge_depth` sets the nesting depth up to which values are merged instead
158-
/// of overridden.
178+
/// `max_merge_depth` sets the nesting depth up to which values are merged
179+
/// instead of overridden.
180+
///
181+
/// When an array exists in both `left` and `right`, the merged array is formed
182+
/// by concatenating `left`'s elements with `right`'s. But if any elements share
183+
/// the same `name` field, they are merged recursively and included only once.
159184
///
160185
/// When a table exists in both `left` and `right`, the merged table consists of
161186
/// all keys in `left`'s table unioned with all keys in `right` with the values
162187
/// of `right` being merged recursively onto values of `left`.
163188
///
189+
/// Setting `max_merge_depth` is useful for TOML documents that use a
190+
/// top-level array of values, where the top-level arrays should be merged
191+
/// but nested arrays should act as overrides. For the `languages.toml`
192+
/// config for example, this means that you can specify a sub-set of
193+
/// languages in an overriding `languages.toml` but that nested arrays
194+
/// like Language Server arguments are replaced instead of merged.
195+
///
164196
/// `crate::merge_toml_values(a, b, 3)` combines, for example:
165197
///
166-
/// b:
198+
/// a:
167199
/// ```toml
168200
/// [[language]]
169201
/// name = "toml"
202+
/// scope = "source.toml"
170203
/// language-server = { command = "taplo", args = ["lsp", "stdio"] }
171204
/// ```
172-
/// a:
205+
/// b:
173206
/// ```toml
174207
/// [[language]]
208+
/// name = "toml"
175209
/// language-server = { command = "/usr/bin/taplo" }
176210
/// ```
177211
///
178212
/// into:
179213
/// ```toml
180214
/// [[language]]
181215
/// name = "toml"
216+
/// scope = "source.toml"
182217
/// language-server = { command = "/usr/bin/taplo" }
183218
/// ```
184219
///
185-
/// thus it overrides the third depth-level of b with values of a if they exist,
220+
/// thus it overrides the third depth-level of a with values of b if they exist,
186221
/// but otherwise merges their values
187-
pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usize) -> toml::Value {
222+
pub fn merge_toml_values(
223+
left: toml::Value,
224+
right: toml::Value,
225+
max_merge_depth: usize,
226+
) -> toml::Value {
227+
merge_toml_values_with_strategy(
228+
left,
229+
right,
230+
&MergeStrategy {
231+
array: MergeMode::MaxDepth(max_merge_depth),
232+
table: MergeMode::MaxDepth(max_merge_depth),
233+
},
234+
)
235+
}
236+
237+
pub fn merge_toml_values_with_strategy(
238+
left: toml::Value,
239+
right: toml::Value,
240+
strategy: &MergeStrategy,
241+
) -> toml::Value {
242+
merge_toml_values_recursive(left, right, strategy, 0)
243+
}
244+
245+
fn merge_toml_values_recursive(
246+
left: toml::Value,
247+
right: toml::Value,
248+
strategy: &MergeStrategy,
249+
depth: usize,
250+
) -> toml::Value {
188251
use toml::Value;
189252

190253
fn get_name(v: &Value) -> Option<&str> {
@@ -193,7 +256,7 @@ pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usi
193256

194257
match (left, right) {
195258
(Value::Array(mut left_items), Value::Array(right_items)) => {
196-
if merge_depth > 0 {
259+
if strategy.array.should_merge(depth) {
197260
left_items.reserve(right_items.len());
198261
for rvalue in right_items {
199262
let lvalue = get_name(&rvalue)
@@ -202,7 +265,9 @@ pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usi
202265
})
203266
.map(|lpos| left_items.remove(lpos));
204267
let mvalue = match lvalue {
205-
Some(lvalue) => merge_toml_values(lvalue, rvalue, merge_depth - 1),
268+
Some(lvalue) => {
269+
merge_toml_values_recursive(lvalue, rvalue, strategy, depth + 1)
270+
}
206271
None => rvalue,
207272
};
208273
left_items.push(mvalue);
@@ -213,11 +278,12 @@ pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usi
213278
}
214279
}
215280
(Value::Table(mut left_map), Value::Table(right_map)) => {
216-
if merge_depth > 0 {
281+
if strategy.table.should_merge(depth) {
217282
for (rname, rvalue) in right_map {
218283
match left_map.remove(&rname) {
219284
Some(lvalue) => {
220-
let merged_value = merge_toml_values(lvalue, rvalue, merge_depth - 1);
285+
let merged_value =
286+
merge_toml_values_recursive(lvalue, rvalue, strategy, depth + 1);
221287
left_map.insert(rname, merged_value);
222288
}
223289
None => {

helix-term/src/config.rs

+46-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::keymap;
22
use crate::keymap::{merge_keys, KeyTrie};
3-
use helix_loader::merge_toml_values;
3+
use helix_loader::{merge_toml_values_with_strategy, MergeMode, MergeStrategy};
44
use helix_view::document::Mode;
55
use serde::Deserialize;
66
use std::collections::HashMap;
@@ -79,9 +79,16 @@ impl Config {
7979
(None, Some(val)) | (Some(val), None) => {
8080
val.try_into().map_err(ConfigLoadError::BadConfig)?
8181
}
82-
(Some(global), Some(local)) => merge_toml_values(global, local, 3)
83-
.try_into()
84-
.map_err(ConfigLoadError::BadConfig)?,
82+
(Some(global), Some(local)) => merge_toml_values_with_strategy(
83+
global,
84+
local,
85+
&MergeStrategy {
86+
array: MergeMode::Never,
87+
table: MergeMode::Always,
88+
},
89+
)
90+
.try_into()
91+
.map_err(ConfigLoadError::BadConfig)?,
8592
};
8693

8794
Config {
@@ -131,11 +138,42 @@ mod tests {
131138
use super::*;
132139

133140
impl Config {
134-
fn load_test(config: &str) -> Config {
135-
Config::load(Ok(config.to_owned()), Err(ConfigLoadError::default())).unwrap()
141+
fn load_test(global: &str, local: &str) -> Config {
142+
Config::load(Ok(global.to_owned()), Ok(local.to_owned())).unwrap()
136143
}
137144
}
138145

146+
#[test]
147+
fn should_merge_editor_config_tables() {
148+
let global = r#"
149+
[editor.statusline]
150+
mode.insert = "INSERT"
151+
mode.select = "SELECT"
152+
"#;
153+
let local = r#"
154+
[editor.statusline]
155+
mode.select = "VIS"
156+
"#;
157+
let config = Config::load_test(global, local);
158+
assert_eq!(config.editor.statusline.mode.normal, "NOR"); // Default
159+
assert_eq!(config.editor.statusline.mode.insert, "INSERT"); // Global
160+
assert_eq!(config.editor.statusline.mode.select, "VIS"); // Local
161+
}
162+
163+
#[test]
164+
fn should_override_editor_config_arrays() {
165+
let global = r#"
166+
[editor]
167+
shell = ["bash", "-c"]
168+
"#;
169+
let local = r#"
170+
[editor]
171+
shell = ["fish", "-c"]
172+
"#;
173+
let config = Config::load_test(global, local);
174+
assert_eq!(config.editor.shell, ["fish", "-c"]);
175+
}
176+
139177
#[test]
140178
fn parsing_keymaps_config_file() {
141179
use crate::keymap;
@@ -166,7 +204,7 @@ mod tests {
166204
);
167205

168206
assert_eq!(
169-
Config::load_test(sample_keymaps),
207+
Config::load_test(sample_keymaps, ""),
170208
Config {
171209
keys,
172210
..Default::default()
@@ -177,7 +215,7 @@ mod tests {
177215
#[test]
178216
fn keys_resolve_to_correct_defaults() {
179217
// From serde default
180-
let default_keys = Config::load_test("").keys;
218+
let default_keys = Config::load_test("", "").keys;
181219
assert_eq!(default_keys, keymap::default());
182220

183221
// From the Default trait

0 commit comments

Comments
 (0)