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]
19pub fn compute_diff_chunks<'a>(old: &'a str, new: &'a str) -> Vec<Chunk<'a>> {
20 if old == new {
21 return (!old.is_empty()).then_some(Chunk::Equal(old)).into_iter().collect();
22 }
23 let diff = TextDiff::configure()
24 .algorithm(similar::Algorithm::Myers)
25 .timeout(Duration::from_millis(200))
26 .diff_chars(old, new);
27 let mut chunks = Vec::new();
28 let mut old_offset = 0usize;
29 let mut new_offset = 0usize;
30 let mut run: Option<(ChangeTag, usize, usize)> = None;
31
32 for change in diff.iter_all_changes() {
33 let value = change.value();
34 let byte_len = value.len();
35 let (start, end) = match change.tag() {
36 ChangeTag::Equal | ChangeTag::Delete => (old_offset, old_offset.saturating_add(byte_len)),
37 ChangeTag::Insert => (new_offset, new_offset.saturating_add(byte_len)),
38 };
39 if let Some((tag, run_start, run_end)) = run {
40 if tag == change.tag() && run_end == start {
41 run = Some((tag, run_start, end));
42 } else {
43 push_chunk(&mut chunks, tag, run_start, run_end, old, new);
44 run = Some((change.tag(), start, end));
45 }
46 } else {
47 run = Some((change.tag(), start, end));
48 }
49 match change.tag() {
50 ChangeTag::Equal => {
51 old_offset = old_offset.saturating_add(byte_len);
52 new_offset = new_offset.saturating_add(byte_len);
53 }
54 ChangeTag::Delete => old_offset = old_offset.saturating_add(byte_len),
55 ChangeTag::Insert => new_offset = new_offset.saturating_add(byte_len),
56 }
57 }
58 if let Some((tag, start, end)) = run {
59 push_chunk(&mut chunks, tag, start, end, old, new);
60 }
61 chunks
62}
63
64fn push_chunk<'a>(chunks: &mut Vec<Chunk<'a>>, tag: ChangeTag, start: usize, end: usize, old: &'a str, new: &'a str) {
65 debug_assert!(start <= end, "chunk range must not be inverted");
70 let chunk = match tag {
71 ChangeTag::Equal => {
72 debug_assert!(old.is_char_boundary(start) && old.is_char_boundary(end));
73 Chunk::Equal(&old[start..end])
74 }
75 ChangeTag::Delete => {
76 debug_assert!(old.is_char_boundary(start) && old.is_char_boundary(end));
77 Chunk::Delete(&old[start..end])
78 }
79 ChangeTag::Insert => {
80 debug_assert!(new.is_char_boundary(start) && new.is_char_boundary(end));
81 Chunk::Insert(&new[start..end])
82 }
83 };
84 chunks.push(chunk);
85}
86pub fn annotate_word_level_diffs(lines: &mut [DiffDisplayLine]) {
88 annotate_word_level_diffs_with_timeout(lines, default_inline_timeout());
89}
90
91pub(crate) fn annotate_word_level_diffs_with_timeout(lines: &mut [DiffDisplayLine], timeout: Duration) {
92 if lines.iter().any(|line| line.text.as_bytes().contains(&0)) {
95 return;
96 }
97 let deadline = Instant::now().checked_add(timeout);
98 let mut index = 0usize;
99 while index < lines.len() {
100 if lines[index].kind != DiffDisplayKind::Deletion {
101 index += 1;
102 continue;
103 }
104 let delete_start = index;
105 while index < lines.len() && lines[index].kind == DiffDisplayKind::Deletion {
106 index += 1;
107 }
108 let insert_start = index;
109 while index < lines.len() && lines[index].kind == DiffDisplayKind::Addition {
110 index += 1;
111 }
112 let pair_count = (insert_start - delete_start).min(index - insert_start);
113 for offset in 0..pair_count {
114 if deadline.is_some_and(|limit| Instant::now() >= limit) {
115 return;
116 }
117 let (old_ranges, new_ranges) =
118 word_level_changed_ranges(&lines[delete_start + offset].text, &lines[insert_start + offset].text);
119 lines[delete_start + offset].changed = old_ranges;
120 lines[insert_start + offset].changed = new_ranges;
121 }
122 }
123}
124
125#[must_use]
135pub fn word_level_changed_ranges(old: &str, new: &str) -> (WordChangedRanges, WordChangedRanges) {
136 if old.is_empty() || new.is_empty() || old.len().saturating_add(new.len()) > 16_384 {
137 return (Vec::new(), Vec::new());
138 }
139 let diff = TextDiff::configure()
140 .algorithm(similar::Algorithm::Myers)
141 .timeout(Duration::from_millis(10))
142 .diff_unicode_words(old, new);
143 if diff.ratio() < 0.35 {
144 return (Vec::new(), Vec::new());
145 }
146 let mut old_offset = 0usize;
147 let mut new_offset = 0usize;
148 let mut old_ranges = Vec::new();
149 let mut new_ranges = Vec::new();
150 for change in diff.iter_all_changes() {
151 let length = change.value().len();
152 match change.tag() {
153 ChangeTag::Equal => {
154 old_offset = old_offset.saturating_add(length);
155 new_offset = new_offset.saturating_add(length);
156 }
157 ChangeTag::Delete => {
158 push_range(&mut old_ranges, old_offset, old_offset.saturating_add(length));
159 old_offset = old_offset.saturating_add(length);
160 }
161 ChangeTag::Insert => {
162 push_range(&mut new_ranges, new_offset, new_offset.saturating_add(length));
163 new_offset = new_offset.saturating_add(length);
164 }
165 }
166 }
167 (old_ranges, new_ranges)
168}
169
170fn push_range(ranges: &mut WordChangedRanges, start: usize, end: usize) {
171 debug_assert!(start <= end, "intraline range must not be inverted");
174 match ranges.last_mut() {
175 Some((_, prior_end)) if *prior_end == start => *prior_end = end,
176 _ => ranges.push((start, end)),
177 }
178}