feat: show repository changes in normal file pickers
This commit is contained in:
+263
-8
@@ -4269,6 +4269,7 @@ fn draw_file_list(
|
||||
);
|
||||
}
|
||||
let entries = picker.entries();
|
||||
let normal_markers = changed_files.is_none() && picker.has_repository_changes();
|
||||
if entries.len() == 0 {
|
||||
frame.render_widget(
|
||||
Paragraph::new(if picker.query.is_empty() {
|
||||
@@ -4304,6 +4305,10 @@ fn draw_file_list(
|
||||
let changed = entry
|
||||
.catalog_index
|
||||
.and_then(|index| changed_files.and_then(|(files, _)| files.get(index)));
|
||||
let status = changed.map(|file| file.status).or(entry.status);
|
||||
let old_path = changed
|
||||
.and_then(|file| file.old_path.as_deref())
|
||||
.or(entry.old_path);
|
||||
let prefix = format!(
|
||||
"{} {:indent$}",
|
||||
if selected { ">" } else { " " },
|
||||
@@ -4314,9 +4319,9 @@ fn draw_file_list(
|
||||
0
|
||||
},
|
||||
);
|
||||
let mut spans = Vec::with_capacity(if changed.is_some() { 7 } else { 3 });
|
||||
let mut spans = Vec::with_capacity(if status.is_some() { 7 } else { 3 });
|
||||
spans.push(Span::raw(prefix));
|
||||
if let Some(file) = changed {
|
||||
if changed.is_some() {
|
||||
let reviewed = entry.catalog_index.is_some_and(|index| {
|
||||
changed_files.is_some_and(|(_, reviewed)| reviewed.contains(&index))
|
||||
});
|
||||
@@ -4324,25 +4329,32 @@ fn draw_file_list(
|
||||
if reviewed { "✓ " } else { " " },
|
||||
Style::default().fg(Color::Green),
|
||||
));
|
||||
let color = match file.status {
|
||||
}
|
||||
if let Some(status) = status {
|
||||
let color = match status {
|
||||
FileStatus::Added => Color::Green,
|
||||
FileStatus::Removed => Color::Red,
|
||||
FileStatus::Modified => Color::Yellow,
|
||||
FileStatus::Renamed => Color::Cyan,
|
||||
};
|
||||
spans.push(Span::styled(
|
||||
diff_status(file.status),
|
||||
diff_status(status),
|
||||
Style::default().fg(color),
|
||||
));
|
||||
spans.push(Span::raw(" "));
|
||||
} else if normal_markers {
|
||||
spans.push(Span::raw(" "));
|
||||
}
|
||||
if let Some(file) = changed
|
||||
&& file.status == FileStatus::Renamed
|
||||
&& let Some(old_path) = &file.old_path
|
||||
if status == Some(FileStatus::Renamed)
|
||||
&& let Some(old_path) = old_path
|
||||
{
|
||||
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())));
|
||||
spans.push(Span::raw(display_text(
|
||||
&changed
|
||||
.map_or(entry.path, |file| file.path.as_path())
|
||||
.to_string_lossy(),
|
||||
)));
|
||||
} else {
|
||||
spans.push(Span::raw(entry.label));
|
||||
if entry.is_dir && picker.view == FileView::Tree {
|
||||
@@ -5956,6 +5968,249 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_file_markers_keep_clean_rows_and_match_diff_colors_in_every_view() {
|
||||
use crate::repository::WorkingFileChange;
|
||||
|
||||
let directory = Directory::new();
|
||||
let changes: Vec<_> = [
|
||||
("z.txt", FileStatus::Added),
|
||||
("chain/a/m.txt", FileStatus::Modified),
|
||||
("gone/deep/d.txt", FileStatus::Removed),
|
||||
("r.txt", FileStatus::Renamed),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(path, status)| {
|
||||
if status != FileStatus::Removed {
|
||||
directory.file(path, "");
|
||||
}
|
||||
WorkingFileChange {
|
||||
path: path.into(),
|
||||
status,
|
||||
old_path: (status == FileStatus::Renamed).then(|| "old\nname.txt".into()),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
directory.file("clean.txt", "");
|
||||
let mut picker = FilePicker::scan(
|
||||
directory.0.clone(),
|
||||
FilePresentation::Floating,
|
||||
FileView::List,
|
||||
)
|
||||
.unwrap();
|
||||
picker.set_repository_changes(changes.clone());
|
||||
for change in &changes {
|
||||
picker.reveal_path(&change.path);
|
||||
}
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 24)).unwrap();
|
||||
for presentation in [FilePresentation::Floating, FilePresentation::Docked] {
|
||||
picker.presentation = presentation;
|
||||
for view in [FileView::Tree, FileView::List] {
|
||||
picker.set_view(view);
|
||||
for focused in [true, false] {
|
||||
picker.focused = focused;
|
||||
for selected in changes
|
||||
.iter()
|
||||
.map(|change| change.path.as_path())
|
||||
.chain(std::iter::once(std::path::Path::new("clean.txt")))
|
||||
{
|
||||
picker.reveal_path(selected);
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw_file_list(
|
||||
frame,
|
||||
&picker,
|
||||
frame.area(),
|
||||
"",
|
||||
&mut WheelPanels::default(),
|
||||
None,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let text = screen_text(&terminal);
|
||||
assert!(text.contains("clean.txt"));
|
||||
assert!(!text.contains('✓'));
|
||||
assert!(!text.contains("old\nname.txt"));
|
||||
let buffer = terminal.backend().buffer();
|
||||
for (change, color) in changes.iter().zip([
|
||||
Color::Green,
|
||||
Color::Yellow,
|
||||
Color::Red,
|
||||
Color::Cyan,
|
||||
]) {
|
||||
let path = if view == FileView::Tree {
|
||||
std::path::Path::new(change.path.file_name().unwrap())
|
||||
} else {
|
||||
change.path.as_path()
|
||||
};
|
||||
let expected = if change.status == FileStatus::Renamed {
|
||||
"R old�name.txt -> r.txt".to_owned()
|
||||
} else {
|
||||
format!("{} {}", diff_status(change.status), path.to_string_lossy())
|
||||
};
|
||||
let (x, y) = (0..24)
|
||||
.find_map(|y| {
|
||||
let row = (0..100)
|
||||
.map(|x| buffer[(x, y)].symbol())
|
||||
.collect::<String>();
|
||||
row.find(&expected).map(|offset| {
|
||||
(UnicodeWidthStr::width(&row[..offset]) as u16, y)
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| panic!("missing {expected}"));
|
||||
assert_eq!(buffer[(x, y)].fg, color);
|
||||
assert_eq!(
|
||||
buffer[(x, y)].bg,
|
||||
if change.path == selected {
|
||||
if focused {
|
||||
Color::Blue
|
||||
} else {
|
||||
Color::DarkGray
|
||||
}
|
||||
} else {
|
||||
Color::Reset
|
||||
}
|
||||
);
|
||||
}
|
||||
let clean_row = text.lines().find(|row| row.contains("clean.txt")).unwrap();
|
||||
assert!(clean_row.contains(
|
||||
if selected == std::path::Path::new("clean.txt") {
|
||||
"> clean.txt"
|
||||
} else {
|
||||
" clean.txt"
|
||||
}
|
||||
));
|
||||
}
|
||||
for query in ["m.txt", "clean"] {
|
||||
picker.query = query.into();
|
||||
picker.refilter();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw_file_list(
|
||||
frame,
|
||||
&picker,
|
||||
frame.area(),
|
||||
"",
|
||||
&mut WheelPanels::default(),
|
||||
None,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let text = screen_text(&terminal);
|
||||
if query == "m.txt" {
|
||||
assert!(text.contains("M "));
|
||||
assert!(!text.contains("clean.txt"));
|
||||
} else {
|
||||
assert!(text.contains("> clean.txt"));
|
||||
assert!(!text.contains("M "));
|
||||
}
|
||||
assert!(!text.contains('✓'));
|
||||
picker.clear_filter();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
picker.refresh();
|
||||
assert!(picker.has_repository_changes());
|
||||
assert!(
|
||||
picker
|
||||
.entries()
|
||||
.any(|row| row.status == Some(FileStatus::Renamed))
|
||||
);
|
||||
picker.set_repository_changes(Vec::new());
|
||||
picker.set_view(FileView::List);
|
||||
picker.reveal_path(std::path::Path::new("clean.txt"));
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw_file_list(
|
||||
frame,
|
||||
&picker,
|
||||
frame.area(),
|
||||
"",
|
||||
&mut WheelPanels::default(),
|
||||
None,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let text = screen_text(&terminal);
|
||||
assert!(text.contains("> clean.txt"));
|
||||
assert!(text.contains("r.txt"));
|
||||
assert!(!text.contains("old�name.txt"));
|
||||
assert!(!text.contains("d.txt"));
|
||||
assert!(!text.contains('✓'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_status_prefix_clips_without_review_columns_or_painting_outside_tiny_bounds() {
|
||||
let directory = Directory::new();
|
||||
directory.file("界.txt", "");
|
||||
let mut picker = FilePicker::scan(
|
||||
directory.0.clone(),
|
||||
FilePresentation::Floating,
|
||||
FileView::List,
|
||||
)
|
||||
.unwrap();
|
||||
picker.set_repository_changes(vec![crate::repository::WorkingFileChange {
|
||||
path: "界.txt".into(),
|
||||
status: FileStatus::Modified,
|
||||
old_path: None,
|
||||
}]);
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(12, 4)).unwrap();
|
||||
for focused in [true, false] {
|
||||
picker.focused = focused;
|
||||
for height in 0..=3 {
|
||||
for width in 0..=8 {
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
frame.render_widget(
|
||||
Paragraph::new(
|
||||
"XXXXXXXXXXXX\nXXXXXXXXXXXX\nXXXXXXXXXXXX\nXXXXXXXXXXXX",
|
||||
),
|
||||
frame.area(),
|
||||
);
|
||||
draw_file_list(
|
||||
frame,
|
||||
&picker,
|
||||
Rect::new(0, 0, width, height),
|
||||
"",
|
||||
&mut WheelPanels::default(),
|
||||
None,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
for y in 0..4 {
|
||||
for x in 0..12 {
|
||||
if x >= width || y >= height {
|
||||
assert_eq!(buffer[(x, y)].symbol(), "X");
|
||||
}
|
||||
}
|
||||
}
|
||||
if height < 2 {
|
||||
continue;
|
||||
}
|
||||
for (x, symbol) in "> M ".chars().take(usize::from(width)).enumerate() {
|
||||
assert_eq!(buffer[(x as u16, 1)].symbol(), symbol.to_string());
|
||||
}
|
||||
if width >= 3 {
|
||||
assert_eq!(buffer[(2, 1)].fg, Color::Yellow);
|
||||
assert_eq!(
|
||||
buffer[(2, 1)].bg,
|
||||
if focused {
|
||||
Color::Blue
|
||||
} else {
|
||||
Color::DarkGray
|
||||
}
|
||||
);
|
||||
}
|
||||
if width >= 6 {
|
||||
assert_eq!(buffer[(4, 1)].symbol(), "界");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hiding_diff_explorer_reclaims_workspace_and_restores_its_geometry() {
|
||||
let mut diff = diff_comparison(vec![deletion_file()]);
|
||||
|
||||
Reference in New Issue
Block a user