pull/13332/merge
Thomas Miedema 2025-06-13 14:47:07 -04:00 committed by GitHub
commit 2dfb09e1d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 125 additions and 21 deletions

View File

@ -152,26 +152,60 @@ pub fn default_log_file() -> PathBuf {
cache_dir().join("helix.log") cache_dir().join("helix.log")
} }
pub struct MergeStrategy {
pub array: MergeMode,
pub table: MergeMode,
}
pub enum MergeMode {
Never,
Always,
MaxDepth(usize),
}
impl MergeMode {
pub fn should_merge(&self, depth: usize) -> bool {
match self {
MergeMode::Always => true,
MergeMode::MaxDepth(max_depth) => depth < *max_depth,
MergeMode::Never => false,
}
}
}
/// Merge two TOML documents, merging values from `right` onto `left` /// Merge two TOML documents, merging values from `right` onto `left`
/// ///
/// `merge_depth` sets the nesting depth up to which values are merged instead /// `max_merge_depth` sets the nesting depth up to which values are merged
/// of overridden. /// instead of overridden.
///
/// When an array exists in both `left` and `right`, the merged array is formed
/// by concatenating `left`'s elements with `right`'s. But if any elements share
/// the same `name` field, they are merged recursively and included only once.
/// ///
/// When a table exists in both `left` and `right`, the merged table consists of /// When a table exists in both `left` and `right`, the merged table consists of
/// all keys in `left`'s table unioned with all keys in `right` with the values /// all keys in `left`'s table unioned with all keys in `right` with the values
/// of `right` being merged recursively onto values of `left`. /// of `right` being merged recursively onto values of `left`.
/// ///
/// Setting `max_merge_depth` is useful for TOML documents that use a
/// top-level array of values, where the top-level arrays should be merged
/// but nested arrays should act as overrides. For the `languages.toml`
/// config for example, this means that you can specify a sub-set of
/// languages in an overriding `languages.toml` but that nested arrays
/// like Language Server arguments are replaced instead of merged.
///
/// `crate::merge_toml_values(a, b, 3)` combines, for example: /// `crate::merge_toml_values(a, b, 3)` combines, for example:
/// ///
/// a:
/// ```toml
/// [[language]]
/// name = "toml"
/// scope = "source.toml"
/// language-server = { command = "taplo", args = ["lsp", "stdio"] }
/// ```
/// b: /// b:
/// ```toml /// ```toml
/// [[language]] /// [[language]]
/// name = "toml" /// name = "toml"
/// language-server = { command = "taplo", args = ["lsp", "stdio"] }
/// ```
/// a:
/// ```toml
/// [[language]]
/// language-server = { command = "/usr/bin/taplo" } /// language-server = { command = "/usr/bin/taplo" }
/// ``` /// ```
/// ///
@ -179,12 +213,41 @@ pub fn default_log_file() -> PathBuf {
/// ```toml /// ```toml
/// [[language]] /// [[language]]
/// name = "toml" /// name = "toml"
/// scope = "source.toml"
/// language-server = { command = "/usr/bin/taplo" } /// language-server = { command = "/usr/bin/taplo" }
/// ``` /// ```
/// ///
/// thus it overrides the third depth-level of b with values of a if they exist, /// thus it overrides the third depth-level of a with values of b if they exist,
/// but otherwise merges their values /// but otherwise merges their values
pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usize) -> toml::Value { pub fn merge_toml_values(
left: toml::Value,
right: toml::Value,
max_merge_depth: usize,
) -> toml::Value {
merge_toml_values_with_strategy(
left,
right,
&MergeStrategy {
array: MergeMode::MaxDepth(max_merge_depth),
table: MergeMode::MaxDepth(max_merge_depth),
},
)
}
pub fn merge_toml_values_with_strategy(
left: toml::Value,
right: toml::Value,
strategy: &MergeStrategy,
) -> toml::Value {
merge_toml_values_recursive(left, right, strategy, 0)
}
fn merge_toml_values_recursive(
left: toml::Value,
right: toml::Value,
strategy: &MergeStrategy,
depth: usize,
) -> toml::Value {
use toml::Value; use toml::Value;
fn get_name(v: &Value) -> Option<&str> { 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
match (left, right) { match (left, right) {
(Value::Array(mut left_items), Value::Array(right_items)) => { (Value::Array(mut left_items), Value::Array(right_items)) => {
if merge_depth > 0 { if strategy.array.should_merge(depth) {
left_items.reserve(right_items.len()); left_items.reserve(right_items.len());
for rvalue in right_items { for rvalue in right_items {
let lvalue = get_name(&rvalue) let lvalue = get_name(&rvalue)
@ -202,7 +265,9 @@ pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usi
}) })
.map(|lpos| left_items.remove(lpos)); .map(|lpos| left_items.remove(lpos));
let mvalue = match lvalue { let mvalue = match lvalue {
Some(lvalue) => merge_toml_values(lvalue, rvalue, merge_depth - 1), Some(lvalue) => {
merge_toml_values_recursive(lvalue, rvalue, strategy, depth + 1)
}
None => rvalue, None => rvalue,
}; };
left_items.push(mvalue); left_items.push(mvalue);
@ -213,11 +278,12 @@ pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usi
} }
} }
(Value::Table(mut left_map), Value::Table(right_map)) => { (Value::Table(mut left_map), Value::Table(right_map)) => {
if merge_depth > 0 { if strategy.table.should_merge(depth) {
for (rname, rvalue) in right_map { for (rname, rvalue) in right_map {
match left_map.remove(&rname) { match left_map.remove(&rname) {
Some(lvalue) => { Some(lvalue) => {
let merged_value = merge_toml_values(lvalue, rvalue, merge_depth - 1); let merged_value =
merge_toml_values_recursive(lvalue, rvalue, strategy, depth + 1);
left_map.insert(rname, merged_value); left_map.insert(rname, merged_value);
} }
None => { None => {

View File

@ -1,6 +1,6 @@
use crate::keymap; use crate::keymap;
use crate::keymap::{merge_keys, KeyTrie}; use crate::keymap::{merge_keys, KeyTrie};
use helix_loader::merge_toml_values; use helix_loader::{merge_toml_values_with_strategy, MergeMode, MergeStrategy};
use helix_view::document::Mode; use helix_view::document::Mode;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
@ -79,7 +79,14 @@ impl Config {
(None, Some(val)) | (Some(val), None) => { (None, Some(val)) | (Some(val), None) => {
val.try_into().map_err(ConfigLoadError::BadConfig)? val.try_into().map_err(ConfigLoadError::BadConfig)?
} }
(Some(global), Some(local)) => merge_toml_values(global, local, 3) (Some(global), Some(local)) => merge_toml_values_with_strategy(
global,
local,
&MergeStrategy {
array: MergeMode::Never,
table: MergeMode::Always,
},
)
.try_into() .try_into()
.map_err(ConfigLoadError::BadConfig)?, .map_err(ConfigLoadError::BadConfig)?,
}; };
@ -131,11 +138,42 @@ mod tests {
use super::*; use super::*;
impl Config { impl Config {
fn load_test(config: &str) -> Config { fn load_test(global: &str, local: &str) -> Config {
Config::load(Ok(config.to_owned()), Err(ConfigLoadError::default())).unwrap() Config::load(Ok(global.to_owned()), Ok(local.to_owned())).unwrap()
} }
} }
#[test]
fn should_merge_editor_config_tables() {
let global = r#"
[editor.statusline]
mode.insert = "INSERT"
mode.select = "SELECT"
"#;
let local = r#"
[editor.statusline]
mode.select = "VIS"
"#;
let config = Config::load_test(global, local);
assert_eq!(config.editor.statusline.mode.normal, "NOR"); // Default
assert_eq!(config.editor.statusline.mode.insert, "INSERT"); // Global
assert_eq!(config.editor.statusline.mode.select, "VIS"); // Local
}
#[test]
fn should_override_editor_config_arrays() {
let global = r#"
[editor]
shell = ["bash", "-c"]
"#;
let local = r#"
[editor]
shell = ["fish", "-c"]
"#;
let config = Config::load_test(global, local);
assert_eq!(config.editor.shell, ["fish", "-c"]);
}
#[test] #[test]
fn parsing_keymaps_config_file() { fn parsing_keymaps_config_file() {
use crate::keymap; use crate::keymap;
@ -166,7 +204,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
Config::load_test(sample_keymaps), Config::load_test(sample_keymaps, ""),
Config { Config {
keys, keys,
..Default::default() ..Default::default()
@ -177,7 +215,7 @@ mod tests {
#[test] #[test]
fn keys_resolve_to_correct_defaults() { fn keys_resolve_to_correct_defaults() {
// From serde default // From serde default
let default_keys = Config::load_test("").keys; let default_keys = Config::load_test("", "").keys;
assert_eq!(default_keys, keymap::default()); assert_eq!(default_keys, keymap::default());
// From the Default trait // From the Default trait