1#![forbid(unsafe_code)]
46
47use std::ops::Range;
48
49use serde::{Deserialize, Serialize};
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
53#[serde(rename_all = "snake_case")]
54pub enum Op {
55 Same,
57 Removed,
59 Added,
61 Locked,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
68pub struct Segment {
69 pub op: Op,
70 pub text: String,
71}
72
73pub fn diff_words(before: &str, after: &str) -> Vec<Segment> {
75 diff_words_locked(before, after, &[], &[])
76}
77
78pub fn diff_words_locked(
82 before: &str,
83 after: &str,
84 before_locked: &[Range<usize>],
85 after_locked: &[Range<usize>],
86) -> Vec<Segment> {
87 let a = tokenize(before, before_locked);
88 let b = tokenize(after, after_locked);
89 let a_keys: Vec<&str> = a.iter().map(|t| t.core).collect();
90 let b_keys: Vec<&str> = b.iter().map(|t| t.core).collect();
91
92 let mut edits = Vec::with_capacity(a.len().max(b.len()));
93 Myers::new(&a_keys, &b_keys).run(&mut edits);
94
95 let mut ordered = Vec::with_capacity(edits.len());
98 let mut added = Vec::new();
99 for edit in edits {
100 match edit {
101 Edit::Added(_) => added.push(edit),
102 Edit::Removed(_) => ordered.push(edit),
103 Edit::Same(..) => {
104 ordered.append(&mut added);
105 ordered.push(edit);
106 }
107 }
108 }
109 ordered.append(&mut added);
110
111 let mut out: Vec<Segment> = Vec::new();
112 for edit in ordered {
113 let (op, text) = match edit {
114 Edit::Same(i, j) => {
115 let op = if a[i].locked && b[j].locked {
116 Op::Locked
117 } else {
118 Op::Same
119 };
120 (op, b[j].text())
121 }
122 Edit::Removed(i) => (Op::Removed, a[i].text()),
123 Edit::Added(j) => (Op::Added, b[j].text()),
124 };
125 match out.last_mut() {
126 Some(last) if last.op == op && op != Op::Locked => last.text.push_str(text),
127 _ => out.push(Segment {
128 op,
129 text: text.to_owned(),
130 }),
131 }
132 }
133 out
134}
135
136#[derive(Debug)]
138struct Token<'a> {
139 full: &'a str,
141 core: &'a str,
142 locked: bool,
143}
144
145impl<'a> Token<'a> {
146 fn text(&self) -> &'a str {
147 self.full
148 }
149}
150
151fn is_joiner(c: char) -> bool {
152 matches!(c, '\'' | '’' | '-' | '.' | ',' | '%')
153}
154
155fn tokenize<'a>(text: &'a str, locked: &[Range<usize>]) -> Vec<Token<'a>> {
156 let mut ranges: Vec<Range<usize>> = locked
157 .iter()
158 .filter(|r| {
159 r.start < r.end
160 && r.end <= text.len()
161 && text.is_char_boundary(r.start)
162 && text.is_char_boundary(r.end)
163 })
164 .cloned()
165 .collect();
166 ranges.sort_by_key(|r| r.start);
167 let mut kept: Vec<Range<usize>> = Vec::with_capacity(ranges.len());
168 for r in ranges {
169 if kept.last().is_none_or(|k| k.end <= r.start) {
170 kept.push(r);
171 }
172 }
173 let mut locked = kept.into_iter().peekable();
174
175 let mut tokens = Vec::new();
176 let mut i = 0;
177 let lead = text.len() - text.trim_start().len();
179 if lead > 0 {
180 tokens.push(Token {
181 full: &text[..lead],
182 core: "",
183 locked: false,
184 });
185 i = lead;
186 }
187 while i < text.len() {
188 let (end, is_locked) = if let Some(r) = locked.next_if(|r| r.start <= i) {
189 (r.end.max(i + 1).min(text.len()), true)
192 } else {
193 let next_lock = locked.peek().map_or(text.len(), |r| r.start);
194 (word_end(text, i, next_lock), false)
195 };
196 let end = end.max(i + text[i..].chars().next().map_or(1, char::len_utf8));
197 let ws = text[end..].len() - text[end..].trim_start().len();
198 let ws_end = locked
199 .peek()
200 .map_or(end + ws, |r| (end + ws).min(r.start.max(end)));
201 tokens.push(Token {
202 full: &text[i..ws_end],
203 core: &text[i..end],
204 locked: is_locked,
205 });
206 i = ws_end;
207 }
208 tokens
209}
210
211fn word_end(text: &str, start: usize, limit: usize) -> usize {
214 let mut chars = text[start..limit].char_indices().peekable();
215 let Some((_, first)) = chars.next() else {
216 return start;
217 };
218 if !first.is_alphanumeric() {
219 return start + first.len_utf8();
220 }
221 let mut end = start + first.len_utf8();
222 while let Some((at, c)) = chars.next() {
223 if c.is_alphanumeric() {
224 end = start + at + c.len_utf8();
225 } else if is_joiner(c) && end == start + at {
226 let followed = chars.peek().is_some_and(|(_, n)| n.is_alphanumeric());
229 if followed || c == '%' {
230 end = start + at + c.len_utf8();
231 } else {
232 break;
233 }
234 } else {
235 break;
236 }
237 }
238 end
239}
240
241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
242enum Edit {
243 Same(usize, usize),
244 Removed(usize),
245 Added(usize),
246}
247
248struct Myers<'k> {
250 a: &'k [&'k str],
251 b: &'k [&'k str],
252 forward: Vec<usize>,
253 backward: Vec<usize>,
254}
255
256struct Snake {
259 x: usize,
260 y: usize,
261 u: usize,
262 v: usize,
263}
264
265impl<'k> Myers<'k> {
266 fn new(a: &'k [&'k str], b: &'k [&'k str]) -> Self {
267 let size = 2 * (a.len() + b.len()) + 4;
268 Self {
269 a,
270 b,
271 forward: vec![0; size],
272 backward: vec![0; size],
273 }
274 }
275
276 fn run(mut self, out: &mut Vec<Edit>) {
277 self.compare(0..self.a.len(), 0..self.b.len(), out);
278 }
279
280 fn compare(&mut self, mut a: Range<usize>, mut b: Range<usize>, out: &mut Vec<Edit>) {
281 while a.start < a.end && b.start < b.end && self.a[a.start] == self.b[b.start] {
282 out.push(Edit::Same(a.start, b.start));
283 a.start += 1;
284 b.start += 1;
285 }
286 let mut suffix = 0;
287 while a.start < a.end - suffix
288 && b.start < b.end - suffix
289 && self.a[a.end - suffix - 1] == self.b[b.end - suffix - 1]
290 {
291 suffix += 1;
292 }
293 a.end -= suffix;
294 b.end -= suffix;
295
296 if a.is_empty() {
297 out.extend(b.clone().map(Edit::Added));
298 } else if b.is_empty() {
299 out.extend(a.clone().map(Edit::Removed));
300 } else {
301 let snake = self.middle_snake(a.clone(), b.clone());
302 self.compare(a.start..a.start + snake.x, b.start..b.start + snake.y, out);
303 for k in 0..snake.u - snake.x {
304 out.push(Edit::Same(a.start + snake.x + k, b.start + snake.y + k));
305 }
306 self.compare(a.start + snake.u..a.end, b.start + snake.v..b.end, out);
307 }
308
309 for k in (1..=suffix).rev() {
310 out.push(Edit::Same(a.end + suffix - k, b.end + suffix - k));
311 }
312 }
313
314 #[allow(
319 clippy::cast_possible_wrap,
320 clippy::cast_sign_loss,
321 clippy::many_single_char_names
322 )]
323 fn middle_snake(&mut self, a: Range<usize>, b: Range<usize>) -> Snake {
324 let n = a.len() as isize;
325 let m = b.len() as isize;
326 let delta = n - m;
327 let odd = delta % 2 != 0;
328 let max = (n + m + 1) / 2;
329 let offset = max + 1;
330 let idx = |k: isize| (k + offset) as usize;
331
332 self.forward[idx(1)] = 0;
333 self.backward[idx(1)] = 0;
334 let fwd_eq =
335 |s: &Self, x: isize, y: isize| s.a[a.start + x as usize] == s.b[b.start + y as usize];
336 let bwd_eq = |s: &Self, x: isize, y: isize| {
337 s.a[a.end - 1 - x as usize] == s.b[b.end - 1 - y as usize]
338 };
339
340 for d in 0..=max {
341 let mut k = -d;
342 while k <= d {
343 let mut x =
344 if k == -d || (k != d && self.forward[idx(k - 1)] < self.forward[idx(k + 1)]) {
345 self.forward[idx(k + 1)] as isize
346 } else {
347 self.forward[idx(k - 1)] as isize + 1
348 };
349 let mut y = x - k;
350 let (x0, y0) = (x, y);
351 while x < n && y < m && fwd_eq(self, x, y) {
352 x += 1;
353 y += 1;
354 }
355 self.forward[idx(k)] = x as usize;
356 let c = delta - k;
357 if odd && c > -d && c < d && x + self.backward[idx(c)] as isize >= n {
358 return Snake {
359 x: x0 as usize,
360 y: y0 as usize,
361 u: x as usize,
362 v: y as usize,
363 };
364 }
365 k += 2;
366 }
367
368 let mut k = -d;
369 while k <= d {
370 let mut x = if k == -d
371 || (k != d && self.backward[idx(k - 1)] < self.backward[idx(k + 1)])
372 {
373 self.backward[idx(k + 1)] as isize
374 } else {
375 self.backward[idx(k - 1)] as isize + 1
376 };
377 let mut y = x - k;
378 let (x0, y0) = (x, y);
379 while x < n && y < m && bwd_eq(self, x, y) {
380 x += 1;
381 y += 1;
382 }
383 self.backward[idx(k)] = x as usize;
384 let c = delta - k;
385 if !odd && c >= -d && c <= d && x + self.forward[idx(c)] as isize >= n {
386 return Snake {
387 x: (n - x) as usize,
388 y: (m - y) as usize,
389 u: (n - x0) as usize,
390 v: (m - y0) as usize,
391 };
392 }
393 k += 2;
394 }
395 }
396 unreachable!("two non-empty sequences always have a middle snake within (n + m + 1) / 2")
397 }
398}
399
400#[cfg(test)]
401mod tests;