feat: enable native readonly diff interaction and synchronized pane cursors

This commit is contained in:
2026-10-11 11:31:59 +02:00
parent f43a43fd53
commit 5186714ef5
8 changed files with 1329 additions and 272 deletions
+70 -18
View File
@@ -690,21 +690,69 @@ impl DiffState {
true
}
pub fn after_display_row(&self, source_row: usize) -> Option<usize> {
pub(crate) fn source_display_row(&self, focus: Focus, source_row: usize) -> Option<usize> {
self.display_rows.iter().position(|display| {
matches!(*display, DisplayRow::Line(index) if self.rows[index].after == Some(source_row))
matches!(*display, DisplayRow::Line(index) if self.source_row(focus, index) == Some(source_row))
})
}
pub(crate) fn after_rows(&self) -> impl DoubleEndedIterator<Item = usize> + '_ {
self.excerpt_ranges
.iter()
.flat_map(|&(start, end)| self.rows[start..end].iter().filter_map(|row| row.after))
pub fn after_display_row(&self, source_row: usize) -> Option<usize> {
self.source_display_row(Focus::After, source_row)
}
pub(crate) fn clamp_after_row(&self, row: usize) -> Option<usize> {
fn source_row(&self, focus: Focus, index: usize) -> Option<usize> {
match focus {
Focus::Before => self.rows[index].before,
Focus::After => self.rows[index].after,
Focus::Files => None,
}
}
pub(crate) fn source_rows(&self, focus: Focus) -> impl DoubleEndedIterator<Item = usize> + '_ {
self.excerpt_ranges.iter().flat_map(move |&(start, end)| {
(start..end).filter_map(move |index| self.source_row(focus, index))
})
}
pub(crate) fn snapshot_lines(&self, focus: Focus) -> Vec<String> {
let file = &self.comparison.as_ref().expect("snapshot comparison").files[self.file];
let (source, eof, live) = match focus {
Focus::Before => (&file.before, file.before_eof, false),
Focus::After => (
&file.after,
file.after_eof,
self.live_files.contains(&self.file),
),
Focus::Files => unreachable!("snapshot belongs to a text pane"),
};
let mut lines = source.clone();
if !live && (eof || lines.is_empty()) {
lines.push(String::new());
}
lines
}
pub(crate) fn corresponding_row(&self, from: Focus, to: Focus, row: usize) -> Option<usize> {
let aligned = self.rows.iter().position(|aligned| match from {
Focus::Before => aligned.before == Some(row),
Focus::After => aligned.after == Some(row),
Focus::Files => false,
})?;
self.display_rows
.iter()
.filter_map(|display| match *display {
DisplayRow::Line(index) => self
.source_row(to, index)
.map(|row| (index.abs_diff(aligned), row)),
DisplayRow::Fold { .. } => None,
})
.min_by_key(|&(distance, row)| (distance, row))
.map(|(_, row)| row)
}
pub(crate) fn clamp_source_row(&self, focus: Focus, row: usize) -> Option<usize> {
let mut previous = None;
for visible in self.after_rows() {
for visible in self.source_rows(focus) {
if visible >= row {
return Some(match previous {
Some(previous) if row - previous < visible - row => previous,
@@ -1007,8 +1055,8 @@ 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 {
pub(crate) fn expand_source_excerpt(&mut self, focus: Focus, below: bool, source_row: usize) {
let Some(display_row) = self.source_display_row(focus, source_row) else {
return;
};
self.expand_excerpt_at(below, self.display_rows[display_row]);
@@ -1983,17 +2031,21 @@ mod tests {
assert_eq!(editor.diff().unwrap().focus, Focus::Before);
editor.handle(Input::Down);
editor.handle(Input::Right);
assert_eq!(editor.diff().unwrap().top, 1);
assert_eq!(editor.diff().unwrap().left_before, 1);
assert_eq!(editor.cursor().row, 1);
assert_eq!(editor.cursor().column, 1);
editor.scroll_diff(Focus::Before, true);
editor.set_diff_viewport(id, 0, 1, 1, 0);
editor.handle(Input::Char('\t'));
assert_eq!(editor.diff().unwrap().focus, Focus::After);
editor.handle(Input::Ctrl('j'));
editor.handle(Input::Ctrl('l'));
assert_eq!(editor.diff().unwrap().top, 2);
editor.handle(Input::Down);
editor.handle(Input::Right);
assert_eq!(editor.cursor().row, 2);
assert_eq!(editor.diff().unwrap().top, 1);
editor.set_diff_viewport(id, 0, 2, 1, 1);
assert_eq!(editor.diff().unwrap().left_after, 1);
assert_eq!(editor.diff().unwrap().left_before, 1);
editor.handle(Input::End);
assert_eq!(editor.diff().unwrap().top, 5);
assert_eq!(editor.diff().unwrap().top, 2);
editor.handle(Input::BackTab);
assert_eq!(editor.diff().unwrap().focus, Focus::Before);
editor.handle(Input::BackTab);
@@ -2136,12 +2188,12 @@ mod tests {
] {
editor.handle(input);
}
assert_eq!(editor.context().text, original);
assert_eq!(editor.carets(), carets);
editor.handle(Input::Escape);
editor.handle(Input::Escape);
assert!(editor.diff().is_some());
editor.execute("diff.close");
assert_eq!(editor.context().text, original);
assert_eq!(editor.carets(), carets);
editor.handle(Input::Char('u'));
assert_eq!(editor.context().text, "original\\nsecond");
}