1use similar::TextDiff;
6
7use crate::diff::intraline;
8use crate::model::{DiffLine, FileDiff, Hunk, LineKind};
9
10const MIN_SIMILARITY: f32 = 0.4;
12
13pub fn enrich_file(file: &mut FileDiff) {
17 for hunk in &mut file.hunks {
18 enrich_hunk(hunk);
19 }
20}
21
22fn enrich_hunk(hunk: &mut Hunk) {
23 let mut i = 0;
24 while i < hunk.lines.len() {
25 if kind_at(&hunk.lines, i) != Some(LineKind::Deleted) {
26 i += 1;
27 continue;
28 }
29 let del_start = i;
30 while kind_at(&hunk.lines, i) == Some(LineKind::Deleted) {
31 i += 1;
32 }
33 let add_start = i;
34 while kind_at(&hunk.lines, i) == Some(LineKind::Added) {
35 i += 1;
36 }
37 let pairs = (add_start - del_start).min(i - add_start);
38 for p in 0..pairs {
39 let (del_idx, add_idx) = (del_start + p, add_start + p);
40 let (Some(old), Some(new)) = (hunk.lines.get(del_idx), hunk.lines.get(add_idx)) else {
41 continue;
42 };
43 if similarity(&old.text, &new.text) < MIN_SIMILARITY {
44 continue;
45 }
46 let (old_emphasis, new_emphasis) = intraline(&old.text, &new.text);
47 if let Some(line) = hunk.lines.get_mut(del_idx) {
48 line.emphasis = old_emphasis;
49 }
50 if let Some(line) = hunk.lines.get_mut(add_idx) {
51 line.emphasis = new_emphasis;
52 }
53 }
54 }
55}
56
57fn kind_at(lines: &[DiffLine], i: usize) -> Option<LineKind> {
58 lines.get(i).map(|l| l.kind)
59}
60
61fn similarity(old: &str, new: &str) -> f32 {
62 if old.is_empty() && new.is_empty() {
63 return 1.0;
64 }
65 TextDiff::from_graphemes(old, new).ratio()
66}
67
68#[cfg(test)]
69mod tests {
70 use crate::model::{DiffLine, HunkId, LineKind};
71
72 use super::*;
73
74 fn hunk(lines: Vec<(LineKind, &str)>) -> Hunk {
75 Hunk {
76 id: HunkId("test".into()),
77 old_start: 1,
78 old_lines: 1,
79 new_start: 1,
80 new_lines: 1,
81 context: String::new(),
82 lines: lines
83 .into_iter()
84 .map(|(k, t)| DiffLine::new(k, None, None, t.to_owned()))
85 .collect(),
86 }
87 }
88
89 #[test]
90 fn similar_pair_gets_emphasis_on_both_sides() {
91 let mut h = hunk(vec![
92 (LineKind::Context, "def f():"),
93 (LineKind::Deleted, " if x < y:"),
94 (LineKind::Added, " if x <= y:"),
95 ]);
96 enrich_hunk(&mut h);
97 assert!(h.lines[1].emphasis.is_empty()); assert_eq!(h.lines[2].emphasis, vec![10..11]);
99 }
100
101 #[test]
102 fn dissimilar_pair_gets_no_emphasis() {
103 let mut h = hunk(vec![
104 (LineKind::Deleted, "totally_different_thing()"),
105 (LineKind::Added, "x = 1"),
106 ]);
107 enrich_hunk(&mut h);
108 assert!(h.lines[0].emphasis.is_empty());
109 assert!(h.lines[1].emphasis.is_empty());
110 }
111
112 #[test]
113 fn unbalanced_runs_pair_prefix_only() {
114 let mut h = hunk(vec![
115 (LineKind::Deleted, "alpha line one"),
116 (LineKind::Deleted, "beta line two"),
117 (LineKind::Added, "alpha line ONE"),
118 ]);
119 enrich_hunk(&mut h);
120 assert!(!h.lines[2].emphasis.is_empty()); assert!(h.lines[1].emphasis.is_empty()); }
123
124 #[test]
125 fn separate_runs_pair_independently() {
126 let mut h = hunk(vec![
127 (LineKind::Deleted, "first old line"),
128 (LineKind::Added, "first new line"),
129 (LineKind::Context, "middle"),
130 (LineKind::Deleted, "second old line"),
131 (LineKind::Added, "second new line"),
132 ]);
133 enrich_hunk(&mut h);
134 assert!(!h.lines[0].emphasis.is_empty());
135 assert!(!h.lines[1].emphasis.is_empty());
136 assert!(!h.lines[3].emphasis.is_empty());
137 assert!(!h.lines[4].emphasis.is_empty());
138 }
139}