fix: target caret excerpts and retain diff viewport offsets

This commit is contained in:
2026-10-11 10:50:54 +02:00
parent 86d22d4fa2
commit 976cfc9e62
5 changed files with 237 additions and 4 deletions
+125 -1
View File
@@ -172,6 +172,18 @@ impl BufferViewports {
);
}
fn draw_session(&mut self, frame: &mut Frame, session: &mut EditorSession) {
self.draw(frame, session.editor());
if let Some((comparison_id, file, top, left_after)) = self
.panels
.diff
.as_ref()
.and_then(|geometry| geometry.viewport)
{
session.set_diff_viewport(comparison_id, file, top, left_after);
}
}
fn scroll(&mut self, session: &mut EditorSession, mouse: MouseEvent) -> bool {
if !matches!(
mouse.kind,
@@ -274,7 +286,7 @@ pub fn run(session: &mut EditorSession) -> io::Result<()> {
redraw |= session.poll();
if redraw {
draw_frame(&mut terminal, &mut output, |frame| {
viewports.draw(frame, session.editor())
viewports.draw_session(frame, session)
})?;
redraw = false;
}
@@ -708,6 +720,7 @@ fn draw(
struct DiffGeometry {
regions: [(Rect, Focus); 3],
viewport: Option<(u64, usize, usize, usize)>,
}
fn diff_panel(frame: &mut Frame, area: Rect, title: &str, focused: bool) -> Rect {
@@ -1195,6 +1208,7 @@ fn draw_diff(
(Rect::default(), Focus::Before),
(Rect::default(), Focus::After),
],
viewport: None,
});
if let Some(editor) = editor {
draw_diff_popups(frame, editor, area, None, panels);
@@ -1348,6 +1362,9 @@ fn draw_diff(
(before_area, Focus::Before),
(after_area, Focus::After),
],
viewport: diff
.comparison_id
.map(|id| (id, diff.file, top, after_left)),
});
if let Some(picker) = &diff.files
&& diff.files_visible
@@ -3445,6 +3462,113 @@ mod tests {
assert!(editor.dirty());
}
#[test]
fn session_diff_draw_retains_rendered_offsets_until_caret_crosses_edges() {
let directory = Directory::new();
let original = (0..80)
.map(|row| format!("old row {row:03} {}", "x".repeat(100)))
.collect::<Vec<_>>()
.join("\n");
let path = directory.file("source.txt", &original);
let git = |args: &[&str]| {
let output = std::process::Command::new("git")
.args(args)
.current_dir(&directory.0)
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_AUTHOR_NAME", "Diff fixture")
.env("GIT_AUTHOR_EMAIL", "fixture@example.invalid")
.env("GIT_COMMITTER_NAME", "Diff fixture")
.env("GIT_COMMITTER_EMAIL", "fixture@example.invalid")
.env("GIT_AUTHOR_DATE", "2000-01-01T00:00:00Z")
.env("GIT_COMMITTER_DATE", "2000-01-01T00:00:00Z")
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
};
git(&["init", "-q"]);
git(&["add", "."]);
git(&["commit", "-qm", "base"]);
directory.file("source.txt", &original.replace("old row", "new row"));
let mut session = EditorSession::open(Some(path), vec![], None).unwrap();
let ready = |session: &mut EditorSession, phase| {
let deadline = std::time::Instant::now() + Duration::from_secs(10);
loop {
session.poll();
let diff = session.editor().diff().unwrap();
assert!(diff.error.is_none(), "{:?}", diff.error);
if diff.phase == phase && !diff.loading {
break;
}
assert!(std::time::Instant::now() < deadline, "{diff:?}");
std::thread::sleep(Duration::from_millis(1));
}
};
session.execute("diff");
ready(&mut session, Phase::Before);
session.handle(Input::Down);
session.handle(Input::Enter);
session.handle(Input::Enter);
ready(&mut session, Phase::View);
session.handle(Input::Char('\t'));
session.handle(Input::Char('\t'));
assert!(session.editor().diff_editing());
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 18)).unwrap();
let mut views = BufferViewports::default();
let mut render = |session: &mut EditorSession| {
terminal
.draw(|frame| views.draw_session(frame, session))
.unwrap();
terminal.get_cursor_position().unwrap()
};
render(&mut session);
for _ in 0..40 {
session.handle(Input::Down);
render(&mut session);
}
let top = session.editor().diff().unwrap().top;
assert!(top > 0);
let bottom_caret = render(&mut session);
session.handle(Input::Up);
let reversed_caret = render(&mut session);
assert_eq!(session.editor().diff().unwrap().top, top);
assert_eq!(reversed_caret.y + 1, bottom_caret.y);
while session
.editor()
.diff()
.unwrap()
.after_display_row(session.editor().cursor().row)
.unwrap()
> top
{
session.handle(Input::Up);
render(&mut session);
assert_eq!(session.editor().diff().unwrap().top, top);
}
session.handle(Input::Up);
render(&mut session);
assert!(session.editor().diff().unwrap().top < top);
for _ in 0..80 {
session.handle(Input::Right);
render(&mut session);
}
let left = session.editor().diff().unwrap().left_after;
assert!(left > 0);
let right_caret = render(&mut session);
session.handle(Input::Left);
let reversed_caret = render(&mut session);
assert_eq!(session.editor().diff().unwrap().left_after, left);
assert_eq!(reversed_caret.x + 1, right_caret.x);
while session.editor().cursor().column > left {
session.handle(Input::Left);
render(&mut session);
assert_eq!(session.editor().diff().unwrap().left_after, left);
}
session.handle(Input::Left);
render(&mut session);
assert!(session.editor().diff().unwrap().left_after < left);
}
#[test]
fn diff_wheel_routes_right_tree_and_both_panes_without_touching_document() {
let directory = Directory::new();