feat: show single-pane diffs for added deleted and renamed files
This commit is contained in:
+173
-38
@@ -32,7 +32,7 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
diff::{ChangedFile, DiffState, DisplayRow, FileStatus, Focus, Phase, RowKind},
|
||||
diff::{ChangedFile, DiffState, DisplayRow, FileStatus, Focus, PaneLayout, Phase, RowKind},
|
||||
editor::{
|
||||
Completion, Cursor, Editor, Hover, Input, Mode, Palette, Selection, StartupDiagnostic,
|
||||
},
|
||||
@@ -1279,12 +1279,20 @@ fn draw_diff(
|
||||
} else {
|
||||
(workspace, None)
|
||||
};
|
||||
let [before_area, after_area] =
|
||||
Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(panes);
|
||||
let pane_layout = diff.pane_layout();
|
||||
let [before_area, after_area] = match pane_layout {
|
||||
PaneLayout::Split => {
|
||||
Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(panes)
|
||||
}
|
||||
PaneLayout::Before | PaneLayout::Empty => [panes, Rect::default()],
|
||||
PaneLayout::After => [Rect::default(), panes],
|
||||
};
|
||||
let mut document_caret = None;
|
||||
let mut top = diff.top;
|
||||
let mut after_left = diff.left_after;
|
||||
if let Some(editor) = editor.filter(|editor| editor.diff_editing() && diff.follow_caret) {
|
||||
if let Some(editor) = editor.filter(|editor| {
|
||||
editor.diff_editing() && diff.follow_caret && after_area != Rect::default()
|
||||
}) {
|
||||
let height = usize::from(after_area.height.saturating_sub(2));
|
||||
if let Some(row) = diff.after_display_row(editor.cursor().row) {
|
||||
if row < top {
|
||||
@@ -1317,27 +1325,36 @@ fn draw_diff(
|
||||
if let Some(file) = selected
|
||||
&& let Some(syntax) = &panels.diff_syntax
|
||||
{
|
||||
draw_diff_pane(
|
||||
frame,
|
||||
diff,
|
||||
file,
|
||||
before_area,
|
||||
true,
|
||||
&syntax.before,
|
||||
(top, diff.left_before),
|
||||
);
|
||||
let text_area = draw_diff_pane(
|
||||
frame,
|
||||
diff,
|
||||
file,
|
||||
after_area,
|
||||
false,
|
||||
&syntax.after,
|
||||
(top, after_left),
|
||||
);
|
||||
if let Some(editor) = editor.filter(|editor| editor.diff_editable()) {
|
||||
document_caret = paint_diff_editor(frame, editor, diff, text_area, (top, after_left))
|
||||
.map(|position| (text_area, position));
|
||||
if pane_layout == PaneLayout::Empty {
|
||||
diff_panel(frame, before_area, "", diff.focus == Focus::Before);
|
||||
} else {
|
||||
if before_area != Rect::default() {
|
||||
draw_diff_pane(
|
||||
frame,
|
||||
diff,
|
||||
file,
|
||||
before_area,
|
||||
true,
|
||||
&syntax.before,
|
||||
(top, diff.left_before),
|
||||
);
|
||||
}
|
||||
if after_area != Rect::default() {
|
||||
let text_area = draw_diff_pane(
|
||||
frame,
|
||||
diff,
|
||||
file,
|
||||
after_area,
|
||||
false,
|
||||
&syntax.after,
|
||||
(top, after_left),
|
||||
);
|
||||
if let Some(editor) = editor.filter(|editor| editor.diff_editable()) {
|
||||
document_caret =
|
||||
paint_diff_editor(frame, editor, diff, text_area, (top, after_left))
|
||||
.map(|position| (text_area, position));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
frame.render_widget(
|
||||
@@ -2172,16 +2189,18 @@ fn draw_file_list(
|
||||
));
|
||||
spans.push(Span::raw(" "));
|
||||
}
|
||||
spans.push(Span::raw(entry.label));
|
||||
if entry.is_dir && picker.view == FileView::Tree {
|
||||
spans.push(Span::raw("/"));
|
||||
}
|
||||
if let Some(file) = changed
|
||||
&& file.status == FileStatus::Renamed
|
||||
&& let Some(old_path) = &file.old_path
|
||||
{
|
||||
spans.push(Span::raw(" ← "));
|
||||
spans.push(Span::raw(display_text(&old_path.to_string_lossy())));
|
||||
spans.push(Span::raw(" -> "));
|
||||
spans.push(Span::raw(display_text(&file.path.to_string_lossy())));
|
||||
} else {
|
||||
spans.push(Span::raw(entry.label));
|
||||
if entry.is_dir && picker.view == FileView::Tree {
|
||||
spans.push(Span::raw("/"));
|
||||
}
|
||||
}
|
||||
Line::from(spans).style(style)
|
||||
})
|
||||
@@ -2675,6 +2694,118 @@ mod tests {
|
||||
notice: None,
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn diff_status_layouts_hide_absent_panes_and_leave_unchanged_renames_blank() {
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(160, 12)).unwrap();
|
||||
let mut panels = WheelPanels::default();
|
||||
for status in [FileStatus::Added, FileStatus::Removed] {
|
||||
let added = status == FileStatus::Added;
|
||||
let mut file = deletion_file();
|
||||
file.status = status;
|
||||
file.before = if added {
|
||||
vec![]
|
||||
} else {
|
||||
vec!["removed content".into()]
|
||||
};
|
||||
file.after = if added {
|
||||
vec!["added content".into()]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
file.hunks = vec![crate::diff::Hunk {
|
||||
before_start: 0,
|
||||
before_len: usize::from(!added),
|
||||
after_start: 0,
|
||||
after_len: usize::from(added),
|
||||
}];
|
||||
let mut diff = diff_comparison(vec![file]);
|
||||
diff.files_visible = false;
|
||||
diff.focus = if added { Focus::After } else { Focus::Before };
|
||||
diff.left_after = 2;
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
let geometry = panels.diff.as_ref().unwrap();
|
||||
let present = geometry.regions[if added { 2 } else { 1 }].0;
|
||||
assert_eq!(
|
||||
geometry.regions[if added { 1 } else { 2 }].0,
|
||||
Rect::default()
|
||||
);
|
||||
assert_eq!(present.x, 0);
|
||||
assert_eq!(present.width, 160);
|
||||
assert_eq!(geometry.viewport.unwrap().3, 2);
|
||||
assert_eq!(geometry.regions[if added { 2 } else { 1 }].1, diff.focus);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(present.x, present.y)].fg,
|
||||
Color::Cyan
|
||||
);
|
||||
let text = screen_text(&terminal);
|
||||
assert!(text.contains(if added {
|
||||
"ded content"
|
||||
} else {
|
||||
"removed content"
|
||||
}));
|
||||
assert!(text.contains(if added { "After · A" } else { "Before · D" }));
|
||||
assert!(!text.contains(if added { "Before ·" } else { "After ·" }));
|
||||
assert!(!text.contains('╱'));
|
||||
}
|
||||
let mut empty_added = deletion_file();
|
||||
empty_added.status = FileStatus::Added;
|
||||
empty_added.before.clear();
|
||||
empty_added.after.clear();
|
||||
empty_added.hunks.clear();
|
||||
empty_added.notice = Some("metadata-only addition".into());
|
||||
let mut diff = diff_comparison(vec![empty_added]);
|
||||
diff.files_visible = false;
|
||||
diff.focus = Focus::After;
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
let geometry = panels.diff.as_ref().unwrap();
|
||||
assert_eq!(geometry.regions[1].0, Rect::default());
|
||||
assert_eq!(geometry.regions[2].0.width, 160);
|
||||
assert!(screen_text(&terminal).contains("metadata-only addition"));
|
||||
|
||||
let mut renamed = deletion_file();
|
||||
renamed.status = FileStatus::Renamed;
|
||||
renamed.old_path = Some("old/path.txt".into());
|
||||
renamed.path = "new/path.txt".into();
|
||||
renamed.before = vec!["unchanged rename content".into()];
|
||||
renamed.after = renamed.before.clone();
|
||||
renamed.hunks.clear();
|
||||
let mut diff = diff_comparison(vec![renamed]);
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
assert!(screen_text(&terminal).contains("old/path.txt -> new/path.txt"));
|
||||
assert_eq!(
|
||||
diff.files.as_mut().unwrap().accept(),
|
||||
Some("new/path.txt".into())
|
||||
);
|
||||
diff.files_visible = false;
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
let geometry = panels.diff.as_ref().unwrap();
|
||||
let blank = geometry.regions[1].0;
|
||||
assert_eq!(blank.width, 160);
|
||||
assert_eq!(geometry.regions[2].0, Rect::default());
|
||||
let buffer = terminal.backend().buffer();
|
||||
assert_eq!(buffer[(blank.x, blank.y)].symbol(), "┌");
|
||||
for y in blank.y + 1..blank.bottom() - 1 {
|
||||
for x in blank.x + 1..blank.right() - 1 {
|
||||
assert_eq!(buffer[(x, y)].symbol(), " ");
|
||||
assert_eq!(buffer[(x, y)].bg, Color::Reset);
|
||||
}
|
||||
}
|
||||
let text = screen_text(&terminal);
|
||||
assert!(!text.contains("Before ·"));
|
||||
assert!(!text.contains("After ·"));
|
||||
assert!(!text.contains("unchanged rename content"));
|
||||
assert!(!text.contains('╱'));
|
||||
|
||||
let mut modified = diff_comparison(vec![deletion_file()]);
|
||||
modified.files_visible = false;
|
||||
render_diff(&mut terminal, &modified, &mut panels);
|
||||
let regions = panels.diff.as_ref().unwrap().regions;
|
||||
assert_eq!(regions[1].0.width, 80);
|
||||
assert_eq!(regions[2].0.width, 80);
|
||||
let text = screen_text(&terminal);
|
||||
assert!(text.contains("Before · M"));
|
||||
assert!(text.contains("After · M"));
|
||||
}
|
||||
|
||||
fn render_diff(
|
||||
terminal: &mut Terminal<ratatui::backend::TestBackend>,
|
||||
@@ -2855,7 +2986,7 @@ mod tests {
|
||||
after_eof: false,
|
||||
path: "added.txt".into(),
|
||||
old_path: None,
|
||||
status: FileStatus::Added,
|
||||
status: FileStatus::Modified,
|
||||
before: vec![],
|
||||
after: vec!["first addition".into(), String::new()],
|
||||
hunks: vec![crate::diff::Hunk {
|
||||
@@ -3189,8 +3320,11 @@ mod tests {
|
||||
} else {
|
||||
file.path.as_path()
|
||||
};
|
||||
let expected =
|
||||
format!("{} {}", diff_status(file.status), path.to_string_lossy());
|
||||
let expected = if file.status == FileStatus::Renamed {
|
||||
format!("R old�name.txt -> {}", file.path.to_string_lossy())
|
||||
} else {
|
||||
format!("{} {}", diff_status(file.status), path.to_string_lossy())
|
||||
};
|
||||
let (x, y) = (0..16)
|
||||
.find_map(|y| {
|
||||
let row = (0..100)
|
||||
@@ -3203,7 +3337,6 @@ mod tests {
|
||||
.unwrap_or_else(|| panic!("missing {expected}"));
|
||||
assert_eq!(buffer[(x, y)].fg, color);
|
||||
}
|
||||
assert!(screen_text(&terminal).contains("r.txt ← old�name.txt"));
|
||||
assert_eq!(picker.accept(), Some(files[index].path.clone()));
|
||||
assert!(picker.query.is_empty());
|
||||
}
|
||||
@@ -3326,9 +3459,11 @@ mod tests {
|
||||
assert!(rect.right() <= width && rect.bottom() <= height);
|
||||
}
|
||||
let [navigator, before, after] = geometry.regions.map(|(rect, _)| rect);
|
||||
assert!(before.right() <= after.x);
|
||||
if before.width > 0 && after.width > 0 {
|
||||
assert!(before.right() <= after.x);
|
||||
}
|
||||
if navigator.width > 0 {
|
||||
assert!(after.right() <= navigator.x);
|
||||
assert!(before.right().max(after.right()) <= navigator.x);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3596,7 +3731,7 @@ mod tests {
|
||||
git(&["add", "."]);
|
||||
git(&["commit", "-qm", "base"]);
|
||||
for index in 0..8 {
|
||||
fs::remove_file(directory.0.join(format!("file-{index}.txt"))).unwrap();
|
||||
directory.file(&format!("file-{index}.txt"), "changed\nsecond\nthird\n");
|
||||
}
|
||||
let mut session = EditorSession::open(Some(path), vec![], None).unwrap();
|
||||
let original = session.editor().lines().to_vec();
|
||||
@@ -3616,8 +3751,8 @@ mod tests {
|
||||
};
|
||||
session.execute("diff");
|
||||
ready(&mut session, Phase::Before);
|
||||
session.handle(Input::Down);
|
||||
session.handle(Input::Enter);
|
||||
session.handle(Input::Down);
|
||||
session.handle(Input::Enter);
|
||||
ready(&mut session, Phase::View);
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 18)).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user