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(
|
||||
|
||||
Reference in New Issue
Block a user