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
+185 -1
View File
@@ -1,6 +1,6 @@
use crate::chrome::{ChromeLine, PopupConfig, validate_name};
use crate::clipboard::{ClipboardMode, SystemClipboard};
use crate::diff::{DiffState, Focus, Operation, Outcome, Phase, RevisionTarget};
use crate::diff::{DiffState, Focus, Operation, Outcome, Phase, RevisionTarget, ViewMode};
use crate::files::{DockSide, FilePicker, FilePresentation, FileView};
use crate::panes::{PaneDirection, PaneRect, PaneTree, SplitAxis, directional_neighbor};
use crate::plugin::{
@@ -703,6 +703,18 @@ core_actions![
true
),
("core.diff", Action::Diff, &[Mode::Normal], true),
(
"core.diff.view.toggle",
Action::DiffViewToggle,
&[Mode::Normal],
true,
),
(
"core.diff.orientation.toggle",
Action::DiffOrientationToggle,
&[Mode::Normal],
true,
),
(
"core.word_wrap.toggle",
Action::WordWrapToggle,
@@ -1652,6 +1664,7 @@ pub struct Editor {
diff_bound: Option<(u64, usize, Focus, usize)>,
diff_return_buffer: Option<usize>,
diff_context_options: (usize, usize),
diff_display_options: (ViewMode, SplitAxis),
next_diff_request: u64,
outstanding_diff_request: Option<u64>,
pending_lsp_navigation: Option<PendingLspNavigation>,
@@ -1770,6 +1783,7 @@ impl Editor {
diff_bound: None,
diff_return_buffer: None,
diff_context_options: (10, 15),
diff_display_options: (ViewMode::Split, SplitAxis::Vertical),
next_diff_request: 0,
outstanding_diff_request: None,
pending_lsp_navigation: None,
@@ -1803,6 +1817,8 @@ impl Editor {
("normal", "leader b p", "core.buffer_previous"),
("normal", "leader b l", "core.buffers"),
("normal", "leader b d", "core.buffer_delete"),
("normal", "leader v", "core.diff.view.toggle"),
("normal", "leader s", "core.diff.orientation.toggle"),
] {
editor
.bind(mode, key, Some(target.into()))
@@ -2445,6 +2461,10 @@ impl Editor {
}
}
pub fn set_diff_display_options(&mut self, view_mode: ViewMode, split_axis: SplitAxis) {
self.diff_display_options = (view_mode, split_axis);
}
pub(crate) fn take_diff_request(&mut self) -> Option<(u64, Operation)> {
let mut operation = self
.diff
@@ -2576,6 +2596,8 @@ impl Editor {
self.outstanding_diff_request = None;
let mut diff = DiffState::new();
diff.word_wrap = self.active.view.word_wrap;
diff.view_mode = self.diff_display_options.0;
diff.split_axis = self.diff_display_options.1;
diff.set_context_options(self.diff_context_options.0, self.diff_context_options.1);
self.diff = Some(diff);
}
@@ -3122,6 +3144,8 @@ impl Editor {
| Action::FilePicker
| Action::FileExplorer
| Action::DiffClose
| Action::DiffViewToggle
| Action::DiffOrientationToggle
| Action::WordWrapToggle
| Action::Quit
| Action::ForceQuit
@@ -5881,6 +5905,8 @@ impl Editor {
| Action::BufferForceDelete
| Action::Diff
| Action::DiffClose
| Action::DiffViewToggle
| Action::DiffOrientationToggle
| Action::WordWrapToggle
| Action::LspDefinition
| Action::LspHover
@@ -6685,6 +6711,44 @@ impl Editor {
}
return None;
}
if matches!(
entry.action,
Action::DiffViewToggle | Action::DiffOrientationToggle
) {
let name = entry.ex.expect("diff display action has an Ex command");
if !args.is_empty() {
self.set_message(format!("The {name} command takes no arguments"));
return None;
}
if let Some(owner) = self.diff_owner.filter(|owner| *owner != self.pane_id) {
return self.with_pane(owner, |editor| editor.execute_action(entry, args));
}
self.count = None;
self.clear_sequence();
self.palette = None;
if self.active.view.mode == Mode::Command {
self.active.view.command_line.clear();
self.active.view.mode = Mode::Normal;
}
let Some(diff) = self.diff.as_mut().filter(|diff| diff.phase == Phase::View) else {
self.set_message("No comparison is open");
return None;
};
if entry.action == Action::DiffViewToggle {
diff.set_view_mode(match diff.view_mode {
ViewMode::Split => ViewMode::Unified,
ViewMode::Unified => ViewMode::Split,
});
} else if diff.view_mode == ViewMode::Split {
diff.split_axis = match diff.split_axis {
SplitAxis::Vertical => SplitAxis::Horizontal,
SplitAxis::Horizontal => SplitAxis::Vertical,
};
} else {
self.set_message("Diff orientation applies only to split view");
}
return None;
}
if entry.action == Action::WordWrapToggle {
if !args.is_empty() {
self.set_message("The word_wrap.toggle command takes no arguments");
@@ -12844,6 +12908,126 @@ mod tests {
assert_eq!(editor.lines(), ["beta"]);
}
#[test]
fn diff_display_defaults_tabs_and_remaps_preserve_native_snapshot_carets() {
let directory = Directory::new();
let mut editor = editor();
editor.execute("diff.view.toggle");
assert_eq!(editor.message(), "No comparison is open");
editor.set_diff_display_options(ViewMode::Unified, SplitAxis::Horizontal);
editor.execute("diff");
type_text(&mut editor, " v");
assert_eq!(editor.diff().unwrap().query, " v");
assert_eq!(editor.diff().unwrap().view_mode, ViewMode::Unified);
historical_diff(
&mut editor,
&directory.0,
vec!["old one".into(), "old two".into()],
vec!["new one".into(), "new two".into()],
);
let first_tab = editor.tab_id();
let buffer = editor.buffer_id();
editor.handle(Input::Char('j'));
let caret = editor.cursor();
let text = editor.lines().to_vec();
assert!(!editor.diff_editing());
assert_eq!(editor.diff().unwrap().view_mode, ViewMode::Unified);
editor.execute("diff.orientation.toggle");
assert_eq!(editor.diff().unwrap().split_axis, SplitAxis::Horizontal);
editor
.bind("normal", "Q", Some("core.diff.view.toggle".into()))
.unwrap();
assert!(
editor
.bind("insert", "Q", Some("core.diff.view.toggle".into()))
.is_err()
);
editor.handle(Input::Char('Q'));
assert_eq!(editor.diff().unwrap().view_mode, ViewMode::Split);
editor
.bind("normal", "W", Some("core.diff.orientation.toggle".into()))
.unwrap();
editor.handle(Input::Char('W'));
assert_eq!(editor.diff().unwrap().split_axis, SplitAxis::Vertical);
type_text(&mut editor, " v");
assert_eq!(editor.diff().unwrap().view_mode, ViewMode::Unified);
assert_eq!(editor.buffer_id(), buffer);
assert_eq!(editor.cursor(), caret);
assert_eq!(editor.lines(), text);
editor.set_diff_display_options(ViewMode::Split, SplitAxis::Horizontal);
assert_eq!(editor.diff().unwrap().view_mode, ViewMode::Unified);
editor.handle(Input::Ctrl('p'));
type_text(&mut editor, "diff.view.toggle");
assert!(
editor
.palette()
.unwrap()
.entries
.iter()
.any(|entry| entry.name == "core.diff.view.toggle" && entry.enabled)
);
editor.handle(Input::Enter);
assert_eq!(editor.diff().unwrap().view_mode, ViewMode::Split);
editor.execute("tab.new");
historical_diff(
&mut editor,
&directory.0,
vec!["old".into()],
vec!["new".into()],
);
assert_eq!(editor.diff().unwrap().view_mode, ViewMode::Split);
assert_eq!(editor.diff().unwrap().split_axis, SplitAxis::Horizontal);
editor.execute("tab.previous");
assert_eq!(editor.tab_id(), first_tab);
assert_eq!(editor.buffer_id(), buffer);
assert_eq!(editor.cursor(), caret);
assert_eq!(editor.diff().unwrap().split_axis, SplitAxis::Vertical);
}
#[test]
fn diff_display_actions_keep_working_edits_files_and_filter_input_native() {
let directory = Directory::new();
let first = directory.path("first.txt");
fs::write(&first, "base").unwrap();
fs::write(directory.path("second.txt"), "second").unwrap();
let mut editor = Editor::open(Some(first), vec![]).unwrap();
let working = editor.buffer_id();
working_diff(&mut editor, &directory.0, &["first.txt", "second.txt"]);
assert_eq!(editor.diff().unwrap().focus, Focus::Files);
type_text(&mut editor, " s");
assert_eq!(editor.diff().unwrap().split_axis, SplitAxis::Horizontal);
type_text(&mut editor, " v");
assert_eq!(editor.diff().unwrap().view_mode, ViewMode::Unified);
editor.handle(Input::Char('i'));
type_text(&mut editor, " v");
assert_eq!(editor.diff().unwrap().view_mode, ViewMode::Unified);
assert_eq!(editor.file_picker().unwrap().query, " v");
editor.handle(Input::Escape);
editor.handle(Input::BackTab);
assert!(editor.diff_editing());
assert_eq!(editor.buffer_id(), working);
type_text(&mut editor, "i dv");
assert_eq!(editor.lines(), [" dvbase"]);
editor.handle(Input::Escape);
assert_eq!(editor.diff().unwrap().view_mode, ViewMode::Unified);
assert!(editor.diff().unwrap().after_display_row(0).is_some());
let caret = editor.cursor();
type_text(&mut editor, " v");
assert_eq!(editor.diff().unwrap().view_mode, ViewMode::Split);
assert_eq!(editor.cursor(), caret);
assert!(editor.diff_editing());
assert!(editor.diff.as_mut().unwrap().select_file_index(1));
editor.bind_diff_buffer();
assert_eq!(editor.diff().unwrap().file, 1);
assert_eq!(editor.diff().unwrap().split_axis, SplitAxis::Horizontal);
assert_eq!(editor.diff().unwrap().view_mode, ViewMode::Split);
assert_eq!(editor.diff().unwrap().focus, Focus::After);
type_text(&mut editor, " v");
assert_eq!(editor.diff().unwrap().view_mode, ViewMode::Unified);
assert!(editor.diff_editing());
assert_eq!(editor.lines(), ["second"]);
}
#[test]
fn word_wrap_toggle_retains_buffers_and_readonly_diff_state() {
let directory = Directory::new();