feat: persist diff file review checkmarks

This commit is contained in:
2026-10-19 18:48:44 +02:00
parent fe039e75c8
commit ea04e30072
11 changed files with 655 additions and 61 deletions
+118 -16
View File
@@ -587,6 +587,8 @@ pub struct DiffState {
pub live_revision: Option<u64>,
pub follow_caret: bool,
live_files: BTreeSet<usize>,
/// Files edited since their frozen comparison was loaded; undo does not restore review marks.
modified_after: BTreeSet<usize>,
frozen_after: BTreeMap<usize, (Vec<String>, bool)>,
context_lines: usize,
expand_lines: usize,
@@ -648,6 +650,7 @@ impl DiffState {
live_revision: None,
follow_caret: false,
live_files: BTreeSet::new(),
modified_after: BTreeSet::new(),
frozen_after: BTreeMap::new(),
context_lines: 10,
expand_lines: 15,
@@ -1005,6 +1008,7 @@ impl DiffState {
self.snapshot_roots = [None, None];
self.frozen_after.clear();
self.live_files.clear();
self.modified_after.clear();
self.live_buffer = None;
self.live_revision = None;
self.refresh_view = None;
@@ -1082,6 +1086,7 @@ impl DiffState {
}
Ok(Outcome::Comparison(comparison)) if expected == Some(RequestKind::Comparison) => {
self.live_files.clear();
self.modified_after.clear();
self.frozen_after.clear();
self.refresh_view = None;
self.reviewed_files.clear();
@@ -1777,14 +1782,82 @@ impl DiffState {
} 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 {
let file = comparison
.files
.get(index)
.ok_or("No changed file is selected")?;
let reviewed = !self.reviewed_files.remove(&index);
if reviewed {
self.reviewed_files.insert(index);
Ok(true)
}
// Resolver comparisons have no immutable revision identity or local review.
if comparison
.revisions
.iter()
.all(|revision| !revision.is_empty())
{
if self.local_review.document.root.as_os_str().is_empty() {
self.local_review.document = crate::review::Review::new(comparison.root.clone());
}
let records = &mut self.local_review.document.reviewed_files;
records.retain(|record| {
record.revisions != comparison.revisions || record.path != file.path
});
if reviewed {
records.push(crate::review::ReviewedFile {
revisions: comparison.revisions.clone(),
path: file.path.clone(),
old_path: file.old_path.clone(),
after: self
.live_files
.contains(&index)
.then(|| file.after.join("\n")),
});
}
self.local_review.dirty = true;
}
Ok(reviewed)
}
pub(crate) fn restore_reviewed_files(&mut self) {
self.reviewed_files.clear();
let Some(comparison) = self.comparison.as_ref() else {
return;
};
if self.local_review.document.root != comparison.root {
return;
}
let records: BTreeMap<_, _> = self
.local_review
.document
.reviewed_files
.iter()
.filter(|record| record.revisions == comparison.revisions)
.map(|record| (&record.path, record))
.collect();
for (index, file) in comparison.files.iter().enumerate() {
let Some(record) = records
.get(&file.path)
.filter(|record| record.old_path == file.old_path)
else {
continue;
};
let matches = match &record.after {
None => !self.modified_after.contains(&index),
Some(text) => {
file.loaded
&& (text
.split('\n')
.eq(file.after.iter().map(String::as_str).chain(
((!self.live_files.contains(&index) && file.after_eof)
|| file.after.is_empty())
.then_some(""),
)))
}
};
if matches {
self.reviewed_files.insert(index);
}
}
}
@@ -2173,15 +2246,44 @@ impl DiffState {
}
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);
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.modified_after.insert(self.file);
}
let comparison = self.comparison.as_ref().unwrap();
let record = (self.local_review.document.root == comparison.root)
.then(|| {
self.local_review
.document
.reviewed_files
.iter()
.position(|record| {
record.revisions == comparison.revisions
&& record.path == file.path
&& record.old_path == file.old_path
})
})
.flatten();
let matches = record
.and_then(|position| {
self.local_review.document.reviewed_files[position]
.after
.as_ref()
})
.map(|text| text.split('\n').eq(lines.iter().map(String::as_str)));
if matches == Some(true) {
self.reviewed_files.insert(self.file);
}
if self.reviewed_files.contains(&self.file) && !matches.unwrap_or(unchanged) {
self.reviewed_files.remove(&self.file);
if let Some(position) = record {
self.local_review.document.reviewed_files.remove(position);
self.local_review.dirty = true;
}
}
if was_live && changed_lines.0 == changed_lines.1 && changed_lines.1 == changed_lines.2 {