Skip to main content

clankerdiff_core/
anchor.rs

1//! Stable identities for patch lines and review comments.
2
3use crate::{DiffSide, FileDiff, Fingerprint, PatchLine, PatchLineKind, RepoPath};
4use serde::{Deserialize, Serialize};
5
6const CONTEXT_RADIUS: usize = 2;
7
8/// A stable, serializable identity for a source line in a diff.
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct LineAnchor {
11    pub path: RepoPath,
12    pub side: DiffSide,
13    pub old_line_no: Option<usize>,
14    pub new_line_no: Option<usize>,
15    pub kind: PatchLineKind,
16    pub fingerprint: Fingerprint,
17    /// Digest of side, kind, and line content, used when nearby context moves.
18    pub content_fingerprint: Fingerprint,
19}
20
21impl LineAnchor {
22    /// Creates an anchor from a file and hunk line index, collecting local context.
23    #[must_use]
24    pub fn for_line(file: &FileDiff, side: DiffSide, hunk: usize, line: usize) -> Option<Self> {
25        let lines = &file.hunks.get(hunk)?.lines;
26        let anchored = lines.get(line)?;
27        anchored.line_number(side)?;
28        let start = line.saturating_sub(CONTEXT_RADIUS);
29        let end = (line + CONTEXT_RADIUS + 1).min(lines.len());
30        Some(Self {
31            path: file.path.clone(),
32            side,
33            old_line_no: anchored.old_line_no,
34            new_line_no: anchored.new_line_no,
35            kind: anchored.kind,
36            fingerprint: context_fingerprint(&file.path, side, anchored, &lines[start..end]),
37            content_fingerprint: Self::content_fingerprint_of(side, anchored),
38        })
39    }
40
41    #[must_use]
42    pub fn content_fingerprint_of(side: DiffSide, line: &PatchLine) -> Fingerprint {
43        Fingerprint::of([side.as_str(), line.kind.as_str(), line.text.as_ref()])
44    }
45
46    /// Returns the relevant line number for this side.
47    #[must_use]
48    pub const fn line_number(&self) -> Option<usize> {
49        match self.side {
50            DiffSide::Old => self.old_line_no,
51            DiffSide::New => self.new_line_no,
52        }
53    }
54
55    #[must_use]
56    pub fn addresses_same_side(&self, other: &Self) -> bool {
57        self.path == other.path && self.side == other.side
58    }
59}
60
61fn context_fingerprint(
62    path: &RepoPath,
63    side: DiffSide,
64    line: &PatchLine,
65    nearby: &[PatchLine],
66) -> Fingerprint {
67    Fingerprint::of(
68        [
69            path.as_str(),
70            side.as_str(),
71            line.text.as_ref(),
72            line.kind.as_str(),
73        ]
74        .into_iter()
75        .chain(nearby.iter().map(|context| context.text.as_ref())),
76    )
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::FileDiff;
83
84    #[test]
85    fn deterministic_and_side_sensitive() {
86        let file = FileDiff::from_texts("a.rs", "x\n", "y\n").unwrap();
87        let old = LineAnchor::for_line(&file, DiffSide::Old, 0, 0).unwrap();
88        let new = LineAnchor::for_line(&file, DiffSide::New, 0, 1).unwrap();
89        assert_ne!(old.fingerprint, new.fingerprint);
90        assert_eq!(
91            old,
92            LineAnchor::for_line(&file, DiffSide::Old, 0, 0).unwrap()
93        );
94        assert!(!old.addresses_same_side(&new));
95    }
96
97    #[test]
98    fn cheap_content_digest_matches_the_stored_one() {
99        let file = FileDiff::from_texts("a.rs", "x\n", "y\n").unwrap();
100        let anchor = LineAnchor::for_line(&file, DiffSide::New, 0, 1).unwrap();
101        let line = file.line(0, 1).unwrap();
102        assert_eq!(
103            anchor.content_fingerprint,
104            LineAnchor::content_fingerprint_of(DiffSide::New, line)
105        );
106    }
107
108    #[test]
109    fn rejects_lines_without_a_number_on_the_requested_side() {
110        let file = FileDiff::from_texts("a.rs", "x\n", "y\n").unwrap();
111        assert!(LineAnchor::for_line(&file, DiffSide::Old, 0, 1).is_none());
112        assert!(LineAnchor::for_line(&file, DiffSide::New, 9, 0).is_none());
113    }
114}