1use std::collections::HashMap;
12use std::hash::Hash;
13use std::ops::Range;
14
15pub const COST_LIMIT: usize = 1024;
17
18#[derive(Clone, Debug, PartialEq, Eq, Hash)]
23pub enum Op {
24 Equal {
26 old: Range<usize>,
27 new: Range<usize>,
28 },
29 Delete {
31 old: Range<usize>,
32 new: Range<usize>,
33 },
34 Insert {
36 old: Range<usize>,
37 new: Range<usize>,
38 },
39}
40
41impl Op {
42 pub fn old(&self) -> Range<usize> {
44 match self {
45 Op::Equal { old, .. } | Op::Delete { old, .. } | Op::Insert { old, .. } => old.clone(),
46 }
47 }
48 pub fn new_range(&self) -> Range<usize> {
50 match self {
51 Op::Equal { new, .. } | Op::Delete { new, .. } | Op::Insert { new, .. } => new.clone(),
52 }
53 }
54 pub fn is_equal(&self) -> bool {
56 matches!(self, Op::Equal { .. })
57 }
58}
59
60pub fn diff_slices<T: Hash + Eq>(old: &[T], new: &[T]) -> Vec<Op> {
62 let mut ids: HashMap<&T, u32> = HashMap::new();
63 let mut intern = |item| {
64 let next = ids.len() as u32;
65 *ids.entry(item).or_insert(next)
66 };
67 let a: Vec<u32> = old.iter().map(&mut intern).collect();
68 let b: Vec<u32> = new.iter().map(&mut intern).collect();
69 let mut raw = Vec::new();
70 let max = (a.len() + b.len()).div_ceil(2) + 1;
71 let mut vf = vec![0usize; 2 * max + 2];
72 let mut vb = vec![0usize; 2 * max + 2];
73 conquer(&a, 0..a.len(), &b, 0..b.len(), &mut vf, &mut vb, &mut raw);
74 normalize(raw)
75}
76
77pub fn diff_lines(old: &[&str], new: &[&str]) -> Vec<Op> {
80 diff_slices(old, new)
81}
82
83pub fn tokenize(text: &str) -> Vec<Range<usize>> {
87 #[derive(PartialEq)]
88 enum Class {
89 Word,
90 Space,
91 Other,
92 }
93 let class = |c: char| {
94 if c.is_alphanumeric() || c == '_' {
95 Class::Word
96 } else if c.is_whitespace() {
97 Class::Space
98 } else {
99 Class::Other
100 }
101 };
102 let mut out: Vec<Range<usize>> = Vec::new();
103 let mut last: Option<Class> = None;
104 for (i, c) in text.char_indices() {
105 let k = class(c);
106 let joins = k != Class::Other && last.as_ref() == Some(&k);
107 match out.last_mut() {
108 Some(range) if joins => range.end = i + c.len_utf8(),
109 _ => out.push(i..i + c.len_utf8()),
110 }
111 last = Some(k);
112 }
113 out
114}
115
116fn diff_tokens(old: &str, new: &str, a: Vec<Range<usize>>, b: Vec<Range<usize>>) -> Vec<Op> {
117 let at = |tokens: &[Range<usize>], text: &str, i: usize| {
118 tokens.get(i).map_or(text.len(), |range| range.start)
119 };
120 let old_items: Vec<&str> = a.iter().map(|r| &old[r.clone()]).collect();
121 let new_items: Vec<&str> = b.iter().map(|r| &new[r.clone()]).collect();
122 diff_slices(&old_items, &new_items)
123 .into_iter()
124 .map(|op| {
125 let (o, n) = (op.old(), op.new_range());
126 let old = at(&a, old, o.start)..at(&a, old, o.end);
127 let new = at(&b, new, n.start)..at(&b, new, n.end);
128 match op {
129 Op::Equal { .. } => Op::Equal { old, new },
130 Op::Delete { .. } => Op::Delete { old, new },
131 Op::Insert { .. } => Op::Insert { old, new },
132 }
133 })
134 .collect()
135}
136
137pub fn diff_words(old: &str, new: &str) -> Vec<Op> {
139 diff_tokens(old, new, tokenize(old), tokenize(new))
140}
141
142pub fn diff_chars(old: &str, new: &str) -> Vec<Op> {
144 let chars = |s: &str| {
145 s.char_indices()
146 .map(|(i, c)| i..i + c.len_utf8())
147 .collect::<Vec<_>>()
148 };
149 diff_tokens(old, new, chars(old), chars(new))
150}
151
152#[derive(Clone, Copy, PartialEq, Eq, Debug)]
153pub(crate) enum Tag {
154 Equal,
155 Delete,
156 Insert,
157}
158
159type Raw = (Tag, Range<usize>, Range<usize>);
160
161fn push(out: &mut Vec<Raw>, tag: Tag, old: Range<usize>, new: Range<usize>) {
162 if old.is_empty() && new.is_empty() {
163 return;
164 }
165 out.push((tag, old, new));
166}
167
168fn conquer(
169 a: &[u32],
170 mut ar: Range<usize>,
171 b: &[u32],
172 mut br: Range<usize>,
173 vf: &mut [usize],
174 vb: &mut [usize],
175 out: &mut Vec<Raw>,
176) {
177 let prefix = a[ar.clone()]
179 .iter()
180 .zip(&b[br.clone()])
181 .take_while(|(x, y)| x == y)
182 .count();
183 push(
184 out,
185 Tag::Equal,
186 ar.start..ar.start + prefix,
187 br.start..br.start + prefix,
188 );
189 ar.start += prefix;
190 br.start += prefix;
191 let suffix = a[ar.clone()]
193 .iter()
194 .rev()
195 .zip(b[br.clone()].iter().rev())
196 .take_while(|(x, y)| x == y)
197 .count();
198 let tail = (ar.end - suffix..ar.end, br.end - suffix..br.end);
199 ar.end -= suffix;
200 br.end -= suffix;
201
202 if ar.is_empty() || br.is_empty() || disjoint(&a[ar.clone()], &b[br.clone()]) {
204 push(out, Tag::Delete, ar.clone(), br.start..br.start);
205 push(out, Tag::Insert, ar.end..ar.end, br.clone());
206 } else {
207 match middle_snake(a, ar.clone(), b, br.clone(), vf, vb) {
208 Some((x, y)) if (x, y) != (ar.start, br.start) && (x, y) != (ar.end, br.end) => {
209 conquer(a, ar.start..x, b, br.start..y, vf, vb, out);
210 conquer(a, x..ar.end, b, y..br.end, vf, vb, out);
211 }
212 _ => {
213 push(out, Tag::Delete, ar.clone(), br.start..br.start);
214 push(out, Tag::Insert, ar.end..ar.end, br.clone());
215 }
216 }
217 }
218 push(out, Tag::Equal, tail.0, tail.1);
219}
220
221fn disjoint(a: &[u32], b: &[u32]) -> bool {
223 if a.len() * b.len() <= 64 {
224 return !a.iter().any(|x| b.contains(x));
225 }
226 let set: std::collections::HashSet<u32> = b.iter().copied().collect();
227 !a.iter().any(|x| set.contains(x))
228}
229
230fn middle_snake(
234 a: &[u32],
235 ar: Range<usize>,
236 b: &[u32],
237 br: Range<usize>,
238 vf: &mut [usize],
239 vb: &mut [usize],
240) -> Option<(usize, usize)> {
241 let (n, m) = (ar.len() as isize, br.len() as isize);
242 let a = &a[ar.clone()];
243 let b = &b[br.clone()];
244 let delta = n - m;
245 let odd = delta & 1 == 1;
246 let max = (n + m + 1) / 2;
247 let offset = max + 1;
248 let idx = |k: isize| (k + offset) as usize;
249 vf[idx(1)] = 0;
250 vb[idx(1)] = 0;
251 for d in 0..=max {
252 let mut k = -d;
254 while k <= d {
255 let mut x = if k == -d || (k != d && vf[idx(k - 1)] < vf[idx(k + 1)]) {
256 vf[idx(k + 1)] as isize
257 } else {
258 vf[idx(k - 1)] as isize + 1
259 };
260 let mut y = x - k;
261 let (x0, y0) = (x, y);
262 while x < n && y < m && a[x as usize] == b[y as usize] {
263 x += 1;
264 y += 1;
265 }
266 vf[idx(k)] = x as usize;
267 let c = delta - k;
268 if odd && c >= -(d - 1) && c < d && x + vb[idx(c)] as isize >= n {
269 return Some((ar.start + x0 as usize, br.start + y0 as usize));
270 }
271 k += 2;
272 }
273 let mut k = -d;
275 while k <= d {
276 let mut x = if k == -d || (k != d && vb[idx(k - 1)] < vb[idx(k + 1)]) {
277 vb[idx(k + 1)] as isize
278 } else {
279 vb[idx(k - 1)] as isize + 1
280 };
281 let mut y = x - k;
282 while x < n && y < m && a[(n - x - 1) as usize] == b[(m - y - 1) as usize] {
283 x += 1;
284 y += 1;
285 }
286 vb[idx(k)] = x as usize;
287 let c = delta - k;
288 if !odd && c >= -d && c <= d && x + vf[idx(c)] as isize >= n {
289 return Some((ar.start + (n - x) as usize, br.start + (m - y) as usize));
290 }
291 k += 2;
292 }
293 if d as usize >= COST_LIMIT {
294 let mut best: Option<(isize, isize)> = None;
296 let mut k = -d;
297 while k <= d {
298 let x = (vf[idx(k)] as isize).min(n);
299 let y = x - k;
300 if (0..=m).contains(&y)
301 && x + y > 0
302 && x + y < n + m
303 && best.is_none_or(|(bx, by)| x + y > bx + by)
304 {
305 best = Some((x, y));
306 }
307 k += 2;
308 }
309 return best.map(|(x, y)| (ar.start + x as usize, br.start + y as usize));
310 }
311 }
312 None
313}
314
315fn normalize(raw: Vec<Raw>) -> Vec<Op> {
318 let mut merged: Vec<Raw> = Vec::new();
319 let mut pending_delete: Option<Raw> = None;
320 let mut pending_insert: Option<Raw> = None;
321 let flush = |merged: &mut Vec<Raw>, d: &mut Option<Raw>, i: &mut Option<Raw>| {
322 if let Some(d) = d.take() {
323 merged.push(d);
324 }
325 if let Some(mut i) = i.take() {
326 if let Some(last) = merged.last() {
328 if last.0 == Tag::Delete {
329 i.1 = last.1.end..last.1.end;
330 }
331 }
332 merged.push(i);
333 }
334 };
335 for (tag, old, new) in raw {
336 match tag {
337 Tag::Equal => {
338 flush(&mut merged, &mut pending_delete, &mut pending_insert);
339 match merged.last_mut() {
340 Some(last) if last.0 == Tag::Equal => {
341 last.1.end = old.end;
342 last.2.end = new.end;
343 }
344 _ => merged.push((tag, old, new)),
345 }
346 }
347 Tag::Delete => match &mut pending_delete {
348 Some(d) => d.1.end = old.end,
349 None => {
350 let new_at = pending_insert.as_ref().map_or(new.start, |i| i.2.start);
353 pending_delete = Some((tag, old, new_at..new_at));
354 }
355 },
356 Tag::Insert => match &mut pending_insert {
357 Some(i) => i.2.end = new.end,
358 None => pending_insert = Some((tag, old, new)),
359 },
360 }
361 }
362 flush(&mut merged, &mut pending_delete, &mut pending_insert);
363 merged
364 .into_iter()
365 .map(|(tag, old, new)| match tag {
366 Tag::Equal => Op::Equal { old, new },
367 Tag::Delete => Op::Delete { old, new },
368 Tag::Insert => Op::Insert { old, new },
369 })
370 .collect()
371}
372
373#[derive(Clone, Debug, PartialEq, Eq)]
375pub struct Hunk {
376 pub ops: Vec<Op>,
378}
379
380impl Hunk {
381 pub fn old_range(&self) -> Range<usize> {
383 let first = self.ops.first().map_or(0, |op| op.old().start);
384 let last = self.ops.last().map_or(first, |op| op.old().end);
385 first..last
386 }
387 pub fn new_range(&self) -> Range<usize> {
389 let first = self.ops.first().map_or(0, |op| op.new_range().start);
390 let last = self.ops.last().map_or(first, |op| op.new_range().end);
391 first..last
392 }
393 pub fn header(&self) -> String {
395 hunk_header(self.old_range(), self.new_range())
396 }
397}
398
399pub fn hunk_header(old: Range<usize>, new: Range<usize>) -> String {
402 fn part(r: Range<usize>) -> String {
403 match r.len() {
404 0 => format!("{},0", r.start),
405 1 => format!("{}", r.start + 1),
406 n => format!("{},{n}", r.start + 1),
407 }
408 }
409 format!("@@ -{} +{} @@", part(old), part(new))
410}
411
412pub fn group_hunks(ops: &[Op], context: usize) -> Vec<Hunk> {
415 let spans: Vec<(bool, Range<usize>, Range<usize>)> = ops
416 .iter()
417 .map(|op| (op.is_equal(), op.old(), op.new_range()))
418 .collect();
419 group_ranges(&spans, context)
420 .into_iter()
421 .map(|group| Hunk {
422 ops: group
423 .into_iter()
424 .map(|(index, old, new)| match &ops[index] {
425 Op::Equal { .. } => Op::Equal { old, new },
426 Op::Delete { .. } => Op::Delete { old, new },
427 Op::Insert { .. } => Op::Insert { old, new },
428 })
429 .collect(),
430 })
431 .collect()
432}
433
434pub(crate) type Grouped = (usize, Range<usize>, Range<usize>);
436
437pub(crate) fn group_ranges(
440 runs: &[(bool, Range<usize>, Range<usize>)],
441 context: usize,
442) -> Vec<Vec<Grouped>> {
443 if runs.iter().all(|r| r.0) {
445 return Vec::new();
446 }
447 let mut codes: Vec<Grouped> = runs
448 .iter()
449 .enumerate()
450 .map(|(i, (_, a, b))| (i, a.clone(), b.clone()))
451 .collect();
452 if let Some(first) = codes.first_mut() {
453 if runs[first.0].0 {
454 first.1.start = first.1.start.max(first.1.end.saturating_sub(context));
455 first.2.start = first.2.start.max(first.2.end.saturating_sub(context));
456 }
457 }
458 if let Some(last) = codes.last_mut() {
459 if runs[last.0].0 {
460 last.1.end = last.1.end.min(last.1.start + context);
461 last.2.end = last.2.end.min(last.2.start + context);
462 }
463 }
464 let mut hunks = Vec::new();
465 let mut group: Vec<Grouped> = Vec::new();
466 for (i, mut a, mut b) in codes {
467 if runs[i].0 && a.len() > context * 2 {
468 group.push((i, a.start..a.start + context, b.start..b.start + context));
469 hunks.push(std::mem::take(&mut group));
470 a.start = a.end - context;
471 b.start = b.end - context;
472 }
473 group.push((i, a, b));
474 }
475 if group.iter().any(|(i, _, _)| !runs[*i].0) {
476 hunks.push(group);
477 }
478 for hunk in &mut hunks {
479 hunk.retain(|(i, a, b)| !(runs[*i].0 && a.is_empty() && b.is_empty()));
480 }
481 hunks.retain(|hunk| hunk.iter().any(|(i, _, _)| !runs[*i].0));
482 hunks
483}
484
485pub(crate) fn split_lines_inclusive(text: &str) -> Vec<&str> {
487 text.split_inclusive('\n').collect()
488}
489
490pub(crate) fn strip_eol(line: &str) -> &str {
492 let line = line.strip_suffix('\n').unwrap_or(line);
493 line.strip_suffix('\r').unwrap_or(line)
494}
495
496#[derive(Clone, Debug)]
509pub struct TextDiff {
510 old: Vec<String>,
511 new: Vec<String>,
512 ops: Vec<Op>,
513 context: usize,
514}
515
516impl TextDiff {
517 pub fn new(old: &str, new: &str) -> Self {
520 let a = split_lines_inclusive(old);
521 let b = split_lines_inclusive(new);
522 let ops = diff_lines(&a, &b);
523 TextDiff {
524 old: a.into_iter().map(str::to_owned).collect(),
525 new: b.into_iter().map(str::to_owned).collect(),
526 ops,
527 context: 3,
528 }
529 }
530 pub fn context(mut self, lines: usize) -> Self {
532 self.context = lines;
533 self
534 }
535 pub fn context_lines(&self) -> usize {
537 self.context
538 }
539 pub fn ops(&self) -> &[Op] {
541 &self.ops
542 }
543 pub fn old_lines(&self) -> &[String] {
545 &self.old
546 }
547 pub fn new_lines(&self) -> &[String] {
549 &self.new
550 }
551 pub fn hunks(&self) -> Vec<Hunk> {
553 group_hunks(&self.ops, self.context)
554 }
555 pub fn is_equal(&self) -> bool {
557 self.ops.iter().all(Op::is_equal)
558 }
559 pub fn stats(&self) -> (usize, usize) {
561 self.ops.iter().fold((0, 0), |(a, r), op| match op {
562 Op::Insert { new, .. } => (a + new.len(), r),
563 Op::Delete { old, .. } => (a, r + old.len()),
564 Op::Equal { .. } => (a, r),
565 })
566 }
567 pub fn unified(&self, old_name: &str, new_name: &str) -> String {
570 let hunks = self.hunks();
571 if hunks.is_empty() {
572 return String::new();
573 }
574 let mut out = format!("--- {old_name}\n+++ {new_name}\n");
575 let line = |out: &mut String, prefix: char, text: &str| {
576 out.push(prefix);
577 out.push_str(text);
578 if !text.ends_with('\n') {
579 out.push_str("\n\\ No newline at end of file\n");
580 }
581 };
582 for hunk in &hunks {
583 out.push_str(&hunk.header());
584 out.push('\n');
585 for op in &hunk.ops {
586 match op {
587 Op::Equal { old, .. } => {
588 for text in &self.old[old.clone()] {
589 line(&mut out, ' ', text);
590 }
591 }
592 Op::Delete { old, .. } => {
593 for text in &self.old[old.clone()] {
594 line(&mut out, '-', text);
595 }
596 }
597 Op::Insert { new, .. } => {
598 for text in &self.new[new.clone()] {
599 line(&mut out, '+', text);
600 }
601 }
602 }
603 }
604 }
605 out
606 }
607}