feat: mark diff files as reviewed
This commit is contained in:
+190
@@ -213,6 +213,7 @@ pub struct DiffState {
|
||||
pub error: Option<String>,
|
||||
pub comparison: Option<Comparison>,
|
||||
pub comparison_id: Option<u64>,
|
||||
pub reviewed_files: BTreeSet<usize>,
|
||||
pub(crate) snapshot_roots: [Option<PathBuf>; 2],
|
||||
pub files: Option<FilePicker>,
|
||||
pub files_visible: bool,
|
||||
@@ -259,6 +260,7 @@ impl DiffState {
|
||||
error: None,
|
||||
comparison: None,
|
||||
comparison_id: None,
|
||||
reviewed_files: BTreeSet::new(),
|
||||
files: None,
|
||||
snapshot_roots: [None, None],
|
||||
files_visible: true,
|
||||
@@ -371,6 +373,7 @@ impl DiffState {
|
||||
}
|
||||
Ok(Outcome::Comparison(comparison)) if expected == Some(RequestKind::Comparison) => {
|
||||
self.live_files.clear();
|
||||
self.reviewed_files.clear();
|
||||
self.live_buffer = None;
|
||||
self.live_revision = None;
|
||||
self.follow_caret = false;
|
||||
@@ -649,6 +652,7 @@ impl DiffState {
|
||||
self.phase = Phase::After;
|
||||
self.comparison = None;
|
||||
self.comparison_id = None;
|
||||
self.reviewed_files.clear();
|
||||
self.queue(operation);
|
||||
}
|
||||
}
|
||||
@@ -872,6 +876,45 @@ impl DiffState {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_reviewed(&mut self) -> Result<bool, &'static str> {
|
||||
let comparison = self
|
||||
.comparison
|
||||
.as_ref()
|
||||
.filter(|_| self.phase == Phase::View)
|
||||
.ok_or("No comparison is open")?;
|
||||
let index = if self.focus == Focus::Files {
|
||||
let entry = self
|
||||
.files
|
||||
.as_ref()
|
||||
.filter(|_| self.files_visible)
|
||||
.and_then(|files| files.entries().nth(files.selected))
|
||||
.ok_or("No changed file is selected")?;
|
||||
if entry.is_dir {
|
||||
return Err("Select a changed file, not a directory");
|
||||
}
|
||||
entry
|
||||
.catalog_index
|
||||
.filter(|&index| {
|
||||
comparison
|
||||
.files
|
||||
.get(index)
|
||||
.is_some_and(|file| file.path == entry.path)
|
||||
})
|
||||
.ok_or("No changed file is selected")?
|
||||
} else {
|
||||
self.file
|
||||
};
|
||||
if comparison.files.get(index).is_none() {
|
||||
return Err("No changed file is selected");
|
||||
}
|
||||
if self.reviewed_files.remove(&index) {
|
||||
Ok(false)
|
||||
} else {
|
||||
self.reviewed_files.insert(index);
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn select_file(&mut self, path: &Path) -> bool {
|
||||
let Some(index) = self.comparison.as_ref().and_then(|comparison| {
|
||||
let relative = path.strip_prefix(&comparison.root).unwrap_or(path);
|
||||
@@ -1138,6 +1181,17 @@ impl DiffState {
|
||||
let file = &self.comparison.as_ref().unwrap().files[self.file];
|
||||
let was_live = self.live_files.contains(&self.file);
|
||||
let changed_lines = changed_line_range(&file.after, lines);
|
||||
if self.reviewed_files.contains(&self.file) {
|
||||
let unchanged = if was_live {
|
||||
changed_lines.0 == changed_lines.1 && changed_lines.1 == changed_lines.2
|
||||
} else {
|
||||
let eof = lines.len() > 1 && lines.last().is_some_and(String::is_empty);
|
||||
file.after_eof == eof && file.after == live_content(lines, eof)
|
||||
};
|
||||
if !unchanged {
|
||||
self.reviewed_files.remove(&self.file);
|
||||
}
|
||||
}
|
||||
if was_live && changed_lines.0 == changed_lines.1 && changed_lines.1 == changed_lines.2 {
|
||||
self.live_revision = Some(revision);
|
||||
return;
|
||||
@@ -3094,6 +3148,142 @@ mod tests {
|
||||
state
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reviewed_files_use_highlighted_catalog_indices_not_sorted_or_filtered_rows() {
|
||||
let mut state = DiffState::new();
|
||||
let mut changes = comparison();
|
||||
changes.files[0].path = "nested/z.txt".into();
|
||||
changes.files[1].path = "nested/a.txt".into();
|
||||
state.request_kind = Some(RequestKind::Comparison);
|
||||
state.apply_result(1, Ok(Outcome::Comparison(changes)));
|
||||
for view in [FileView::List, FileView::Tree] {
|
||||
let files = state.files.as_mut().unwrap();
|
||||
files.set_view(view);
|
||||
files.clear_filter();
|
||||
files.start_search();
|
||||
for ch in "nested/a".chars() {
|
||||
files.handle_filter_input(&Input::Char(ch));
|
||||
}
|
||||
files.reveal_path(Path::new("nested/a.txt"));
|
||||
assert_eq!(state.file, 0);
|
||||
assert_eq!(state.toggle_reviewed(), Ok(true));
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([1]));
|
||||
assert_eq!(state.toggle_reviewed(), Ok(false));
|
||||
assert!(state.reviewed_files.is_empty());
|
||||
}
|
||||
let files = state.files.as_mut().unwrap();
|
||||
files.clear_filter();
|
||||
files.finish_search();
|
||||
let selected = files.entries().position(|entry| entry.is_dir).unwrap();
|
||||
files.selected = selected;
|
||||
assert_eq!(
|
||||
state.toggle_reviewed(),
|
||||
Err("Select a changed file, not a directory")
|
||||
);
|
||||
assert!(state.reviewed_files.is_empty());
|
||||
let files = state.files.as_mut().unwrap();
|
||||
files.start_search();
|
||||
for ch in "missing".chars() {
|
||||
files.handle_filter_input(&Input::Char(ch));
|
||||
}
|
||||
assert_eq!(state.toggle_reviewed(), Err("No changed file is selected"));
|
||||
state.files = Some(FilePicker::from_paths(
|
||||
PathBuf::new(),
|
||||
[(0, PathBuf::from("ordinary.txt"))],
|
||||
));
|
||||
assert_eq!(state.toggle_reviewed(), Err("No changed file is selected"));
|
||||
state.focus = Focus::Before;
|
||||
assert_eq!(state.toggle_reviewed(), Ok(true));
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([0]));
|
||||
assert!(state.select_file_index(2));
|
||||
state.focus = Focus::Before;
|
||||
assert_eq!(state.toggle_reviewed(), Ok(true));
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([0, 2]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reviewed_files_survive_lazy_details_and_views_but_reset_on_comparison() {
|
||||
let mut state = DiffState::new();
|
||||
assert_eq!(state.toggle_reviewed(), Err("No comparison is open"));
|
||||
let mut changes = comparison();
|
||||
changes.files[0].loaded = false;
|
||||
changes.files[0].before.clear();
|
||||
changes.files[0].after.clear();
|
||||
state.request_kind = Some(RequestKind::Comparison);
|
||||
state.apply_result(1, Ok(Outcome::Comparison(changes)));
|
||||
assert_eq!(state.toggle_reviewed(), Ok(true));
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([0]));
|
||||
let file = comparison().files.remove(0);
|
||||
state.apply_result(
|
||||
2,
|
||||
Ok(Outcome::File {
|
||||
comparison_id: 1,
|
||||
file_index: 0,
|
||||
file,
|
||||
}),
|
||||
);
|
||||
assert!(state.selected_file_loaded());
|
||||
state.set_view_mode(ViewMode::Unified);
|
||||
state.set_view_mode(ViewMode::Split);
|
||||
state.split_axis = crate::panes::SplitAxis::Horizontal;
|
||||
assert!(state.select_file_index(1));
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([0]));
|
||||
state.before = Some(revision(
|
||||
RevisionTarget::Commit("base".into()),
|
||||
"base",
|
||||
"",
|
||||
Category::Commits,
|
||||
));
|
||||
state.after = Some(revision(
|
||||
RevisionTarget::Commit("head".into()),
|
||||
"head",
|
||||
"",
|
||||
Category::Commits,
|
||||
));
|
||||
state.toggle_comparison_mode().unwrap();
|
||||
assert!(state.reviewed_files.is_empty());
|
||||
assert_eq!(state.toggle_reviewed(), Err("No comparison is open"));
|
||||
state.apply_result(3, Ok(Outcome::Comparison(comparison())));
|
||||
assert!(state.reviewed_files.is_empty());
|
||||
assert_eq!(state.toggle_reviewed(), Ok(true));
|
||||
state.request_kind = Some(RequestKind::Comparison);
|
||||
state.apply_result(4, Ok(Outcome::Comparison(comparison())));
|
||||
assert!(state.reviewed_files.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn working_review_marks_clear_only_for_changed_bytes_or_eof() {
|
||||
for (after, eof, lines) in [
|
||||
(vec!["changed".into()], false, vec!["changed".into()]),
|
||||
(
|
||||
vec!["changed".into()],
|
||||
true,
|
||||
vec!["changed".into(), String::new()],
|
||||
),
|
||||
(Vec::new(), false, vec![String::new()]),
|
||||
] {
|
||||
let mut state = live_state(vec!["base".into()], after, eof);
|
||||
assert_eq!(state.toggle_reviewed(), Ok(true));
|
||||
state.refresh_after(&lines, 1);
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([0]));
|
||||
state.refresh_after(&lines, 2);
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([0]));
|
||||
state.refresh_after(&["edited".into()], 3);
|
||||
assert!(state.reviewed_files.is_empty());
|
||||
assert_eq!(state.toggle_reviewed(), Ok(true));
|
||||
state.refresh_after(&["edited".into(), String::new()], 4);
|
||||
assert!(state.reviewed_files.is_empty());
|
||||
assert_eq!(state.toggle_reviewed(), Ok(true));
|
||||
state.refresh_after(&["edited".into()], 5);
|
||||
assert!(state.reviewed_files.is_empty());
|
||||
}
|
||||
let mut state = live_state(vec!["base".into()], vec!["changed".into()], false);
|
||||
state.comparison.as_mut().unwrap().files[0].loaded = false;
|
||||
assert_eq!(state.toggle_reviewed(), Ok(true));
|
||||
state.refresh_after(&["edited".into()], 1);
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unified_groups_changed_runs_and_roundtrips_side_specific_source_rows() {
|
||||
let mut state = live_state(
|
||||
|
||||
+186
-2
@@ -766,6 +766,12 @@ core_actions![
|
||||
&[Mode::Normal],
|
||||
true,
|
||||
),
|
||||
(
|
||||
"core.diff.review.toggle",
|
||||
Action::DiffReviewToggle,
|
||||
&[Mode::Normal],
|
||||
true,
|
||||
),
|
||||
(
|
||||
"core.word_wrap.toggle",
|
||||
Action::WordWrapToggle,
|
||||
@@ -3246,6 +3252,7 @@ impl Editor {
|
||||
| Action::DiffModeToggle
|
||||
| Action::DiffViewToggle
|
||||
| Action::DiffOrientationToggle
|
||||
| Action::DiffReviewToggle
|
||||
| Action::WordWrapToggle
|
||||
| Action::GitBlameToggle
|
||||
| Action::Quit
|
||||
@@ -6185,6 +6192,7 @@ impl Editor {
|
||||
| Action::DiffModeToggle
|
||||
| Action::DiffViewToggle
|
||||
| Action::DiffOrientationToggle
|
||||
| Action::DiffReviewToggle
|
||||
| Action::WordWrapToggle
|
||||
| Action::GitBlameToggle
|
||||
| Action::LspDefinition
|
||||
@@ -7002,13 +7010,25 @@ impl Editor {
|
||||
}
|
||||
if matches!(
|
||||
entry.action,
|
||||
Action::DiffModeToggle | Action::DiffViewToggle | Action::DiffOrientationToggle
|
||||
Action::DiffModeToggle
|
||||
| Action::DiffViewToggle
|
||||
| Action::DiffOrientationToggle
|
||||
| Action::DiffReviewToggle
|
||||
) {
|
||||
let name = entry.ex.expect("diff display action has an Ex command");
|
||||
let name = entry.ex.expect("diff action has an Ex command");
|
||||
if !args.is_empty() {
|
||||
self.set_message(format!("The {name} command takes no arguments"));
|
||||
return None;
|
||||
}
|
||||
if entry.action == Action::DiffReviewToggle
|
||||
&& self
|
||||
.file_picker
|
||||
.as_ref()
|
||||
.is_some_and(|picker| picker.focused)
|
||||
{
|
||||
self.set_message("Select a file in the comparison");
|
||||
return None;
|
||||
}
|
||||
if let Some(owner) = self.diff_owner.filter(|owner| *owner != self.pane_id) {
|
||||
return self.with_pane(owner, |editor| editor.execute_action(entry, args));
|
||||
}
|
||||
@@ -7036,6 +7056,14 @@ impl Editor {
|
||||
self.set_message("No comparison is open");
|
||||
return None;
|
||||
};
|
||||
if entry.action == Action::DiffReviewToggle {
|
||||
match diff.toggle_reviewed() {
|
||||
Ok(true) => self.set_message("File marked reviewed"),
|
||||
Ok(false) => self.set_message("File marked unreviewed"),
|
||||
Err(error) => self.set_message(error),
|
||||
}
|
||||
return None;
|
||||
}
|
||||
if entry.action == Action::DiffViewToggle {
|
||||
diff.set_view_mode(match diff.view_mode {
|
||||
ViewMode::Split => ViewMode::Unified,
|
||||
@@ -10285,6 +10313,7 @@ fn atomic_save(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::plugin::CompletionEdit;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
struct Directory(PathBuf);
|
||||
impl Directory {
|
||||
@@ -13479,6 +13508,161 @@ mod tests {
|
||||
assert_eq!(editor.lines(), ["beta"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_review_actions_preserve_readonly_text_history_and_parked_tabs() {
|
||||
let directory = Directory::new();
|
||||
let mut editor = editor();
|
||||
editor.execute("diff.review.toggle");
|
||||
assert_eq!(editor.message(), "No comparison is open");
|
||||
historical_diff(
|
||||
&mut editor,
|
||||
&directory.0,
|
||||
vec!["before".into(), "second".into()],
|
||||
vec!["after".into(), "second".into()],
|
||||
);
|
||||
editor
|
||||
.bind("normal", "ctrl+y", Some("core.diff.review.toggle".into()))
|
||||
.unwrap();
|
||||
let text = editor.lines().to_vec();
|
||||
let carets = editor.carets();
|
||||
let revision = editor.text_revision();
|
||||
let undo = editor.active.undo.len();
|
||||
let redo = editor.active.redo.len();
|
||||
let dirty = editor.dirty();
|
||||
assert!(editor.mutation_blocked());
|
||||
editor.handle(Input::Ctrl('y'));
|
||||
assert_eq!(editor.diff().unwrap().reviewed_files, BTreeSet::from([0]));
|
||||
editor.execute("diff.review.toggle unexpected");
|
||||
assert!(editor.message().contains("takes no arguments"));
|
||||
assert_eq!(editor.diff().unwrap().reviewed_files, BTreeSet::from([0]));
|
||||
editor.handle(Input::Ctrl('p'));
|
||||
type_text(&mut editor, "diff.review.toggle");
|
||||
editor.handle(Input::Enter);
|
||||
assert!(editor.diff().unwrap().reviewed_files.is_empty());
|
||||
assert_eq!(editor.lines(), text);
|
||||
assert_eq!(editor.carets(), carets);
|
||||
assert_eq!(editor.text_revision(), revision);
|
||||
assert_eq!(editor.active.undo.len(), undo);
|
||||
assert_eq!(editor.active.redo.len(), redo);
|
||||
assert_eq!(editor.dirty(), dirty);
|
||||
assert!(!directory.path("file.txt").exists());
|
||||
editor.handle(Input::Char('\t'));
|
||||
assert_eq!(editor.diff().unwrap().focus, Focus::After);
|
||||
assert!(editor.mutation_blocked());
|
||||
editor.execute(":diff.review.toggle");
|
||||
editor.execute("diff.view.toggle");
|
||||
editor.execute("diff.view.toggle");
|
||||
editor.execute("diff.orientation.toggle");
|
||||
let first_tab = editor.tab_id();
|
||||
editor.execute("tab.new");
|
||||
historical_diff(
|
||||
&mut editor,
|
||||
&directory.0,
|
||||
vec!["base".into()],
|
||||
vec!["different".into()],
|
||||
);
|
||||
assert!(editor.diff().unwrap().reviewed_files.is_empty());
|
||||
editor.execute("tab.previous");
|
||||
assert_eq!(editor.tab_id(), first_tab);
|
||||
assert_eq!(editor.diff().unwrap().reviewed_files, BTreeSet::from([0]));
|
||||
editor.execute("diff.close");
|
||||
historical_diff(
|
||||
&mut editor,
|
||||
&directory.0,
|
||||
vec!["before".into()],
|
||||
vec!["after".into()],
|
||||
);
|
||||
assert!(editor.diff().unwrap().reviewed_files.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_review_files_action_targets_highlight_without_stealing_filter_text() {
|
||||
let directory = Directory::new();
|
||||
fs::write(directory.path("z.txt"), "z").unwrap();
|
||||
fs::write(directory.path("a.txt"), "a").unwrap();
|
||||
let mut editor = editor();
|
||||
working_diff(&mut editor, &directory.0, &["z.txt", "a.txt"]);
|
||||
editor
|
||||
.bind("normal", "ctrl+y", Some("core.diff.review.toggle".into()))
|
||||
.unwrap();
|
||||
editor
|
||||
.bind("normal", "Q", Some("core.diff.review.toggle".into()))
|
||||
.unwrap();
|
||||
let files = editor.diff.as_mut().unwrap().files.as_mut().unwrap();
|
||||
files.set_view(FileView::List);
|
||||
files.reveal_path(Path::new("a.txt"));
|
||||
editor.handle(Input::Ctrl('y'));
|
||||
assert_eq!(editor.diff().unwrap().file, 0);
|
||||
assert_eq!(editor.diff().unwrap().reviewed_files, BTreeSet::from([1]));
|
||||
editor.handle(Input::Char('Q'));
|
||||
assert_eq!(editor.file_picker().unwrap().query, "Q");
|
||||
assert_eq!(editor.diff().unwrap().reviewed_files, BTreeSet::from([1]));
|
||||
editor.handle(Input::Ctrl('u'));
|
||||
let files = editor.diff.as_mut().unwrap().files.as_mut().unwrap();
|
||||
files.set_view(FileView::Tree);
|
||||
files.start_search();
|
||||
editor.handle(Input::Char('Q'));
|
||||
assert_eq!(editor.file_picker().unwrap().query, "Q");
|
||||
assert_eq!(editor.diff().unwrap().reviewed_files, BTreeSet::from([1]));
|
||||
editor.handle(Input::Ctrl('y'));
|
||||
assert_eq!(editor.message(), "No changed file is selected");
|
||||
assert_eq!(editor.diff().unwrap().reviewed_files, BTreeSet::from([1]));
|
||||
editor.file_picker = Some(
|
||||
FilePicker::scan(
|
||||
directory.0.clone(),
|
||||
FilePresentation::Docked,
|
||||
FileView::List,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
editor.execute("diff.review.toggle");
|
||||
assert_eq!(editor.message(), "Select a file in the comparison");
|
||||
assert_eq!(editor.diff().unwrap().reviewed_files, BTreeSet::from([1]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn working_review_marks_survive_save_and_clear_only_the_edited_file() {
|
||||
let directory = Directory::new();
|
||||
let first = directory.path("first.txt");
|
||||
fs::write(&first, "base\n").unwrap();
|
||||
fs::write(directory.path("second.txt"), "second").unwrap();
|
||||
let mut editor = Editor::open(Some(first.clone()), vec![]).unwrap();
|
||||
working_diff(&mut editor, &directory.0, &["first.txt", "second.txt"]);
|
||||
editor.execute("diff.review.toggle");
|
||||
editor
|
||||
.diff
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.files
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.reveal_path(Path::new("second.txt"));
|
||||
editor.execute("diff.review.toggle");
|
||||
assert_eq!(
|
||||
editor.diff().unwrap().reviewed_files,
|
||||
BTreeSet::from([0, 1])
|
||||
);
|
||||
editor.handle(Input::BackTab);
|
||||
assert!(editor.diff_editing());
|
||||
editor.execute("w");
|
||||
assert_eq!(
|
||||
editor.diff().unwrap().reviewed_files,
|
||||
BTreeSet::from([0, 1])
|
||||
);
|
||||
type_text(&mut editor, "iX");
|
||||
editor.handle(Input::Escape);
|
||||
assert_eq!(editor.diff().unwrap().reviewed_files, BTreeSet::from([1]));
|
||||
editor.execute("diff.review.toggle");
|
||||
editor.execute("w");
|
||||
assert_eq!(
|
||||
editor.diff().unwrap().reviewed_files,
|
||||
BTreeSet::from([0, 1])
|
||||
);
|
||||
assert_eq!(fs::read_to_string(&first).unwrap(), "Xbase\n");
|
||||
editor.handle(Input::Char('u'));
|
||||
assert_eq!(editor.diff().unwrap().reviewed_files, BTreeSet::from([1]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_display_defaults_tabs_and_remaps_preserve_native_snapshot_carets() {
|
||||
let directory = Directory::new();
|
||||
|
||||
+136
-34
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user