feat: add configurable diff modes and split orientation

This commit is contained in:
2026-10-12 23:43:15 +02:00
parent c35abe0f03
commit d1855dbdb7
14 changed files with 847 additions and 169 deletions
+75 -61
View File
@@ -44,6 +44,8 @@ pub struct Config {
pub labels: BTreeMap<String, String>,
pub diff_context_lines: usize,
pub diff_expand_lines: usize,
pub diff_view_mode: crate::diff::ViewMode,
pub diff_split_orientation: crate::panes::SplitAxis,
}
#[derive(Debug, Deserialize)]
@@ -84,6 +86,8 @@ impl Default for Config {
labels: BTreeMap::new(),
diff_context_lines: 10,
diff_expand_lines: 15,
diff_view_mode: crate::diff::ViewMode::Split,
diff_split_orientation: crate::panes::SplitAxis::Vertical,
}
}
}
@@ -95,23 +99,26 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
let mut host = Host::spawn("config", &path, &serde_json::json!({}))?;
let response = host.receive(deadline)?;
match response {
Response::Config {
plugins,
lsp,
bindings,
leader,
file_explorer_side,
file_picker_view,
file_explorer_view,
clipboard_mode,
top_bar,
bottom_bar,
popup,
labels,
diff_context_lines,
diff_expand_lines,
..
} => {
Response::Config(response) => {
let ConfigFrame {
plugins,
lsp,
bindings,
leader,
file_explorer_side,
file_picker_view,
file_explorer_view,
clipboard_mode,
top_bar,
bottom_bar,
popup,
labels,
diff_context_lines,
diff_expand_lines,
diff_view_mode,
diff_split_orientation,
..
} = *response;
let mut config = Config::default();
let directory = path.parent().expect("absolute file has a parent");
for plugin in plugins {
@@ -138,6 +145,8 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
config.labels = labels;
config.diff_context_lines = diff_context_lines;
config.diff_expand_lines = diff_expand_lines;
config.diff_view_mode = diff_view_mode;
config.diff_split_orientation = diff_split_orientation;
Ok(config)
}
Response::Error {
@@ -429,6 +438,36 @@ fn diff_expand_lines<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Resul
diff_line_count(deserializer, "diff_expand_lines", 1)
}
// Startup-only configuration does not inflate every queued plugin response.
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ConfigFrame {
version: u8,
plugins: Vec<PluginConfig>,
// Keep this wire field required; resolve its configuration after decoding.
#[serde(deserialize_with = "serde_json::Value::deserialize")]
lsp: serde_json::Value,
bindings: Vec<Binding>,
leader: String,
file_explorer_side: String,
file_picker_view: String,
file_explorer_view: String,
clipboard_mode: String,
#[serde(deserialize_with = "crate::chrome::top")]
top_bar: ChromeLine,
#[serde(deserialize_with = "crate::chrome::bottom")]
bottom_bar: ChromeLine,
popup: PopupConfig,
#[serde(deserialize_with = "crate::chrome::labels")]
labels: BTreeMap<String, String>,
#[serde(deserialize_with = "diff_context_lines")]
diff_context_lines: usize,
#[serde(deserialize_with = "diff_expand_lines")]
diff_expand_lines: usize,
diff_view_mode: crate::diff::ViewMode,
diff_split_orientation: crate::panes::SplitAxis,
}
#[derive(Deserialize)]
#[serde(tag = "type", deny_unknown_fields)]
enum Response {
@@ -442,30 +481,7 @@ enum Response {
asynchronous: bool,
},
#[serde(rename = "config")]
Config {
version: u8,
plugins: Vec<PluginConfig>,
// Keep this wire field required; resolve its configuration after decoding.
#[serde(deserialize_with = "serde_json::Value::deserialize")]
lsp: serde_json::Value,
bindings: Vec<Binding>,
leader: String,
file_explorer_side: String,
file_picker_view: String,
file_explorer_view: String,
clipboard_mode: String,
#[serde(deserialize_with = "crate::chrome::top")]
top_bar: ChromeLine,
#[serde(deserialize_with = "crate::chrome::bottom")]
bottom_bar: ChromeLine,
popup: PopupConfig,
#[serde(deserialize_with = "crate::chrome::labels")]
labels: BTreeMap<String, String>,
#[serde(deserialize_with = "diff_context_lines")]
diff_context_lines: usize,
#[serde(deserialize_with = "diff_expand_lines")]
diff_expand_lines: usize,
},
Config(Box<ConfigFrame>),
#[serde(rename = "result")]
Result {
version: u8,
@@ -497,10 +513,10 @@ impl Response {
fn version(&self) -> u8 {
match self {
Self::Manifest { version, .. }
| Self::Config { version, .. }
| Self::Result { version, .. }
| Self::Update { version, .. }
| Self::Error { version, .. } => *version,
Self::Config(config) => config.version,
}
}
}
@@ -1188,9 +1204,9 @@ end
assert!(load_config(&path).unwrap_err().contains(error), "{value}");
}
for frame in [
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15}"#,
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":null,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15}"#,
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":7,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15}"#,
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"left_right"}"#,
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":null,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"left_right"}"#,
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":7,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"left_right"}"#,
] {
assert!(serde_json::from_str::<Response>(frame).is_err(), "{frame}");
}
@@ -1408,7 +1424,8 @@ end
"file_picker_view": "tree", "file_explorer_view": "tree",
"top_bar": {}, "bottom_bar": {}, "labels": {},
"popup": {"command": {}, "search": {}},
"clipboard_mode": "system", "diff_context_lines": 0, "diff_expand_lines": 7
"clipboard_mode": "system", "diff_context_lines": 0, "diff_expand_lines": 7,
"diff_view_mode": "split", "diff_split_orientation": "left_right"
});
for setting in ["diff_context_lines", "diff_expand_lines"] {
for value in [
@@ -1822,22 +1839,21 @@ end"#,
"file_picker_view": "tree", "file_explorer_view": "tree",
"top_bar": {}, "bottom_bar": {}, "labels": {},
"popup": {"command": {}, "search": {}},
"clipboard_mode": "system", "diff_context_lines": 10, "diff_expand_lines": 15
"clipboard_mode": "system", "diff_context_lines": 10, "diff_expand_lines": 15,
"diff_view_mode": "split", "diff_split_orientation": "left_right"
});
let Response::Config { lsp, .. } =
serde_json::from_value::<Response>(frame.clone()).unwrap()
let Response::Config(config) = serde_json::from_value::<Response>(frame.clone()).unwrap()
else {
panic!("expected config frame");
};
assert!(crate::lsp_config::resolve(lsp).unwrap().is_empty());
assert!(crate::lsp_config::resolve(config.lsp).unwrap().is_empty());
frame["lsp"] = serde_json::json!({"rust": true});
let Response::Config { lsp, .. } =
serde_json::from_value::<Response>(frame.clone()).unwrap()
let Response::Config(config) = serde_json::from_value::<Response>(frame.clone()).unwrap()
else {
panic!("expected config frame");
};
assert_eq!(
crate::lsp_config::resolve(lsp).unwrap()[0].command,
crate::lsp_config::resolve(config.lsp).unwrap()[0].command,
["rust-analyzer"]
);
for value in [
@@ -1861,12 +1877,12 @@ end"#,
serde_json::json!({"python": {"servers": [{"command": ["server"]}], "settings": {}}}),
] {
frame["lsp"] = value;
let Response::Config { lsp, .. } =
let Response::Config(config) =
serde_json::from_value::<Response>(frame.clone()).unwrap()
else {
panic!("expected config frame");
};
assert!(crate::lsp_config::resolve(lsp).is_err(), "{frame}");
assert!(crate::lsp_config::resolve(config.lsp).is_err(), "{frame}");
}
let mut languages: serde_json::Map<String, serde_json::Value> = (0..14)
.map(|index| {
@@ -1878,20 +1894,18 @@ end"#,
.collect();
languages.insert("python".into(), serde_json::json!(true));
frame["lsp"] = serde_json::Value::Object(languages);
let Response::Config { lsp, .. } =
serde_json::from_value::<Response>(frame.clone()).unwrap()
let Response::Config(config) = serde_json::from_value::<Response>(frame.clone()).unwrap()
else {
panic!("expected config frame");
};
assert_eq!(crate::lsp_config::resolve(lsp).unwrap().len(), 16);
assert_eq!(crate::lsp_config::resolve(config.lsp).unwrap().len(), 16);
frame["lsp"]["ruby"] = serde_json::json!(true);
let Response::Config { lsp, .. } =
serde_json::from_value::<Response>(frame.clone()).unwrap()
let Response::Config(config) = serde_json::from_value::<Response>(frame.clone()).unwrap()
else {
panic!("expected config frame");
};
assert_eq!(
crate::lsp_config::resolve(lsp).unwrap_err(),
crate::lsp_config::resolve(config.lsp).unwrap_err(),
"LSP configuration supports at most 16 configured servers"
);
frame.as_object_mut().unwrap().remove("lsp");