feat: add local diff review workflow

This commit is contained in:
Valdemar Lorenzen
2026-10-16 13:10:07 +02:00
parent 930735c537
commit 0446ec86c7
15 changed files with 1871 additions and 7 deletions
+79
View File
@@ -139,6 +139,8 @@ pub struct ChangedFile {
}
#[derive(Debug)]
pub struct Comparison {
/// Immutable comparison-context IDs; captured review text also freezes live edits.
pub revisions: [String; 2],
pub before: String,
pub after: String,
pub files: Vec<ChangedFile>,
@@ -273,6 +275,7 @@ pub struct DiffState {
pub loading: bool,
pub error: Option<String>,
pub comparison: Option<Comparison>,
pub local_review: crate::review::ReviewState,
pub comparison_id: Option<u64>,
pub reviewed_files: BTreeSet<usize>,
pub(crate) snapshot_roots: [Option<PathBuf>; 2],
@@ -330,6 +333,7 @@ impl DiffState {
loading: true,
error: None,
comparison: None,
local_review: Default::default(),
comparison_id: None,
reviewed_files: BTreeSet::new(),
files: None,
@@ -366,6 +370,7 @@ impl DiffState {
}
pub(crate) fn set_resolver_paths(&mut self, root: PathBuf, paths: Vec<PathBuf>, file: usize) {
let comparison = self.comparison.get_or_insert_with(|| Comparison {
revisions: Default::default(),
before: "Resolution".into(),
after: "Other version".into(),
files: Vec::new(),
@@ -1710,6 +1715,40 @@ impl DiffState {
lines
}
pub fn review_anchor_current(&self, anchor: &crate::review::Anchor) -> bool {
use crate::review::Side;
let Some(comparison) = &self.comparison else {
return false;
};
if self.local_review.document.root != comparison.root {
return false;
}
comparison.files.iter().enumerate().any(|(index, file)| {
if !file.loaded || file.notice.is_some() {
return false;
}
let (path, lines, eof) = match anchor.side {
Side::Before => (
file.old_path.as_deref().unwrap_or(&file.path),
&file.before,
file.before_eof,
),
Side::After => (
file.path.as_path(),
&file.after,
file.after_eof && !self.live_files.contains(&index),
),
};
anchor.matches_lines(
&comparison.revisions[anchor.side.index()],
path,
anchor.side,
lines,
eof,
)
})
}
pub(crate) fn corresponding_row(&self, from: Focus, to: Focus, row: usize) -> Option<usize> {
let aligned = self.rows.iter().position(|aligned| match from {
Focus::Before => aligned.before == Some(row),
@@ -3172,6 +3211,7 @@ mod tests {
fn comparison() -> Comparison {
Comparison {
revisions: ["abcdef123".into(), "working:snapshot".into()],
before: "abcdef123".into(),
after: "Working checkout".into(),
root: PathBuf::new(),
@@ -4159,6 +4199,7 @@ mod tests {
assert!(editor.apply_diff_result(
id,
Ok(Outcome::Comparison(Comparison {
revisions: ["working:snapshot".into(), "working:snapshot".into()],
before: "Working".into(),
after: "Working".into(),
files: vec![],
@@ -4190,6 +4231,7 @@ mod tests {
Category::Commits,
));
state.comparison = Some(Comparison {
revisions: ["base".into(), "working:snapshot".into()],
root: PathBuf::new(),
before: "base".into(),
after: "Working".into(),
@@ -4982,4 +5024,41 @@ mod tests {
));
assert!(editor.diff().unwrap().error.is_none());
}
#[test]
fn review_anchors_match_frozen_context_and_exact_live_eof_without_allocating() {
use crate::review::{Anchor, Side};
let mut state = live_state(vec!["old".into()], vec!["new".into()], true);
let anchor = Anchor {
revision: "working:snapshot".into(),
path: "file".into(),
side: Side::After,
start_line: 1,
end_line: 1,
content: "new\n".into(),
};
assert!(state.review_anchor_current(&anchor));
state.live_files.insert(0);
assert!(!state.review_anchor_current(&anchor));
state.comparison.as_mut().unwrap().files[0]
.after
.push(String::new());
assert!(state.review_anchor_current(&anchor));
state.comparison.as_mut().unwrap().files[0].after[0] = "changed".into();
assert!(!state.review_anchor_current(&anchor));
state.comparison.as_mut().unwrap().files[0].old_path = Some("original".into());
let before = Anchor {
revision: "base".into(),
path: "original".into(),
side: Side::Before,
content: "old\n".into(),
..anchor
};
assert!(state.review_anchor_current(&before));
state.comparison.as_mut().unwrap().files[0].loaded = false;
assert!(!state.review_anchor_current(&before));
state.comparison.as_mut().unwrap().files[0].loaded = true;
state.comparison.as_mut().unwrap().revisions[0] = "other".into();
assert!(!state.review_anchor_current(&before));
}
}