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