fix: target caret excerpts and retain diff viewport offsets
This commit is contained in:
+15
-1
@@ -939,11 +939,25 @@ impl DiffState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn expand_after_excerpt(&mut self, below: bool, source_row: usize) {
|
||||
let Some(display_row) = self.after_display_row(source_row) else {
|
||||
return;
|
||||
};
|
||||
self.expand_excerpt_at(below, self.display_rows[display_row]);
|
||||
}
|
||||
|
||||
fn expand_excerpt(&mut self, below: bool) {
|
||||
let Some(target) = self.display_rows.get(self.top).copied() else {
|
||||
return;
|
||||
};
|
||||
self.expand_excerpt_at(below, target);
|
||||
}
|
||||
|
||||
fn expand_excerpt_at(&mut self, below: bool, target: DisplayRow) {
|
||||
let Some(top) = self.display_rows.get(self.top).copied() else {
|
||||
return;
|
||||
};
|
||||
let index = match top {
|
||||
let index = match target {
|
||||
DisplayRow::Line(row) => self
|
||||
.excerpt_ranges
|
||||
.iter()
|
||||
|
||||
@@ -1462,6 +1462,22 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_diff_viewport(
|
||||
&mut self,
|
||||
comparison_id: u64,
|
||||
file: usize,
|
||||
top: usize,
|
||||
left_after: usize,
|
||||
) {
|
||||
if let Some(diff) = &mut self.diff
|
||||
&& diff.comparison_id == Some(comparison_id)
|
||||
&& diff.file == file
|
||||
{
|
||||
diff.top = top;
|
||||
diff.left_after = left_after;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_diff(&mut self, input: Input) -> Option<Invocation> {
|
||||
if input == Input::Escape
|
||||
&& (self.count.is_some() || self.g_pending || self.line_pending.is_some())
|
||||
@@ -1565,6 +1581,14 @@ impl Editor {
|
||||
if live && !matches!(input, Input::Char('\t' | '[' | ']') | Input::BackTab) {
|
||||
return self.handle_input(input);
|
||||
}
|
||||
if live && matches!(input, Input::Char('[' | ']')) {
|
||||
let row = self.cursor().row;
|
||||
self.diff
|
||||
.as_mut()
|
||||
.expect("active diff")
|
||||
.expand_after_excerpt(input == Input::Char(']'), row);
|
||||
return None;
|
||||
}
|
||||
if !files_focused
|
||||
&& self
|
||||
.diff
|
||||
@@ -7050,6 +7074,66 @@ mod tests {
|
||||
buffers
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editable_diff_brackets_expand_caret_excerpt_without_moving_source_anchor() {
|
||||
for selected in [30, 50] {
|
||||
let directory = Directory::new();
|
||||
let path = directory.path("file.txt");
|
||||
let before: Vec<String> = (0..80).map(|row| format!("line {row}")).collect();
|
||||
fs::write(&path, before.join("\n")).unwrap();
|
||||
let mut editor = Editor::open(Some(path), vec![]).unwrap();
|
||||
let mut after = before;
|
||||
for row in [10, 30, 50] {
|
||||
after[row] = format!("changed {row}");
|
||||
}
|
||||
editor
|
||||
.apply(vec![Effect::Replace(after.join("\n"))])
|
||||
.unwrap();
|
||||
editor.set_diff_context_options(1, 4);
|
||||
working_diff(&mut editor, &directory.0, &["file.txt"]);
|
||||
editor.handle(Input::BackTab);
|
||||
assert!(editor.diff_editing());
|
||||
editor.active.targets = vec![Target::caret(Cursor {
|
||||
row: selected,
|
||||
column: 2,
|
||||
})];
|
||||
let top = editor.diff().unwrap().after_display_row(9).unwrap();
|
||||
editor.diff.as_mut().unwrap().top = top;
|
||||
let cursor = editor.cursor();
|
||||
let visible = |editor: &Editor| editor.diff().unwrap().after_rows().collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
visible(&editor),
|
||||
(9..12).chain(29..32).chain(49..52).collect::<Vec<_>>()
|
||||
);
|
||||
editor.handle(Input::Char('['));
|
||||
editor.handle(Input::Char(']'));
|
||||
let expected = if selected == 30 {
|
||||
(9..12).chain(25..36).chain(49..52).collect::<Vec<_>>()
|
||||
} else {
|
||||
(9..12).chain(29..32).chain(45..56).collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(visible(&editor), expected);
|
||||
assert_eq!(editor.cursor(), cursor);
|
||||
assert_eq!(
|
||||
editor.diff().unwrap().after_display_row(9),
|
||||
Some(editor.diff().unwrap().top)
|
||||
);
|
||||
if selected == 30 {
|
||||
// The caret keeps targeting the second excerpt after it merges
|
||||
// with the third, rather than expanding the first excerpt.
|
||||
for _ in 0..5 {
|
||||
editor.handle(Input::Char(']'));
|
||||
}
|
||||
assert_eq!(visible(&editor), (9..12).chain(25..56).collect::<Vec<_>>());
|
||||
assert_eq!(editor.cursor(), cursor);
|
||||
assert_eq!(
|
||||
editor.diff().unwrap().after_display_row(9),
|
||||
Some(editor.diff().unwrap().top)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn working_diff_reuses_dirty_buffers_and_normal_edit_undo_save_paths() {
|
||||
let directory = Directory::new();
|
||||
|
||||
@@ -258,6 +258,17 @@ impl EditorSession {
|
||||
&self.editor
|
||||
}
|
||||
|
||||
pub(crate) fn set_diff_viewport(
|
||||
&mut self,
|
||||
comparison_id: u64,
|
||||
file: usize,
|
||||
top: usize,
|
||||
left_after: usize,
|
||||
) {
|
||||
self.editor
|
||||
.set_diff_viewport(comparison_id, file, top, left_after);
|
||||
}
|
||||
|
||||
pub(crate) fn scroll_diff(&mut self, focus: Focus, down: bool) {
|
||||
self.editor.scroll_diff(focus, down);
|
||||
}
|
||||
|
||||
+125
-1
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user