feat: add expandable diff excerpts with configurable context

This commit is contained in:
2026-10-11 01:45:13 +02:00
parent 5e0700bb8c
commit 704dce2539
8 changed files with 590 additions and 21 deletions
+95 -12
View File
@@ -32,7 +32,7 @@ use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::{
diff::{ChangedFile, DiffState, FileStatus, Focus, Phase, RowKind},
diff::{ChangedFile, DiffState, DisplayRow, FileStatus, Focus, Phase, RowKind},
editor::{
Completion, Cursor, Editor, Hover, Input, Mode, Palette, Selection, StartupDiagnostic,
},
@@ -1206,7 +1206,7 @@ fn draw_diff(
}
frame.render_widget(
Paragraph::new(
"^E explorer · Tab focus · i filter · Enter open/toggle · ↑/↓ scroll/tree · ←/→ horizontal · Esc close",
"^E explorer · Tab focus · i filter · Enter open/toggle · ↑/↓ scroll/tree · [/] expand above/below · ←/→ horizontal · Esc close",
)
.style(Style::default().fg(Color::DarkGray)),
help,
@@ -1253,14 +1253,16 @@ fn draw_diff_pane(
);
let focus = if before { Focus::Before } else { Focus::After };
let content = diff_panel(frame, area, &title, diff.focus == focus);
if diff.rows.is_empty() {
if diff.display_rows.is_empty() {
frame.render_widget(
Paragraph::new(
file.notice
.as_deref()
.map(display_text)
.unwrap_or_else(|| "Empty file".into()),
),
Paragraph::new(file.notice.as_deref().map(display_text).unwrap_or_else(|| {
if diff.rows.is_empty() {
"Empty file"
} else {
"No text changes"
}
.into()
})),
content,
);
return;
@@ -1278,8 +1280,8 @@ fn draw_diff_pane(
0
};
let width = usize::from(content.width).saturating_sub(gutter);
for (aligned_row, row) in diff
.rows
for (display_row, row) in diff
.display_rows
.iter()
.enumerate()
.skip(diff.top)
@@ -1287,10 +1289,25 @@ fn draw_diff_pane(
{
let row_area = Rect::new(
content.x,
content.y + (aligned_row - diff.top) as u16,
content.y + (display_row - diff.top) as u16,
content.width,
1,
);
let aligned_row = match row {
DisplayRow::Line(index) => *index,
DisplayRow::Fold { start, end } => {
frame.render_widget(
Paragraph::new(format!(
"… {} unchanged lines hidden · [/] expand context",
end - start
))
.style(Style::default().fg(Color::DarkGray)),
row_area,
);
continue;
}
};
let row = &diff.rows[aligned_row];
let index = if before { row.before } else { row.after };
let Some(index) = index else {
let style = Style::default().fg(Color::Rgb(72, 72, 72)).bg(Color::Reset);
@@ -2546,6 +2563,72 @@ mod tests {
);
}
#[test]
fn diff_excerpts_render_original_lines_and_expand_without_losing_changes() {
let before: Vec<_> = (0..80)
.map(|index| format!("let row_{index:03} = {index};"))
.collect();
let mut after = before.clone();
after[30] = "let first_change = 900;".into();
after[60] = "let second_change = 901;".into();
let mut diff = diff_comparison(vec![ChangedFile {
path: "excerpts.rs".into(),
old_path: None,
status: FileStatus::Modified,
before,
after,
hunks: [30, 60]
.map(|index| crate::diff::Hunk {
before_start: index,
before_len: 1,
after_start: index,
after_len: 1,
})
.into(),
notice: None,
}]);
diff.set_context_options(1, 3);
diff.handle(Input::Ctrl('e'));
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(160, 24)).unwrap();
let mut panels = WheelPanels::default();
render_diff(&mut terminal, &diff, &mut panels);
let text = screen_text(&terminal);
for visible in [
"30 let row_029",
"31- let row_030",
"31+ let first_change",
"61+ let second_change",
] {
assert!(text.contains(visible), "missing {visible}: {text}");
}
for hidden in ["row_028", "row_032", "row_058", "row_062"] {
assert!(!text.contains(hidden), "unexpected {hidden}: {text}");
}
let before_area = panels.diff.as_ref().unwrap().regions[1].0;
let after_area = panels.diff.as_ref().unwrap().regions[2].0;
let buffer = terminal.backend().buffer();
assert_eq!(
buffer[(before_area.x + 5, before_area.y + 3)].fg,
Color::Magenta
);
assert_eq!(
buffer[(before_area.x + 5, before_area.y + 3)].bg,
Color::Rgb(64, 24, 24)
);
assert_eq!(
buffer[(after_area.x + 5, after_area.y + 3)].bg,
Color::Rgb(24, 48, 32)
);
diff.handle(Input::Char('['));
diff.handle(Input::Home);
render_diff(&mut terminal, &diff, &mut panels);
let text = screen_text(&terminal);
assert!(text.contains("row_026"));
assert!(!text.contains("row_025"));
assert!(text.contains("first_change"));
assert!(text.contains("second_change"));
}
#[test]
fn diff_view_aligns_deletion_holes_numbers_colors_and_shared_vertical_scroll() {
let mut diff = diff_comparison(vec![deletion_file()]);