feat: show single-pane diffs for added deleted and renamed files

This commit is contained in:
2026-10-11 11:17:38 +02:00
parent 976cfc9e62
commit fc960ea0bf
4 changed files with 383 additions and 65 deletions
+4 -4
View File
@@ -222,11 +222,11 @@ The floating selector has **Revisions / Bookmarks** tabs for jj, or **Commits /
Remote references such as `some-bookmark@origin` appear from remote advertisements, including refs never fetched locally. Selecting one retrieves missing content into a temporary comparison repository. **Load history from origin** (or another configured remote) in Revisions/Commits imports searchable remote history without advancing the selection stage. These operations may require network access and the repository's existing credentials; errors stay in the selector. They do not update active refs, snapshot jj's working copy, or change the checkout.
The comparison has Before on the left, After on the right, and the changed-file explorer **docked on the right**. Before and historical/remote After snapshots are read-only. Selecting **Working checkout** as After binds eligible regular UTF-8 files to normal editor buffers, reusing already-open dirty buffers and their undo history. Focus After to use native editing, selections, search, completion, undo/redo, and `:w`. Removed files, binary files, symlinks, and submodules remain read-only. Closing the diff keeps its buffers and unsaved edits.
The changed-file explorer is **docked on the right**. Modified files show Before on the left and After on the right. Added files show only After, using the full text workspace; deleted files show only Before. Rename-only files show one empty pane, with the old and new paths in the explorer; renames with text or final-newline changes retain the split view. Before and historical/remote After snapshots are read-only. Selecting **Working checkout** as After binds eligible regular UTF-8 files to normal editor buffers, reusing already-open dirty buffers and their undo history. Focus After to use native editing, selections, search, completion, undo/redo, and `:w`. Removed files, rename-only files, binary files, symlinks, and submodules remain read-only. Closing the diff keeps its buffers and unsaved edits.
Use the ordinary **file-picker/file-explorer actions and bindings**: Space `o` opens a floating picker, Space `e` opens/focuses the docked explorer, and Ctrl+w closes the focused picker/explorer and reclaims its width. Remapped bindings work too; Ctrl+e is no longer reserved by the diff. The changed-path catalog retains A/M/D/R markers and the normal compact tree, filtering, folder expansion, and configurable `tree`/`selection` navigation. It includes removed and historical paths without scanning the working directory. Enter toggles a folder or opens the selected comparison file and focuses Before. While the explorer is hidden, Tab/Shift+Tab cycles only between the two text panes.
Use the ordinary **file-picker/file-explorer actions and bindings**: Space `o` opens a floating picker, Space `e` opens/focuses the docked explorer, and Ctrl+w closes the focused picker/explorer and reclaims its width. Remapped bindings work too; Ctrl+e is no longer reserved by the diff. The changed-path catalog retains A/M/D/R markers and the normal compact tree, filtering, folder expansion, and configurable `tree`/`selection` navigation. It includes removed and historical paths without scanning the working directory. Enter toggles a folder or opens the selected comparison file and focuses its first visible text pane: After for added files, otherwise Before. While the explorer is hidden, Tab/Shift+Tab cycles between visible text panes only; a single-pane view keeps its focus.
File rows show colored **A** (added, green), **M** (modified, yellow), **D** (removed, red), or **R** (renamed, cyan) markers in Tree and List views, including filtered results. Rename rows show the new name followed by `← old/path`; directories have no file-status marker. Ordinary file exploration remains unannotated.
File rows show colored **A** (added, green), **M** (modified, yellow), **D** (removed, red), or **R** (renamed, cyan) markers in Tree and List views, including filtered results. Rename rows show `R old/path -> new/path`; directories have no file-status marker. Ordinary file exploration remains unannotated.
Text panes show change excerpts with **10 unchanged lines above and below** each change, preserving original file line numbers. Overlapping or touching excerpts merge; omitted stretches show an unchanged-line count in both panes. Files with only metadata changes show their notice or “No text changes” rather than unrelated source.
@@ -240,7 +240,7 @@ Changed text rows use muted red backgrounds in Before and muted green background
Missing counterparts use adjacent muted `╱` diagonals (U+2571, box drawings light diagonal) on the normal terminal background, one per column. Unlike the text slash `/`, this glyph is designed to reach cell corners so diagonals can join end to end across successive rows. Pixel-perfect joins depend on the terminal's font and line-spacing rendering. Actual empty source lines retain their normal/change background and are not hatched.
In Normal mode, Tab/Shift+Tab cycles focus between Files, Before, and After. Read-only text panes use Up/Down or Ctrl+j/k for shared vertical scrolling, Left/Right or Ctrl+h/l for independent horizontal scrolling, and Home/End for the first/last displayed row; editable After uses normal buffer motions. Wheel input over text panes scrolls both panes without moving the editable caret; over Files it navigates visible tree rows without opening them. Escape cancels modes, prompts, or filtering, or returns picker focus to Before; **it never closes the diff**. Use **`:diff.close`**, find `diff.close` in the palette, or bind **`config.core.diff.close`** to leave the comparison explicitly. The dotted command is intentional; there is no `:diffclose` alias.
In Normal mode, Tab/Shift+Tab cycles focus between Files and the visible text panes, skipping absent panes. Read-only text panes use Up/Down or Ctrl+j/k for shared vertical scrolling, Left/Right or Ctrl+h/l for independent horizontal scrolling, and Home/End for the first/last displayed row; editable After uses normal buffer motions. Wheel input over text panes scrolls the visible panes without moving the editable caret; over Files it navigates visible tree rows without opening them. Escape cancels modes, prompts, or filtering, or returns picker focus to the first visible text pane; **it never closes the diff**. Use **`:diff.close`**, find `diff.close` in the palette, or bind **`config.core.diff.close`** to leave the comparison explicitly. The dotted command is intentional; there is no `:diffclose` alias.
Binary data, missing final newlines, permission/type changes, symlink targets, and submodule pointers have explicit notices. Symlinks are not followed and submodule contents are not traversed. jj conflicts use jj's file-content projection and carry conflict notices; ordinary files remain readable. Identical-content moves are shown as renames; modified moves appear as an added and removed file.
+202 -20
View File
@@ -120,6 +120,14 @@ pub enum Focus {
Before,
After,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum PaneLayout {
Split,
Before,
After,
Empty,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RowKind {
Equal,
@@ -299,8 +307,12 @@ impl DiffState {
self.comparison = Some(comparison);
self.comparison_id = Some(id);
self.phase = Phase::View;
self.focus = Focus::Files;
self.file = 0;
self.focus = if self.files_visible {
Focus::Files
} else {
self.first_text_focus()
};
self.align_file();
self.error = None;
}
@@ -396,7 +408,7 @@ impl DiffState {
{
files.focused = false;
self.files_visible = files.presentation == FilePresentation::Docked;
self.focus = Focus::Before;
self.focus = self.first_text_focus();
}
return;
}
@@ -529,7 +541,7 @@ impl DiffState {
if let Some(path) = files.accept()
&& self.select_file(&path)
{
self.focus = Focus::Before;
self.focus = self.first_text_focus();
let files = self.files.as_mut().expect("comparison picker");
files.focused = false;
self.files_visible = files.presentation == FilePresentation::Docked;
@@ -543,22 +555,8 @@ impl DiffState {
}
}
match input {
Input::Char('\t') => {
self.focus = match self.focus {
Focus::Files => Focus::Before,
Focus::Before => Focus::After,
Focus::After if self.files_visible => Focus::Files,
Focus::After => Focus::Before,
}
}
Input::BackTab => {
self.focus = match self.focus {
Focus::Files => Focus::After,
Focus::After => Focus::Before,
Focus::Before if self.files_visible => Focus::Files,
Focus::Before => Focus::After,
}
}
Input::Char('\t') => self.cycle_focus(true),
Input::BackTab => self.cycle_focus(false),
Input::Up | Input::Ctrl('k') => self.navigate(false),
Input::Down | Input::Ctrl('j') => self.navigate(true),
Input::Left | Input::Ctrl('h') => match self.focus {
@@ -591,8 +589,76 @@ impl DiffState {
}
}
pub(crate) fn pane_layout(&self) -> PaneLayout {
let Some(file) = self
.comparison
.as_ref()
.and_then(|comparison| comparison.files.get(self.file))
else {
return PaneLayout::Empty;
};
match file.status {
FileStatus::Added => PaneLayout::After,
FileStatus::Removed => PaneLayout::Before,
FileStatus::Renamed
if file.before == file.after && file.before_eof == file.after_eof =>
{
PaneLayout::Empty
}
FileStatus::Modified | FileStatus::Renamed => PaneLayout::Split,
}
}
pub(crate) fn first_text_focus(&self) -> Focus {
if self.pane_layout() == PaneLayout::After {
Focus::After
} else {
Focus::Before
}
}
pub(crate) fn normalize_focus(&mut self) {
self.focus = match (self.focus, self.pane_layout()) {
(Focus::Files, _) if self.files_visible => Focus::Files,
(Focus::Before | Focus::After, PaneLayout::Split) => self.focus,
_ => self.first_text_focus(),
};
if let Some(files) = &mut self.files {
files.focused = self.files_visible && self.focus == Focus::Files;
}
}
fn cycle_focus(&mut self, forward: bool) {
let first = self.first_text_focus();
let last = if self.pane_layout() == PaneLayout::Split {
Focus::After
} else {
first
};
self.focus = match self.focus {
Focus::Files => {
if forward {
first
} else {
last
}
}
focus if forward && focus != last => last,
focus if !forward && focus != first => first,
_ if self.files_visible => Focus::Files,
_ => {
if forward {
first
} else {
last
}
}
};
}
pub fn after_editable(&self) -> bool {
self.phase == Phase::View
&& matches!(self.pane_layout(), PaneLayout::Split | PaneLayout::After)
&& self
.after
.as_ref()
@@ -616,6 +682,7 @@ impl DiffState {
return false;
};
self.file = index;
self.normalize_focus();
self.live_buffer = None;
self.live_revision = None;
self.follow_caret = false;
@@ -783,6 +850,7 @@ impl DiffState {
}
fn rebuild_file(&mut self, reset: bool) {
self.normalize_focus();
self.rows.clear();
self.display_rows.clear();
self.excerpt_ranges.clear();
@@ -1491,6 +1559,120 @@ mod tests {
}
}
#[test]
fn status_layout_cycles_only_present_panes_and_normalizes_selection() {
let mut state = live_state(vec!["old".into()], vec!["new".into()], true);
state.comparison = Some(comparison());
state.files = Some(FilePicker::from_paths(
PathBuf::new(),
[
(0, "renamed.txt".into()),
(1, "added.txt".into()),
(2, "removed.bin".into()),
],
));
for (path, layout, first, last) in [
(
"renamed.txt",
PaneLayout::Split,
Focus::Before,
Focus::After,
),
("added.txt", PaneLayout::After, Focus::After, Focus::After),
(
"removed.bin",
PaneLayout::Before,
Focus::Before,
Focus::Before,
),
] {
state.files_visible = true;
state.focus = Focus::Files;
assert!(state.select_file(Path::new(path)));
assert_eq!(state.pane_layout(), layout);
state.handle(Input::Char('\t'));
assert_eq!(state.focus, first);
state.handle(Input::BackTab);
assert_eq!(state.focus, Focus::Files);
state.handle(Input::BackTab);
assert_eq!(state.focus, last);
state.handle(Input::Char('\t'));
assert_eq!(state.focus, Focus::Files);
state.files_visible = false;
state.normalize_focus();
assert_eq!(state.focus, first);
state.handle(Input::BackTab);
assert_eq!(state.focus, last);
state.handle(Input::Char('\t'));
assert_eq!(state.focus, first);
}
state.focus = Focus::Before;
assert!(state.select_file(Path::new("added.txt")));
assert_eq!(state.focus, Focus::After);
assert!(state.select_file(Path::new("removed.bin")));
assert_eq!(state.focus, Focus::Before);
assert!(state.select_file(Path::new("renamed.txt")));
assert_eq!(state.focus, Focus::Before);
}
#[test]
fn rename_layout_and_editability_include_eof_changes() {
let mut state = live_state(vec!["same".into()], vec!["same".into()], true);
state.comparison.as_mut().unwrap().files[0].status = FileStatus::Renamed;
assert_eq!(state.pane_layout(), PaneLayout::Empty);
assert!(!state.after_editable());
state.normalize_focus();
assert_eq!(state.focus, Focus::Before);
state.files_visible = false;
for input in [Input::Char('\t'), Input::BackTab] {
state.handle(input);
assert_eq!(state.focus, Focus::Before);
}
state.comparison.as_mut().unwrap().files[0].after_eof = false;
assert_eq!(state.pane_layout(), PaneLayout::Split);
assert!(state.after_editable());
state.comparison.as_mut().unwrap().files[0].after[0] = "changed".into();
assert!(state.after_editable());
state.comparison.as_mut().unwrap().files[0].status = FileStatus::Removed;
assert!(!state.after_editable());
state.comparison.as_mut().unwrap().files[0].status = FileStatus::Added;
assert!(state.after_editable());
let file = &mut state.comparison.as_mut().unwrap().files[0];
file.before.clear();
file.after.clear();
file.notice = Some("empty file".into());
state.normalize_focus();
assert_eq!(state.pane_layout(), PaneLayout::After);
assert_eq!(state.focus, Focus::After);
assert!(state.after_editable());
assert_eq!(
state.comparison.as_ref().unwrap().files[0]
.notice
.as_deref(),
Some("empty file")
);
}
#[test]
fn added_file_picker_cancel_close_and_mouse_focus_use_after() {
let mut editor = editor();
open(&mut editor);
let id = choose_pair(&mut editor);
let mut changes = comparison();
changes.files = vec![changes.files.remove(1)];
editor.apply_diff_result(id, Ok(Outcome::Comparison(changes)));
editor.handle(Input::Escape);
assert_eq!(editor.diff().unwrap().focus, Focus::After);
text(&mut editor, " e");
editor.handle(Input::Ctrl('w'));
assert_eq!(editor.diff().unwrap().focus, Focus::After);
assert!(!editor.diff().unwrap().files_visible);
editor.scroll_diff(Focus::Before, true);
assert_eq!(editor.diff().unwrap().focus, Focus::After);
editor.handle(Input::BackTab);
assert_eq!(editor.diff().unwrap().focus, Focus::After);
}
#[test]
fn selector_filters_literal_unicode_tokens_and_pins_working_in_each_category() {
let mut editor = editor();
@@ -1623,7 +1805,7 @@ mod tests {
text(&mut editor, "x/y");
editor.handle(Input::Enter);
assert_eq!(editor.diff().unwrap().file, 1);
assert_eq!(editor.diff().unwrap().focus, Focus::Before);
assert_eq!(editor.diff().unwrap().focus, Focus::After);
}
#[test]
+4 -3
View File
@@ -1454,9 +1454,10 @@ impl Editor {
&& diff.phase == Phase::View
{
diff.focus = focus;
diff.normalize_focus();
diff.follow_caret = false;
if let Some(files) = &mut diff.files {
files.focused = diff.files_visible && focus == Focus::Files;
files.focused = diff.files_visible && diff.focus == Focus::Files;
}
diff.navigate(down);
}
@@ -1503,7 +1504,7 @@ impl Editor {
if files_focused && input == Input::Ctrl('w') {
let diff = self.diff.as_mut().expect("active diff");
diff.files_visible = false;
diff.focus = Focus::Before;
diff.focus = diff.first_text_focus();
if let Some(files) = &mut diff.files {
files.focused = false;
}
@@ -3229,7 +3230,7 @@ impl Editor {
}
}
if diff.focus == Focus::Files {
diff.focus = Focus::Before;
diff.focus = diff.first_text_focus();
}
}
match self.file_picker.as_mut() {
+152 -17
View File
@@ -32,7 +32,7 @@ use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::{
diff::{ChangedFile, DiffState, DisplayRow, FileStatus, Focus, Phase, RowKind},
diff::{ChangedFile, DiffState, DisplayRow, FileStatus, Focus, PaneLayout, Phase, RowKind},
editor::{
Completion, Cursor, Editor, Hover, Input, Mode, Palette, Selection, StartupDiagnostic,
},
@@ -1279,12 +1279,20 @@ fn draw_diff(
} else {
(workspace, None)
};
let [before_area, after_area] =
Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(panes);
let pane_layout = diff.pane_layout();
let [before_area, after_area] = match pane_layout {
PaneLayout::Split => {
Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(panes)
}
PaneLayout::Before | PaneLayout::Empty => [panes, Rect::default()],
PaneLayout::After => [Rect::default(), panes],
};
let mut document_caret = None;
let mut top = diff.top;
let mut after_left = diff.left_after;
if let Some(editor) = editor.filter(|editor| editor.diff_editing() && diff.follow_caret) {
if let Some(editor) = editor.filter(|editor| {
editor.diff_editing() && diff.follow_caret && after_area != Rect::default()
}) {
let height = usize::from(after_area.height.saturating_sub(2));
if let Some(row) = diff.after_display_row(editor.cursor().row) {
if row < top {
@@ -1317,6 +1325,10 @@ fn draw_diff(
if let Some(file) = selected
&& let Some(syntax) = &panels.diff_syntax
{
if pane_layout == PaneLayout::Empty {
diff_panel(frame, before_area, "", diff.focus == Focus::Before);
} else {
if before_area != Rect::default() {
draw_diff_pane(
frame,
diff,
@@ -1326,6 +1338,8 @@ fn draw_diff(
&syntax.before,
(top, diff.left_before),
);
}
if after_area != Rect::default() {
let text_area = draw_diff_pane(
frame,
diff,
@@ -1336,9 +1350,12 @@ fn draw_diff(
(top, after_left),
);
if let Some(editor) = editor.filter(|editor| editor.diff_editable()) {
document_caret = paint_diff_editor(frame, editor, diff, text_area, (top, after_left))
document_caret =
paint_diff_editor(frame, editor, diff, text_area, (top, after_left))
.map(|position| (text_area, position));
}
}
}
} else {
frame.render_widget(
Paragraph::new(if diff.loading {
@@ -2172,16 +2189,18 @@ fn draw_file_list(
));
spans.push(Span::raw(" "));
}
spans.push(Span::raw(entry.label));
if entry.is_dir && picker.view == FileView::Tree {
spans.push(Span::raw("/"));
}
if let Some(file) = changed
&& file.status == FileStatus::Renamed
&& let Some(old_path) = &file.old_path
{
spans.push(Span::raw(" ← "));
spans.push(Span::raw(display_text(&old_path.to_string_lossy())));
spans.push(Span::raw(" -> "));
spans.push(Span::raw(display_text(&file.path.to_string_lossy())));
} else {
spans.push(Span::raw(entry.label));
if entry.is_dir && picker.view == FileView::Tree {
spans.push(Span::raw("/"));
}
}
Line::from(spans).style(style)
})
@@ -2675,6 +2694,118 @@ mod tests {
notice: None,
}
}
#[test]
fn diff_status_layouts_hide_absent_panes_and_leave_unchanged_renames_blank() {
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(160, 12)).unwrap();
let mut panels = WheelPanels::default();
for status in [FileStatus::Added, FileStatus::Removed] {
let added = status == FileStatus::Added;
let mut file = deletion_file();
file.status = status;
file.before = if added {
vec![]
} else {
vec!["removed content".into()]
};
file.after = if added {
vec!["added content".into()]
} else {
vec![]
};
file.hunks = vec![crate::diff::Hunk {
before_start: 0,
before_len: usize::from(!added),
after_start: 0,
after_len: usize::from(added),
}];
let mut diff = diff_comparison(vec![file]);
diff.files_visible = false;
diff.focus = if added { Focus::After } else { Focus::Before };
diff.left_after = 2;
render_diff(&mut terminal, &diff, &mut panels);
let geometry = panels.diff.as_ref().unwrap();
let present = geometry.regions[if added { 2 } else { 1 }].0;
assert_eq!(
geometry.regions[if added { 1 } else { 2 }].0,
Rect::default()
);
assert_eq!(present.x, 0);
assert_eq!(present.width, 160);
assert_eq!(geometry.viewport.unwrap().3, 2);
assert_eq!(geometry.regions[if added { 2 } else { 1 }].1, diff.focus);
assert_eq!(
terminal.backend().buffer()[(present.x, present.y)].fg,
Color::Cyan
);
let text = screen_text(&terminal);
assert!(text.contains(if added {
"ded content"
} else {
"removed content"
}));
assert!(text.contains(if added { "After · A" } else { "Before · D" }));
assert!(!text.contains(if added { "Before ·" } else { "After ·" }));
assert!(!text.contains('╱'));
}
let mut empty_added = deletion_file();
empty_added.status = FileStatus::Added;
empty_added.before.clear();
empty_added.after.clear();
empty_added.hunks.clear();
empty_added.notice = Some("metadata-only addition".into());
let mut diff = diff_comparison(vec![empty_added]);
diff.files_visible = false;
diff.focus = Focus::After;
render_diff(&mut terminal, &diff, &mut panels);
let geometry = panels.diff.as_ref().unwrap();
assert_eq!(geometry.regions[1].0, Rect::default());
assert_eq!(geometry.regions[2].0.width, 160);
assert!(screen_text(&terminal).contains("metadata-only addition"));
let mut renamed = deletion_file();
renamed.status = FileStatus::Renamed;
renamed.old_path = Some("old/path.txt".into());
renamed.path = "new/path.txt".into();
renamed.before = vec!["unchanged rename content".into()];
renamed.after = renamed.before.clone();
renamed.hunks.clear();
let mut diff = diff_comparison(vec![renamed]);
render_diff(&mut terminal, &diff, &mut panels);
assert!(screen_text(&terminal).contains("old/path.txt -> new/path.txt"));
assert_eq!(
diff.files.as_mut().unwrap().accept(),
Some("new/path.txt".into())
);
diff.files_visible = false;
render_diff(&mut terminal, &diff, &mut panels);
let geometry = panels.diff.as_ref().unwrap();
let blank = geometry.regions[1].0;
assert_eq!(blank.width, 160);
assert_eq!(geometry.regions[2].0, Rect::default());
let buffer = terminal.backend().buffer();
assert_eq!(buffer[(blank.x, blank.y)].symbol(), "┌");
for y in blank.y + 1..blank.bottom() - 1 {
for x in blank.x + 1..blank.right() - 1 {
assert_eq!(buffer[(x, y)].symbol(), " ");
assert_eq!(buffer[(x, y)].bg, Color::Reset);
}
}
let text = screen_text(&terminal);
assert!(!text.contains("Before ·"));
assert!(!text.contains("After ·"));
assert!(!text.contains("unchanged rename content"));
assert!(!text.contains('╱'));
let mut modified = diff_comparison(vec![deletion_file()]);
modified.files_visible = false;
render_diff(&mut terminal, &modified, &mut panels);
let regions = panels.diff.as_ref().unwrap().regions;
assert_eq!(regions[1].0.width, 80);
assert_eq!(regions[2].0.width, 80);
let text = screen_text(&terminal);
assert!(text.contains("Before · M"));
assert!(text.contains("After · M"));
}
fn render_diff(
terminal: &mut Terminal<ratatui::backend::TestBackend>,
@@ -2855,7 +2986,7 @@ mod tests {
after_eof: false,
path: "added.txt".into(),
old_path: None,
status: FileStatus::Added,
status: FileStatus::Modified,
before: vec![],
after: vec!["first addition".into(), String::new()],
hunks: vec![crate::diff::Hunk {
@@ -3189,8 +3320,11 @@ mod tests {
} else {
file.path.as_path()
};
let expected =
format!("{} {}", diff_status(file.status), path.to_string_lossy());
let expected = if file.status == FileStatus::Renamed {
format!("R old�name.txt -> {}", file.path.to_string_lossy())
} else {
format!("{} {}", diff_status(file.status), path.to_string_lossy())
};
let (x, y) = (0..16)
.find_map(|y| {
let row = (0..100)
@@ -3203,7 +3337,6 @@ mod tests {
.unwrap_or_else(|| panic!("missing {expected}"));
assert_eq!(buffer[(x, y)].fg, color);
}
assert!(screen_text(&terminal).contains("r.txt ← old�name.txt"));
assert_eq!(picker.accept(), Some(files[index].path.clone()));
assert!(picker.query.is_empty());
}
@@ -3326,9 +3459,11 @@ mod tests {
assert!(rect.right() <= width && rect.bottom() <= height);
}
let [navigator, before, after] = geometry.regions.map(|(rect, _)| rect);
if before.width > 0 && after.width > 0 {
assert!(before.right() <= after.x);
}
if navigator.width > 0 {
assert!(after.right() <= navigator.x);
assert!(before.right().max(after.right()) <= navigator.x);
}
}
}
@@ -3596,7 +3731,7 @@ mod tests {
git(&["add", "."]);
git(&["commit", "-qm", "base"]);
for index in 0..8 {
fs::remove_file(directory.0.join(format!("file-{index}.txt"))).unwrap();
directory.file(&format!("file-{index}.txt"), "changed\nsecond\nthird\n");
}
let mut session = EditorSession::open(Some(path), vec![], None).unwrap();
let original = session.editor().lines().to_vec();
@@ -3616,8 +3751,8 @@ mod tests {
};
session.execute("diff");
ready(&mut session, Phase::Before);
session.handle(Input::Down);
session.handle(Input::Enter);
session.handle(Input::Down);
session.handle(Input::Enter);
ready(&mut session, Phase::View);
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 18)).unwrap();