1use std::ops::Range;
24
25use imara_diff::intern::InternedInput;
26use imara_diff::{Algorithm, Sink};
27use ropey::Rope;
28use smallvec::smallvec;
29
30use crate::types::{DiffAlgorithm, Hunk, HunkIndex, HunkKind, LineRange};
31
32fn algorithm_to_imara(alg: DiffAlgorithm) -> Algorithm {
33 match alg {
34 DiffAlgorithm::Histogram => Algorithm::Histogram,
35 DiffAlgorithm::Myers => Algorithm::Myers,
36 DiffAlgorithm::MyersMinimal => Algorithm::MyersMinimal,
37 }
38}
39
40struct TwoWaySink {
43 hunks: Vec<Hunk>,
44}
45
46impl Sink for TwoWaySink {
47 type Out = Vec<Hunk>;
48
49 fn process_change(&mut self, before: Range<u32>, after: Range<u32>) {
50 let a = LineRange::new(before.start, before.end);
51 let b = LineRange::new(after.start, after.end);
52 let kind = classify_two_way(a, b);
53 self.hunks.push(Hunk {
54 kind,
55 ranges: smallvec![a, b],
56 refine: Default::default(),
57 });
58 }
59
60 fn finish(self) -> Self::Out {
61 self.hunks
62 }
63}
64
65fn classify_two_way(a: LineRange, b: LineRange) -> HunkKind {
66 match (a.is_empty(), b.is_empty()) {
67 (true, true) => HunkKind::Change,
70 (true, false) => HunkKind::Add,
71 (false, true) => HunkKind::Remove,
72 (false, false) => HunkKind::Change,
73 }
74}
75
76pub fn compute_diff(
99 sources: &[Rope],
100 algorithm: DiffAlgorithm,
101) -> Result<HunkIndex, DiffEngineError> {
102 match sources.len() {
103 0 => Err(DiffEngineError::Empty),
104 1 => Ok(HunkIndex {
105 hunks: Vec::new(),
106 algorithm,
107 revision: 0,
108 }),
109 2 => Ok(two_way(&sources[0], &sources[1], algorithm)),
110 3 => Ok(three_way(&sources[0], &sources[1], &sources[2], algorithm)),
111 n => Err(DiffEngineError::Unsupported { n }),
112 }
113}
114
115#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
120pub enum DiffEngineError {
121 #[error("diff requires at least one participant")]
122 Empty,
123 #[error("v1 supports up to 3 participants; got N = {n}")]
124 Unsupported { n: usize },
125}
126
127pub(crate) fn two_way(a: &Rope, b: &Rope, algorithm: DiffAlgorithm) -> HunkIndex {
144 let a_str = a.to_string();
145 let b_str = b.to_string();
146 let mut hunks = two_way_str(&a_str, &b_str, algorithm);
147 fill_refinements(&mut hunks, &a_str, &b_str);
152 HunkIndex {
153 hunks,
154 algorithm,
155 revision: 0,
156 }
157}
158
159fn fill_refinements(hunks: &mut [Hunk], a: &str, b: &str) {
171 let a_lines: Vec<&str> = a.lines().collect();
172 let b_lines: Vec<&str> = b.lines().collect();
173 for hunk in hunks.iter_mut() {
174 if hunk.kind != HunkKind::Change {
175 continue;
176 }
177 let (Some(before), Some(after)) = (hunk.ranges.first(), hunk.ranges.get(1)) else {
178 continue;
179 };
180 let slice = |src: &[&str], r: &LineRange| -> Vec<String> {
181 (r.start as usize..r.end as usize)
182 .filter_map(|i| src.get(i).map(|s| s.to_string()))
183 .collect()
184 };
185 let removed = slice(&a_lines, before);
186 let added = slice(&b_lines, after);
187 let removed_refs: Vec<&str> = removed.iter().map(|s| s.as_str()).collect();
188 let added_refs: Vec<&str> = added.iter().map(|s| s.as_str()).collect();
189 hunk.refine = crate::refine::refine_regions(&removed_refs, &added_refs);
190 }
191}
192
193fn two_way_str(a: &str, b: &str, algorithm: DiffAlgorithm) -> Vec<Hunk> {
202 let input = InternedInput::new(
203 imara_diff::sources::lines_with_terminator(a),
204 imara_diff::sources::lines_with_terminator(b),
205 );
206 imara_diff::diff(
207 algorithm_to_imara(algorithm),
208 &input,
209 TwoWaySink { hunks: Vec::new() },
210 )
211}
212
213pub(crate) fn three_way(
233 base: &Rope,
234 local: &Rope,
235 remote: &Rope,
236 algorithm: DiffAlgorithm,
237) -> HunkIndex {
238 let base_str = base.to_string();
239 let local_str = local.to_string();
240 let remote_str = remote.to_string();
241
242 let local_hunks = two_way_str(&base_str, &local_str, algorithm);
243 let remote_hunks = two_way_str(&base_str, &remote_str, algorithm);
244
245 let merged = merge_three_way(&local_hunks, &remote_hunks, &local_str, &remote_str);
246
247 HunkIndex {
248 hunks: merged,
249 algorithm,
250 revision: 0,
251 }
252}
253
254fn line_offsets(s: &str) -> Vec<usize> {
261 let mut offsets = Vec::with_capacity(s.len() / 32 + 2);
262 offsets.push(0);
263 for (i, byte) in s.bytes().enumerate() {
264 if byte == b'\n' {
265 offsets.push(i + 1);
266 }
267 }
268 if offsets.last().copied() != Some(s.len()) {
269 offsets.push(s.len());
270 }
271 offsets
272}
273
274fn line_slice<'a>(s: &'a str, offsets: &[usize], range: LineRange) -> &'a str {
278 let start = offsets
279 .get(range.start as usize)
280 .copied()
281 .unwrap_or(s.len());
282 let end = offsets.get(range.end as usize).copied().unwrap_or(s.len());
283 if start > end || start > s.len() {
284 return "";
285 }
286 &s[start..end.min(s.len())]
287}
288
289fn merge_three_way(
300 local_hunks: &[Hunk],
301 remote_hunks: &[Hunk],
302 local_str: &str,
303 remote_str: &str,
304) -> Vec<Hunk> {
305 let local_offsets = line_offsets(local_str);
306 let remote_offsets = line_offsets(remote_str);
307
308 let mut merged = Vec::new();
309 let mut li = 0;
310 let mut ri = 0;
311
312 let mut local_delta: i64 = 0;
317 let mut remote_delta: i64 = 0;
318
319 while li < local_hunks.len() || ri < remote_hunks.len() {
320 let l_pos = local_hunks.get(li).map(|h| h.ranges[0].start);
321 let r_pos = remote_hunks.get(ri).map(|h| h.ranges[0].start);
322
323 let take_local_first = match (l_pos, r_pos) {
325 (Some(l), Some(r)) => l <= r,
326 (Some(_), None) => true,
327 (None, Some(_)) => false,
328 (None, None) => break,
329 };
330
331 let mut taken_local: Vec<usize> = Vec::new();
332 let mut taken_remote: Vec<usize> = Vec::new();
333 let mut union_base;
334
335 if take_local_first {
336 union_base = local_hunks[li].ranges[0];
337 taken_local.push(li);
338 li += 1;
339 } else {
340 union_base = remote_hunks[ri].ranges[0];
341 taken_remote.push(ri);
342 ri += 1;
343 }
344
345 loop {
348 let mut extended = false;
349 if let Some(h) = local_hunks.get(li)
350 && h.ranges[0].start < union_base.end
351 {
352 union_base = LineRange::new(union_base.start, union_base.end.max(h.ranges[0].end));
353 taken_local.push(li);
354 li += 1;
355 extended = true;
356 }
357 if let Some(h) = remote_hunks.get(ri)
358 && h.ranges[0].start < union_base.end
359 {
360 union_base = LineRange::new(union_base.start, union_base.end.max(h.ranges[0].end));
361 taken_remote.push(ri);
362 ri += 1;
363 extended = true;
364 }
365 if !extended {
366 break;
367 }
368 }
369
370 let local_range = side_range(&taken_local, local_hunks, union_base, local_delta);
371 let remote_range = side_range(&taken_remote, remote_hunks, union_base, remote_delta);
372
373 for &idx in &taken_local {
375 let h = &local_hunks[idx];
376 local_delta += h.ranges[1].len() as i64 - h.ranges[0].len() as i64;
377 }
378 for &idx in &taken_remote {
379 let h = &remote_hunks[idx];
380 remote_delta += h.ranges[1].len() as i64 - h.ranges[0].len() as i64;
381 }
382
383 let kind = if !taken_local.is_empty() && !taken_remote.is_empty() {
384 let local_text = line_slice(local_str, &local_offsets, local_range);
392 let remote_text = line_slice(remote_str, &remote_offsets, remote_range);
393 if local_text == remote_text {
394 classify_three_way_attributed(union_base, local_range)
395 } else {
396 HunkKind::Conflict
397 }
398 } else if !taken_local.is_empty() {
399 classify_three_way_attributed(union_base, local_range)
400 } else {
401 classify_three_way_attributed(union_base, remote_range)
402 };
403
404 merged.push(Hunk {
405 kind,
406 ranges: smallvec![union_base, local_range, remote_range],
407 refine: Default::default(),
408 });
409 }
410
411 merged
412}
413
414fn side_range(
426 taken: &[usize],
427 hunks: &[Hunk],
428 union_base: LineRange,
429 side_delta: i64,
430) -> LineRange {
431 if taken.is_empty() {
432 let start = (union_base.start as i64 + side_delta).max(0) as u32;
433 let end = (union_base.end as i64 + side_delta).max(0) as u32;
434 LineRange::new(start, end)
435 } else {
436 let first = &hunks[taken[0]];
437 let last = &hunks[*taken.last().expect("taken non-empty")];
438 let prefix = first.ranges[0].start.saturating_sub(union_base.start);
441 let suffix = union_base.end.saturating_sub(last.ranges[0].end);
442 let start = first.ranges[1].start.saturating_sub(prefix);
443 let end = last.ranges[1].end + suffix;
444 LineRange::new(start, end)
445 }
446}
447
448fn classify_three_way_attributed(base: LineRange, side: LineRange) -> HunkKind {
449 match (base.is_empty(), side.is_empty()) {
450 (true, false) => HunkKind::Add,
451 (false, true) => HunkKind::Remove,
452 (false, false) => HunkKind::Change,
453 (true, true) => HunkKind::Change,
456 }
457}
458
459#[cfg(test)]
460mod refine_at_compute_time {
461 use super::*;
462
463 fn rope(s: &str) -> Rope {
464 Rope::from_str(s)
465 }
466
467 #[test]
471 fn a_change_hunk_carries_refinement() {
472 let a = rope("fn main() {\n let old = 1;\n}\n");
473 let b = rope("fn main() {\n let new = 1;\n}\n");
474 let idx = two_way(&a, &b, DiffAlgorithm::Histogram);
475 let change = idx
476 .hunks
477 .iter()
478 .find(|h| h.kind == HunkKind::Change)
479 .expect("one changed line");
480 let line = " let old = 1;";
483 let got: Vec<&str> = change
484 .refine
485 .removed_line(0)
486 .iter()
487 .map(|x| &line[x.clone()])
488 .collect();
489 assert_eq!(got, vec!["old"]);
490 }
491
492 #[test]
495 fn an_add_hunk_carries_no_refinement() {
496 let a = rope("one\n");
497 let b = rope("one\ntwo\n");
498 let idx = two_way(&a, &b, DiffAlgorithm::Histogram);
499 assert!(idx.hunks.iter().all(|h| h.refine.is_empty()));
500 }
501
502 #[test]
507 fn an_unequal_change_still_refines() {
508 let a = rope("let value = compute(a);\n");
509 let b = rope("// explain it\nlet value = derive(a);\n");
510 let idx = two_way(&a, &b, DiffAlgorithm::Histogram);
511 let change = idx
512 .hunks
513 .iter()
514 .find(|h| h.kind == HunkKind::Change)
515 .expect("a change hunk");
516 let line = "let value = compute(a);";
517 let got: Vec<&str> = change
518 .refine
519 .removed_line(0)
520 .iter()
521 .map(|x| &line[x.clone()])
522 .collect();
523 assert_eq!(
524 got,
525 vec!["compute"],
526 "the removed side marks the replaced identifier"
527 );
528 }
529
530 #[test]
535 fn each_side_is_aligned_with_its_own_range() {
536 let a = rope("a1\na2\n");
537 let b = rope("b1\nb2\n");
538 let idx = two_way(&a, &b, DiffAlgorithm::Histogram);
539 for h in idx.hunks.iter().filter(|h| h.kind == HunkKind::Change) {
540 if h.refine.is_empty() {
541 continue;
542 }
543 assert_eq!(
544 h.refine.removed.len(),
545 (h.ranges[0].end - h.ranges[0].start) as usize,
546 "one entry per baseline line in the hunk"
547 );
548 assert_eq!(
549 h.refine.added.len(),
550 (h.ranges[1].end - h.ranges[1].start) as usize,
551 "one entry per added line in the hunk"
552 );
553 }
554 }
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560
561 #[test]
562 fn empty_inputs_have_no_hunks() {
563 let a = Rope::new();
564 let b = Rope::new();
565 let idx = two_way(&a, &b, DiffAlgorithm::Histogram);
566 assert!(idx.is_empty());
567 }
568
569 #[test]
570 fn identical_inputs_have_no_hunks() {
571 let a = Rope::from("alpha\nbeta\ngamma\n");
572 let b = Rope::from("alpha\nbeta\ngamma\n");
573 let idx = two_way(&a, &b, DiffAlgorithm::Histogram);
574 assert!(idx.is_empty());
575 }
576
577 #[test]
578 fn pure_add_classifies_as_add() {
579 let a = Rope::from("alpha\ngamma\n");
580 let b = Rope::from("alpha\nbeta\ngamma\n");
581 let idx = two_way(&a, &b, DiffAlgorithm::Histogram);
582 assert_eq!(idx.len(), 1);
583 assert_eq!(idx.hunks[0].kind, HunkKind::Add);
584 }
585
586 #[test]
587 fn pure_remove_classifies_as_remove() {
588 let a = Rope::from("alpha\nbeta\ngamma\n");
589 let b = Rope::from("alpha\ngamma\n");
590 let idx = two_way(&a, &b, DiffAlgorithm::Histogram);
591 assert_eq!(idx.len(), 1);
592 assert_eq!(idx.hunks[0].kind, HunkKind::Remove);
593 }
594
595 #[test]
596 fn change_classifies_as_change() {
597 let a = Rope::from("alpha\nbeta\ngamma\n");
598 let b = Rope::from("alpha\nBETA\ngamma\n");
599 let idx = two_way(&a, &b, DiffAlgorithm::Histogram);
600 assert_eq!(idx.len(), 1);
601 assert_eq!(idx.hunks[0].kind, HunkKind::Change);
602 }
603
604 #[test]
605 fn three_way_non_overlapping_changes_no_conflict() {
606 let base = Rope::from("a\nb\nc\nd\ne\nf\n");
607 let local = Rope::from("a\nB\nc\nd\ne\nf\n"); let remote = Rope::from("a\nb\nc\nd\nE\nf\n"); let idx = three_way(&base, &local, &remote, DiffAlgorithm::Histogram);
610 assert_eq!(idx.len(), 2);
612 assert!(idx.hunks.iter().all(|h| h.kind != HunkKind::Conflict));
613 }
614
615 #[test]
616 fn three_way_overlapping_changes_yield_conflict() {
617 let base = Rope::from("a\nb\nc\n");
618 let local = Rope::from("a\nLOCAL\nc\n"); let remote = Rope::from("a\nREMOTE\nc\n"); let idx = three_way(&base, &local, &remote, DiffAlgorithm::Histogram);
621 assert_eq!(idx.len(), 1);
622 assert_eq!(idx.hunks[0].kind, HunkKind::Conflict);
623 }
624
625 #[test]
626 fn three_way_no_changes_no_hunks() {
627 let base = Rope::from("a\nb\nc\n");
628 let local = base.clone();
629 let remote = base.clone();
630 let idx = three_way(&base, &local, &remote, DiffAlgorithm::Histogram);
631 assert!(idx.is_empty());
632 }
633
634 #[test]
635 fn all_algorithms_agree_on_simple_change() {
636 let a = Rope::from("alpha\nbeta\ngamma\n");
637 let b = Rope::from("alpha\nBETA\ngamma\n");
638 for alg in [
639 DiffAlgorithm::Histogram,
640 DiffAlgorithm::Myers,
641 DiffAlgorithm::MyersMinimal,
642 ] {
643 let idx = two_way(&a, &b, alg);
644 assert_eq!(idx.len(), 1, "algorithm {alg:?}");
645 assert_eq!(idx.hunks[0].kind, HunkKind::Change, "algorithm {alg:?}");
646 assert_eq!(idx.algorithm, alg);
647 }
648 }
649
650 #[test]
655 fn compute_diff_zero_participants_is_empty_error() {
656 let result = compute_diff(&[], DiffAlgorithm::Histogram);
657 assert!(matches!(result, Err(DiffEngineError::Empty)));
658 }
659
660 #[test]
661 fn compute_diff_one_participant_returns_empty_hunk_index() {
662 let rope = Rope::from("alpha\nbeta\n");
665 let idx =
666 compute_diff(&[rope], DiffAlgorithm::Histogram).expect("N=1 must succeed (dormant)");
667 assert!(idx.is_empty());
668 assert_eq!(idx.algorithm, DiffAlgorithm::Histogram);
669 }
670
671 #[test]
672 fn compute_diff_two_participants_dispatches_to_two_way() {
673 let a = Rope::from("alpha\nbeta\ngamma\n");
674 let b = Rope::from("alpha\nBETA\ngamma\n");
675 let via_dispatch = compute_diff(&[a.clone(), b.clone()], DiffAlgorithm::Histogram)
676 .expect("N=2 is supported");
677 let direct = two_way(&a, &b, DiffAlgorithm::Histogram);
678 assert_eq!(via_dispatch.len(), direct.len());
680 assert_eq!(via_dispatch.hunks.len(), 1);
681 assert_eq!(via_dispatch.hunks[0].kind, HunkKind::Change);
682 }
683
684 #[test]
685 fn compute_diff_three_participants_dispatches_to_three_way() {
686 let base = Rope::from("aaa\nbbb\nccc\n");
687 let local = Rope::from("aaa\nBBB\nccc\n");
688 let remote = Rope::from("aaa\nbbb\nCCC\n");
689 let via_dispatch = compute_diff(
690 &[base.clone(), local.clone(), remote.clone()],
691 DiffAlgorithm::Histogram,
692 )
693 .expect("N=3 is supported");
694 let direct = three_way(&base, &local, &remote, DiffAlgorithm::Histogram);
695 assert_eq!(via_dispatch.len(), direct.len());
696 assert!(
698 via_dispatch
699 .hunks
700 .iter()
701 .all(|h| !matches!(h.kind, HunkKind::Conflict))
702 );
703 for h in &via_dispatch.hunks {
705 assert_eq!(h.ranges.len(), 3);
706 }
707 }
708
709 #[test]
710 fn compute_diff_four_participants_errors_unsupported() {
711 let r = Rope::from("x\n");
712 let result = compute_diff(
713 &[r.clone(), r.clone(), r.clone(), r],
714 DiffAlgorithm::Histogram,
715 );
716 assert!(matches!(result, Err(DiffEngineError::Unsupported { n: 4 })));
717 }
718
719 #[test]
720 fn compute_diff_arbitrarily_large_n_errors_unsupported() {
721 let sources: Vec<Rope> = (0..10).map(|i| Rope::from(format!("rope-{i}\n"))).collect();
722 let result = compute_diff(&sources, DiffAlgorithm::Histogram);
723 assert!(matches!(
724 result,
725 Err(DiffEngineError::Unsupported { n: 10 })
726 ));
727 }
728
729 #[test]
730 fn diff_engine_error_messages_name_the_cap() {
731 let empty = DiffEngineError::Empty;
734 let unsup = DiffEngineError::Unsupported { n: 5 };
735 assert_eq!(format!("{empty}"), "diff requires at least one participant");
736 assert_eq!(
737 format!("{unsup}"),
738 "v1 supports up to 3 participants; got N = 5"
739 );
740 }
741
742 #[test]
743 fn compute_diff_three_way_overlap_produces_conflict() {
744 let base = Rope::from("aaa\nbbb\nccc\n");
747 let local = Rope::from("aaa\nLOCAL\nccc\n");
748 let remote = Rope::from("aaa\nREMOTE\nccc\n");
749 let idx =
750 compute_diff(&[base, local, remote], DiffAlgorithm::Histogram).expect("N=3 supported");
751 assert!(
752 idx.hunks
753 .iter()
754 .any(|h| matches!(h.kind, HunkKind::Conflict))
755 );
756 }
757}