clankerdiff_core/
anchor.rs1use crate::{DiffSide, FileDiff, Fingerprint, PatchLine, PatchLineKind, RepoPath};
4use serde::{Deserialize, Serialize};
5
6const CONTEXT_RADIUS: usize = 2;
7
8#[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 pub content_fingerprint: Fingerprint,
19}
20
21impl LineAnchor {
22 #[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 #[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}