1use std::time::{Duration, Instant};
4
5use similar::{ChangeTag, TextDiff};
6
7use super::types::{Chunk, DiffDisplayKind, DiffDisplayLine, WordChangedRanges, default_inline_timeout};
8
9#[must_use]
11pub fn compute_diff_chunks<'a>(old: &'a str, new: &'a str) -> Vec<Chunk<'a>> {
12 if old == new {
13 return (!old.is_empty()).then_some(Chunk::Equal(old)).into_iter().collect();
14 }
15 let diff = TextDiff::configure()
16 .algorithm(similar::Algorithm::Myers)
17 .timeout(Duration::from_millis(200))
18 .diff_chars(old, new);
19 let mut chunks = Vec::new();
20 let mut old_offset = 0usize;
21 let mut new_offset = 0usize;
22 let mut run: Option<(ChangeTag, usize, usize)> = None;
23
24 for change in diff.iter_all_changes() {
25 let value = change.value();
26 let byte_len = value.len();
27 let (start, end) = match change.tag() {
28 ChangeTag::Equal | ChangeTag::Delete => (old_offset, old_offset.saturating_add(byte_len)),
29 ChangeTag::Insert => (new_offset, new_offset.saturating_add(byte_len)),
30 };
31 if let Some((tag, run_start, run_end)) = run {
32 if tag == change.tag() && run_end == start {
33 run = Some((tag, run_start, end));
34 } else {
35 push_chunk(&mut chunks, tag, run_start, run_end, old, new);
36 run = Some((change.tag(), start, end));
37 }
38 } else {
39 run = Some((change.tag(), start, end));
40 }
41 match change.tag() {
42 ChangeTag::Equal => {
43 old_offset = old_offset.saturating_add(byte_len);
44 new_offset = new_offset.saturating_add(byte_len);
45 }
46 ChangeTag::Delete => old_offset = old_offset.saturating_add(byte_len),
47 ChangeTag::Insert => new_offset = new_offset.saturating_add(byte_len),
48 }
49 }
50 if let Some((tag, start, end)) = run {
51 push_chunk(&mut chunks, tag, start, end, old, new);
52 }
53 chunks
54}
55
56fn push_chunk<'a>(chunks: &mut Vec<Chunk<'a>>, tag: ChangeTag, start: usize, end: usize, old: &'a str, new: &'a str) {
57 let chunk = match tag {
58 ChangeTag::Equal => Chunk::Equal(&old[start..end]),
59 ChangeTag::Delete => Chunk::Delete(&old[start..end]),
60 ChangeTag::Insert => Chunk::Insert(&new[start..end]),
61 };
62 chunks.push(chunk);
63}
64pub fn annotate_word_level_diffs(lines: &mut [DiffDisplayLine]) {
66 annotate_word_level_diffs_with_timeout(lines, default_inline_timeout());
67}
68
69pub(crate) fn annotate_word_level_diffs_with_timeout(lines: &mut [DiffDisplayLine], timeout: Duration) {
70 if lines.iter().any(|line| line.text.as_bytes().contains(&0)) {
73 return;
74 }
75 let deadline = Instant::now().checked_add(timeout);
76 let mut index = 0usize;
77 while index < lines.len() {
78 if lines[index].kind != DiffDisplayKind::Deletion {
79 index += 1;
80 continue;
81 }
82 let delete_start = index;
83 while index < lines.len() && lines[index].kind == DiffDisplayKind::Deletion {
84 index += 1;
85 }
86 let insert_start = index;
87 while index < lines.len() && lines[index].kind == DiffDisplayKind::Addition {
88 index += 1;
89 }
90 let pair_count = (insert_start - delete_start).min(index - insert_start);
91 for offset in 0..pair_count {
92 if deadline.is_some_and(|limit| Instant::now() >= limit) {
93 return;
94 }
95 let (old_ranges, new_ranges) =
96 word_level_changed_ranges(&lines[delete_start + offset].text, &lines[insert_start + offset].text);
97 lines[delete_start + offset].changed = old_ranges;
98 lines[insert_start + offset].changed = new_ranges;
99 }
100 }
101}
102
103#[must_use]
105pub fn word_level_changed_ranges(old: &str, new: &str) -> (WordChangedRanges, WordChangedRanges) {
106 if old.is_empty() || new.is_empty() || old.len().saturating_add(new.len()) > 16_384 {
107 return (Vec::new(), Vec::new());
108 }
109 let diff = TextDiff::configure()
110 .algorithm(similar::Algorithm::Myers)
111 .timeout(Duration::from_millis(10))
112 .diff_unicode_words(old, new);
113 if diff.ratio() < 0.35 {
114 return (Vec::new(), Vec::new());
115 }
116 let mut old_offset = 0usize;
117 let mut new_offset = 0usize;
118 let mut old_ranges = Vec::new();
119 let mut new_ranges = Vec::new();
120 for change in diff.iter_all_changes() {
121 let length = change.value().len();
122 match change.tag() {
123 ChangeTag::Equal => {
124 old_offset += length;
125 new_offset += length;
126 }
127 ChangeTag::Delete => {
128 push_range(&mut old_ranges, old_offset, old_offset + length);
129 old_offset += length;
130 }
131 ChangeTag::Insert => {
132 push_range(&mut new_ranges, new_offset, new_offset + length);
133 new_offset += length;
134 }
135 }
136 }
137 (old_ranges, new_ranges)
138}
139
140fn push_range(ranges: &mut WordChangedRanges, start: usize, end: usize) {
141 match ranges.last_mut() {
142 Some((_, prior_end)) if *prior_end == start => *prior_end = end,
143 _ => ranges.push((start, end)),
144 }
145}