feat: mark diff files as reviewed

This commit is contained in:
Valdemar Lorenzen
2026-10-13 13:07:02 +02:00
parent c912abaf17
commit 38fcf86195
6 changed files with 522 additions and 38 deletions
+136 -34
View File
@@ -1,6 +1,6 @@
use std::{
borrow::Cow,
collections::BTreeMap,
collections::{BTreeMap, BTreeSet},
fmt::Write as _,
io::{self, Stdout},
panic::{self, AssertUnwindSafe, PanicHookInfo},
@@ -514,10 +514,11 @@ impl BufferViewports {
panel,
editor.file_navigation_hint(),
&mut self.panels,
editor
.diff()
.and_then(|diff| diff.comparison.as_ref())
.map(|comparison| comparison.files.as_slice()),
editor.diff().and_then(|diff| {
diff.comparison
.as_ref()
.map(|comparison| (comparison.files.as_slice(), &diff.reviewed_files))
}),
);
if let Some(geometry) = &mut self.panels.diff {
geometry.regions[0] = (panel, Focus::Files);
@@ -2060,10 +2061,11 @@ fn draw_overlays(
panel,
editor.file_navigation_hint(),
panels,
editor
.diff()
.and_then(|diff| diff.comparison.as_ref())
.map(|comparison| comparison.files.as_slice()),
editor.diff().and_then(|diff| {
diff.comparison
.as_ref()
.map(|comparison| (comparison.files.as_slice(), &diff.reviewed_files))
}),
);
}
if let Some(palette) = editor.palette() {
@@ -2774,7 +2776,7 @@ fn draw_diff(
navigator,
file_navigation_hint,
panels,
comparison.map(|comparison| comparison.files.as_slice()),
comparison.map(|comparison| (comparison.files.as_slice(), &diff.reviewed_files)),
);
}
let mut text_widths = [0; 2];
@@ -4128,7 +4130,7 @@ fn draw_file_list(
panel: Rect,
navigation_hint: &str,
panels: &mut WheelPanels,
changed_files: Option<&[ChangedFile]>,
changed_files: Option<(&[ChangedFile], &BTreeSet<usize>)>,
) {
if panel.width == 0 || panel.height == 0 {
return;
@@ -4262,7 +4264,7 @@ fn draw_file_list(
}
let changed = entry
.catalog_index
.and_then(|index| changed_files.and_then(|files| files.get(index)));
.and_then(|index| changed_files.and_then(|(files, _)| files.get(index)));
let prefix = format!(
"{} {:indent$}",
if selected { ">" } else { " " },
@@ -4273,9 +4275,16 @@ fn draw_file_list(
0
},
);
let mut spans = Vec::with_capacity(if changed.is_some() { 6 } else { 3 });
let mut spans = Vec::with_capacity(if changed.is_some() { 7 } else { 3 });
spans.push(Span::raw(prefix));
if let Some(file) = changed {
let reviewed = entry.catalog_index.is_some_and(|index| {
changed_files.is_some_and(|(_, reviewed)| reviewed.contains(&index))
});
spans.push(Span::styled(
if reviewed { "✓ " } else { " " },
Style::default().fg(Color::Green),
));
let color = match file.status {
FileStatus::Added => Color::Green,
FileStatus::Removed => Color::Red,
@@ -5664,6 +5673,7 @@ mod tests {
picker.focused = focused;
for index in 0..files.len() {
picker.reveal_path(&files[index].path);
let reviewed = BTreeSet::from([index]);
terminal
.draw(|frame| {
draw_file_list(
@@ -5672,15 +5682,15 @@ mod tests {
frame.area(),
"",
&mut WheelPanels::default(),
Some(&files),
Some((&files, &reviewed)),
);
})
.unwrap();
let buffer = terminal.backend().buffer();
for (file, color) in
files
.iter()
.zip([Color::Green, Color::Yellow, Color::Red, Color::Cyan])
for (catalog_index, (file, color)) in files
.iter()
.zip([Color::Green, Color::Yellow, Color::Red, Color::Cyan])
.enumerate()
{
let path = if view == FileView::Tree {
std::path::Path::new(file.path.file_name().unwrap())
@@ -5703,6 +5713,30 @@ mod tests {
})
.unwrap_or_else(|| panic!("missing {expected}"));
assert_eq!(buffer[(x, y)].fg, color);
assert_eq!(
buffer[(x - 2, y)].symbol(),
if catalog_index == index { "✓" } else { " " }
);
assert_eq!(buffer[(x - 1, y)].symbol(), " ");
if catalog_index == index {
assert_eq!(buffer[(x - 2, y)].fg, Color::Green);
assert_eq!(
buffer[(x - 2, y)].bg,
if focused {
Color::Blue
} else {
Color::DarkGray
}
);
}
}
for row in screen_text(&terminal)
.lines()
.filter(|row| row.contains("chain/a/"))
{
if view == FileView::Tree {
assert!(!row.contains('✓'));
}
}
assert_eq!(picker.accept(), Some(files[index].path.clone()));
assert!(picker.query.is_empty());
@@ -5717,22 +5751,35 @@ mod tests {
.skip(1)
.map(|(index, file)| (index, file.path.clone())),
);
picker.query = "m.txt".into();
picker.refilter();
terminal
.draw(|frame| {
draw_file_list(
frame,
&picker,
frame.area(),
"",
&mut WheelPanels::default(),
Some(&files),
);
})
.unwrap();
assert!(screen_text(&terminal).contains("M chain/a/m.txt"));
assert!(!screen_text(&terminal).contains("R r.txt"));
for view in [FileView::Tree, FileView::List] {
picker.set_view(view);
picker.query = "m.txt".into();
picker.refilter();
terminal
.draw(|frame| {
draw_file_list(
frame,
&picker,
frame.area(),
"",
&mut WheelPanels::default(),
Some((&files, &BTreeSet::from([1]))),
);
})
.unwrap();
let text = screen_text(&terminal);
assert!(text.contains("✓ M "));
assert_eq!(text.matches('✓').count(), 1);
assert!(!text.contains("R r.txt"));
assert_eq!(
picker
.entries()
.find(|entry| !entry.is_dir)
.unwrap()
.catalog_index,
Some(1)
);
}
terminal
.draw(|frame| {
draw_file_list(
@@ -5746,6 +5793,61 @@ mod tests {
})
.unwrap();
assert!(!screen_text(&terminal).contains("M chain/a/m.txt"));
assert!(!screen_text(&terminal).contains('✓'));
}
#[test]
fn reviewed_file_prefix_tracks_state_and_clips_in_narrow_panes() {
let mut file = deletion_file();
file.path = "界.txt".into();
file.status = FileStatus::Modified;
let files = [file];
let mut picker = FilePicker::from_paths(PathBuf::new(), [(0, files[0].path.clone())]);
picker.set_view(FileView::List);
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(12, 3)).unwrap();
let mut reviewed = BTreeSet::new();
for marked in [true, false, true] {
if marked {
reviewed.insert(0);
} else {
reviewed.remove(&0);
}
for width in 1..=8 {
terminal
.draw(|frame| {
frame.render_widget(
Paragraph::new("XXXXXXXXXXXX\nXXXXXXXXXXXX\nXXXXXXXXXXXX"),
frame.area(),
);
draw_file_list(
frame,
&picker,
Rect::new(0, 0, width, 3),
"",
&mut WheelPanels::default(),
Some((&files, &reviewed)),
);
})
.unwrap();
let buffer = terminal.backend().buffer();
let prefix = if marked { "> ✓ M " } else { "> M " };
for (x, symbol) in prefix.chars().take(usize::from(width)).enumerate() {
assert_eq!(buffer[(x as u16, 1)].symbol(), symbol.to_string());
}
for x in width..12 {
assert_eq!(buffer[(x, 1)].symbol(), "X");
}
if width >= 3 && marked {
assert_eq!(buffer[(2, 1)].fg, Color::Green);
}
if width >= 5 {
assert_eq!(buffer[(4, 1)].fg, Color::Yellow);
}
if width >= 8 {
assert_eq!(buffer[(6, 1)].symbol(), "界");
}
}
}
}
#[test]