feat: add expandable diff excerpts with configurable context
This commit is contained in:
+95
-12
@@ -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()]);
|
||||
|
||||
Reference in New Issue
Block a user