1use similar::{capture_diff_slices, Algorithm, DiffOp};
8
9#[derive(Debug, Clone, Copy, Default)]
11pub struct CombinedDiffWsOptions {
12 pub ignore_all_space: bool,
14 pub ignore_space_change: bool,
16 pub ignore_space_at_eol: bool,
18 pub ignore_cr_at_eol: bool,
20}
21
22impl CombinedDiffWsOptions {
23 #[must_use]
24 pub fn any(self) -> bool {
26 self.ignore_all_space
27 || self.ignore_space_change
28 || self.ignore_space_at_eol
29 || self.ignore_cr_at_eol
30 }
31}
32
33#[derive(Clone)]
34struct LostSeg {
35 text: String,
36 parent_map: u32,
37}
38
39fn strip_trailing_cr(s: &str, ignore: bool) -> &str {
40 if ignore && s.ends_with('\r') {
41 &s[..s.len().saturating_sub(1)]
42 } else {
43 s
44 }
45}
46
47fn line_key(line: &str, ws: CombinedDiffWsOptions) -> String {
48 let s = strip_trailing_cr(line, ws.ignore_cr_at_eol);
49 if ws.ignore_all_space {
50 s.chars().filter(|c| !c.is_whitespace()).collect()
51 } else if ws.ignore_space_change {
52 s.split_whitespace().collect::<Vec<_>>().join(" ")
53 } else if ws.ignore_space_at_eol {
54 s.trim_end_matches(|c: char| c.is_whitespace()).to_string()
55 } else {
56 s.to_string()
57 }
58}
59
60fn lines_match(a: &str, b: &str, ws: CombinedDiffWsOptions) -> bool {
61 line_key(a, ws) == line_key(b, ws)
62}
63
64fn coalesce_lost(
66 base: Vec<LostSeg>,
67 incoming: Vec<LostSeg>,
68 parent_bit: u32,
69 ws: CombinedDiffWsOptions,
70) -> Vec<LostSeg> {
71 if incoming.is_empty() {
72 return base;
73 }
74 if base.is_empty() {
75 return incoming;
76 }
77 let ob = base.len();
78 let nw = incoming.len();
79 #[derive(Clone, Copy)]
80 enum Dir {
81 Match,
82 Base,
83 New,
84 }
85 let mut lcs = vec![vec![0usize; nw + 1]; ob + 1];
86 let mut dir = vec![vec![Dir::Base; nw + 1]; ob + 1];
87 for item in dir[0].iter_mut().skip(1) {
88 *item = Dir::New;
89 }
90 for row in dir.iter_mut().skip(1) {
91 row[0] = Dir::Base;
92 }
93 for i in 1..=ob {
94 for j in 1..=nw {
95 if lines_match(&base[i - 1].text, &incoming[j - 1].text, ws) {
96 lcs[i][j] = lcs[i - 1][j - 1] + 1;
97 dir[i][j] = Dir::Match;
98 } else if lcs[i][j - 1] >= lcs[i - 1][j] {
99 lcs[i][j] = lcs[i][j - 1];
100 dir[i][j] = Dir::New;
101 } else {
102 lcs[i][j] = lcs[i - 1][j];
103 dir[i][j] = Dir::Base;
104 }
105 }
106 }
107 let mut out: Vec<LostSeg> = Vec::new();
108 let mut i = ob;
109 let mut j = nw;
110 while i > 0 || j > 0 {
111 match dir[i][j] {
112 Dir::Match => {
113 let mut seg = base[i - 1].clone();
114 seg.parent_map |= parent_bit;
115 out.push(seg);
116 i -= 1;
117 j -= 1;
118 }
119 Dir::New => {
120 out.push(incoming[j - 1].clone());
121 j -= 1;
122 }
123 Dir::Base => {
124 out.push(base[i - 1].clone());
125 i -= 1;
126 }
127 }
128 }
129 out.reverse();
130 out
131}
132
133struct Sline {
134 lost: Vec<LostSeg>,
136 plost: Vec<LostSeg>,
138 flag: u32,
140 bol: String,
141 p_lno: Vec<u32>,
142}
143
144fn split_lines_with_incomplete(text: &str) -> (Vec<String>, usize) {
145 if text.is_empty() {
146 return (Vec::new(), 0);
147 }
148 let lines: Vec<String> = text.lines().map(str::to_owned).collect();
149 let cnt = lines.len();
150 (lines, cnt)
151}
152
153fn combine_one_parent(
154 slines: &mut [Sline],
155 cnt: usize,
156 parent_lines: &[String],
157 n: usize,
158 _num_parent: usize,
159 ws: CombinedDiffWsOptions,
160) {
161 let nmask = 1u32 << n;
162 let old_keys: Vec<String> = parent_lines.iter().map(|l| line_key(l, ws)).collect();
163 let new_keys: Vec<String> = slines[..cnt].iter().map(|s| line_key(&s.bol, ws)).collect();
164 let ops = capture_diff_slices(Algorithm::Myers, &old_keys, &new_keys);
165
166 let mut idx = 0usize;
171 while idx < ops.len() {
172 if matches!(ops[idx], DiffOp::Equal { .. }) {
173 idx += 1;
174 continue;
175 }
176 let start = idx;
178 let mut del_lines: Vec<usize> = Vec::new();
179 let mut ins_lines: Vec<usize> = Vec::new();
180 let mut nb: Option<usize> = None;
182 let mut nn = 0usize;
183 while idx < ops.len() && !matches!(ops[idx], DiffOp::Equal { .. }) {
184 match ops[idx] {
185 DiffOp::Delete {
186 old_index,
187 old_len,
188 new_index,
189 ..
190 } => {
191 if nb.is_none() {
192 nb = Some(new_index);
193 }
194 for k in 0..old_len {
195 del_lines.push(old_index + k);
196 }
197 }
198 DiffOp::Insert {
199 old_index: _,
200 new_index,
201 new_len,
202 } => {
203 if nb.is_none() {
204 nb = Some(new_index);
205 }
206 for k in 0..new_len {
207 ins_lines.push(new_index + k);
208 nn += 1;
209 }
210 }
211 DiffOp::Replace {
212 old_index,
213 old_len,
214 new_index,
215 new_len,
216 } => {
217 if nb.is_none() {
218 nb = Some(new_index);
219 }
220 for k in 0..old_len {
221 del_lines.push(old_index + k);
222 }
223 for k in 0..new_len {
224 ins_lines.push(new_index + k);
225 nn += 1;
226 }
227 }
228 DiffOp::Equal { .. } => unreachable!(),
229 }
230 idx += 1;
231 }
232 let _ = start;
233 let nb0 = nb.unwrap_or(0);
239 let bucket = if nn == 0 {
240 (nb0).min(cnt)
242 } else {
243 nb0.min(cnt)
245 };
246 let bucket = bucket.min(cnt);
247 for &oi in &del_lines {
248 slines[bucket].plost.push(LostSeg {
249 text: parent_lines[oi].clone(),
250 parent_map: nmask,
251 });
252 }
253 for &ni in &ins_lines {
254 if ni < cnt {
255 slines[ni].flag |= nmask;
256 }
257 }
258 }
259
260 let mut p_lno = 1u32;
261 for (lno, sline) in slines.iter_mut().take(cnt + 1).enumerate() {
262 sline.p_lno[n] = p_lno;
263 if !sline.plost.is_empty() {
264 let incoming = std::mem::take(&mut sline.plost);
265 sline.lost = coalesce_lost(std::mem::take(&mut sline.lost), incoming, nmask, ws);
266 }
267 for seg in &sline.lost {
268 if seg.parent_map & nmask != 0 {
269 p_lno = p_lno.saturating_add(1);
270 }
271 }
272 if lno < cnt && sline.flag & nmask == 0 {
273 p_lno = p_lno.saturating_add(1);
274 }
275 }
276 if let Some(trailer) = slines.get_mut(cnt + 1) {
279 trailer.p_lno[n] = p_lno;
280 }
281}
282
283fn interesting(s: &Sline, all_mask: u32) -> bool {
284 (s.flag & all_mask) != 0 || !s.lost.is_empty()
285}
286
287fn find_next(slines: &[Sline], mark: u32, mut i: usize, cnt: usize, want_unmarked: bool) -> usize {
288 while i <= cnt {
289 let marked = slines[i].flag & mark != 0;
290 if want_unmarked != marked {
291 return i;
292 }
293 i += 1;
294 }
295 cnt + 1
296}
297
298fn adjust_hunk_tail(slines: &[Sline], all_mask: u32, hunk_begin: usize, mut i: usize) -> usize {
299 if hunk_begin < i && slines[i - 1].flag & all_mask == 0 {
300 i -= 1;
301 }
302 i
303}
304
305fn give_context(slines: &mut [Sline], cnt: usize, num_parent: usize, context: usize) {
306 let all_mask = (1u32 << num_parent) - 1;
307 let mark = 1u32 << num_parent;
308 let no_pre_delete = 2u32 << num_parent;
309
310 let mut i = find_next(slines, mark, 0, cnt, false);
311 if cnt < i {
312 return;
313 }
314
315 while i <= cnt {
316 let mut j = i.saturating_sub(context);
317 while j < i {
318 if slines[j].flag & mark == 0 {
319 slines[j].flag |= no_pre_delete;
320 }
321 slines[j].flag |= mark;
322 j += 1;
323 }
324
325 loop {
326 j = find_next(slines, mark, i, cnt, true);
327 if cnt < j {
328 return;
329 }
330 let k = find_next(slines, mark, j, cnt, false);
331 let j_adj = adjust_hunk_tail(slines, all_mask, i, j);
332
333 if k < j_adj + context {
334 let mut t = j;
335 while t < k {
336 slines[t].flag |= mark;
337 t += 1;
338 }
339 i = k;
340 continue;
341 }
342
343 i = k;
344 let k_end = (j + context).min(cnt + 1);
345 let mut t = j;
346 while t < k_end {
347 slines[t].flag |= mark;
348 t += 1;
349 }
350 break;
351 }
352 }
353}
354
355fn make_hunks(slines: &mut [Sline], cnt: usize, num_parent: usize, dense: bool, context: usize) {
356 let all_mask = (1u32 << num_parent) - 1;
357 let mark = 1u32 << num_parent;
358
359 for sline in slines.iter_mut().take(cnt + 1) {
360 if interesting(sline, all_mask) {
361 sline.flag |= mark;
362 } else {
363 sline.flag &= !mark;
364 }
365 }
366
367 if dense {
368 let mut i = 0usize;
369 while i <= cnt {
370 while i <= cnt && slines[i].flag & mark == 0 {
371 i += 1;
372 }
373 if cnt < i {
374 break;
375 }
376 let hunk_begin = i;
377 let mut j = i + 1;
378 while j <= cnt {
379 if slines[j].flag & mark == 0 {
380 let j_adj = adjust_hunk_tail(slines, all_mask, hunk_begin, j);
381 let la = (j_adj + context).min(cnt + 1);
382 let mut contin = false;
383 let mut la2 = la;
384 while la2 > j {
385 la2 -= 1;
386 if slines[la2].flag & mark != 0 {
387 contin = true;
388 break;
389 }
390 }
391 if !contin {
392 break;
393 }
394 j = la2;
395 }
396 j += 1;
397 }
398 let hunk_end = j;
399
400 let mut same_diff = 0u32;
401 let mut has_interesting = false;
402 let mut jj = hunk_begin;
403 while jj < hunk_end && !has_interesting {
404 let mut this_diff = slines[jj].flag & all_mask;
405 if this_diff != 0 {
406 if same_diff == 0 {
407 same_diff = this_diff;
408 } else if same_diff != this_diff {
409 has_interesting = true;
410 break;
411 }
412 }
413 let ll_iter = slines[jj].lost.iter();
414 for seg in ll_iter {
415 if has_interesting {
416 break;
417 }
418 this_diff = seg.parent_map;
419 if same_diff == 0 {
420 same_diff = this_diff;
421 } else if same_diff != this_diff {
422 has_interesting = true;
423 }
424 }
425 jj += 1;
426 }
427 if !has_interesting && same_diff != 0 && same_diff != all_mask {
428 for sline in slines.iter_mut().take(hunk_end).skip(hunk_begin) {
429 sline.flag &= !mark;
430 }
431 }
432 i = hunk_end;
433 }
434 }
435
436 give_context(slines, cnt, num_parent, context);
437}
438
439fn show_parent_lno(slines: &[Sline], l0: usize, l1: usize, n: usize, null_ctx: u32) -> String {
440 let a = slines[l0].p_lno[n];
441 let b = slines[l1].p_lno[n];
442 format!(" -{a},{}", b.saturating_sub(a).saturating_sub(null_ctx))
443}
444
445#[derive(Debug, Clone, Copy, PartialEq, Eq)]
447pub enum CombinedStructuredLineKind {
448 Context,
450 Add,
452 Del,
454}
455
456impl CombinedStructuredLineKind {
457 #[must_use]
459 pub fn from_parent_markers(parents: &[char]) -> Self {
460 if parents.contains(&'-') {
461 Self::Del
462 } else if parents.contains(&'+') {
463 Self::Add
464 } else {
465 Self::Context
466 }
467 }
468}
469
470#[derive(Debug, Clone, PartialEq, Eq)]
472pub struct CombinedStructuredLine {
473 pub parents: Vec<char>,
475 pub text: String,
477 pub kind: CombinedStructuredLineKind,
479}
480
481#[derive(Debug, Clone, PartialEq, Eq)]
483pub struct CombinedStructuredHunk {
484 pub header: String,
486 pub lines: Vec<CombinedStructuredLine>,
488}
489
490#[derive(Debug, Clone, PartialEq, Eq, Default)]
492pub struct CombinedStructuredTextDiff {
493 pub hunks: Vec<CombinedStructuredHunk>,
495}
496
497fn dump_slines_structured(
498 slines: &[Sline],
499 cnt: usize,
500 num_parent: usize,
501 context: usize,
502) -> CombinedStructuredTextDiff {
503 let mark = 1u32 << num_parent;
504 let no_pre_delete = 2u32 << num_parent;
505 let mut hunks = Vec::new();
506 let mut lno = 0usize;
507
508 loop {
509 while lno <= cnt && slines[lno].flag & mark == 0 {
510 lno += 1;
511 }
512 if cnt < lno {
513 break;
514 }
515 let h_start = lno;
516 let mut h_end = lno + 1;
517 while h_end <= cnt && slines[h_end].flag & mark != 0 {
518 h_end += 1;
519 }
520 let mut rlines = h_end - h_start;
521 if cnt < h_end {
522 rlines = rlines.saturating_sub(1);
523 }
524 let mut null_ctx = 0u32;
525 if context == 0 {
526 for sl in slines.iter().take(h_end).skip(h_start) {
527 if sl.flag & (mark - 1) == 0 {
528 null_ctx = null_ctx.saturating_add(1);
529 }
530 }
531 rlines = rlines.saturating_sub(null_ctx as usize);
532 }
533 let mut header = String::new();
534 for _ in 0..=num_parent {
535 header.push('@');
536 }
537 for n in 0..num_parent {
538 header.push_str(&show_parent_lno(slines, h_start, h_end, n, null_ctx));
539 }
540 header.push_str(&format!(" +{},{} ", h_start + 1, rlines));
541 for _ in 0..=num_parent {
542 header.push('@');
543 }
544 let mut lines = Vec::new();
545
546 while lno < h_end {
547 let sl = &slines[lno];
548 lno += 1;
549 let show_lost = if sl.flag & no_pre_delete == 0 {
550 sl.lost.as_slice()
551 } else {
552 &[][..]
553 };
554 for seg in show_lost {
555 let mut parents = Vec::with_capacity(num_parent);
556 for j in 0..num_parent {
557 parents.push(if seg.parent_map & (1u32 << j) != 0 {
558 '-'
559 } else {
560 ' '
561 });
562 }
563 lines.push(CombinedStructuredLine {
564 kind: CombinedStructuredLineKind::from_parent_markers(&parents),
565 parents,
566 text: seg.text.clone(),
567 });
568 }
569 if cnt < lno {
570 break;
571 }
572 let sl = &slines[lno - 1];
573 if sl.flag & (mark - 1) == 0 && context == 0 {
574 continue;
575 }
576 let mut parents = Vec::with_capacity(num_parent);
577 let mut p_mask = 1u32;
578 for _ in 0..num_parent {
579 parents.push(if p_mask & sl.flag != 0 { '+' } else { ' ' });
580 p_mask <<= 1;
581 }
582 lines.push(CombinedStructuredLine {
583 kind: CombinedStructuredLineKind::from_parent_markers(&parents),
584 parents,
585 text: sl.bol.clone(),
586 });
587 }
588 hunks.push(CombinedStructuredHunk { header, lines });
589 }
590 CombinedStructuredTextDiff { hunks }
591}
592
593fn dump_slines(slines: &[Sline], cnt: usize, num_parent: usize, context: usize) -> String {
594 let mark = 1u32 << num_parent;
595 let no_pre_delete = 2u32 << num_parent;
596 let mut out = String::new();
597 let mut lno = 0usize;
598
599 loop {
600 while lno <= cnt && slines[lno].flag & mark == 0 {
601 lno += 1;
602 }
603 if cnt < lno {
604 break;
605 }
606 let h_start = lno;
607 let mut h_end = lno + 1;
608 while h_end <= cnt && slines[h_end].flag & mark != 0 {
609 h_end += 1;
610 }
611 let mut rlines = h_end - h_start;
612 if cnt < h_end {
613 rlines = rlines.saturating_sub(1);
614 }
615 let mut null_ctx = 0u32;
616 if context == 0 {
617 for sl in slines.iter().take(h_end).skip(h_start) {
618 if sl.flag & (mark - 1) == 0 {
619 null_ctx = null_ctx.saturating_add(1);
620 }
621 }
622 rlines = rlines.saturating_sub(null_ctx as usize);
623 }
624
625 for _ in 0..=num_parent {
628 out.push('@');
629 }
630 for n in 0..num_parent {
631 out.push_str(&show_parent_lno(slines, h_start, h_end, n, null_ctx));
632 }
633 out.push_str(&format!(" +{},{} ", h_start + 1, rlines));
634 for _ in 0..=num_parent {
635 out.push('@');
636 }
637 out.push('\n');
638
639 while lno < h_end {
640 let sl = &slines[lno];
641 lno += 1;
642 let show_lost = if sl.flag & no_pre_delete == 0 {
643 sl.lost.as_slice()
644 } else {
645 &[][..]
646 };
647 for seg in show_lost {
651 for j in 0..num_parent {
652 if seg.parent_map & (1u32 << j) != 0 {
653 out.push('-');
654 } else {
655 out.push(' ');
656 }
657 }
658 out.push_str(&seg.text);
659 out.push('\n');
660 }
661 if cnt < lno {
662 break;
663 }
664 let sl = &slines[lno - 1];
665 if sl.flag & (mark - 1) == 0 && context == 0 {
666 continue;
667 }
668 let mut p_mask = 1u32;
669 for _ in 0..num_parent {
670 if p_mask & sl.flag != 0 {
671 out.push('+');
672 } else {
673 out.push(' ');
674 }
675 p_mask <<= 1;
676 }
677 out.push_str(&sl.bol);
678 out.push('\n');
679 }
680 }
681 out
682}
683
684fn reuse_parent(slines: &mut [Sline], cnt: usize, i: usize, j: usize) {
685 let im = 1u32 << i;
686 let jm = 1u32 << j;
687 for sline in slines.iter_mut().take(cnt + 1) {
688 for seg in &mut sline.lost {
689 if seg.parent_map & jm != 0 {
690 seg.parent_map |= im;
691 }
692 }
693 if sline.flag & jm != 0 {
694 sline.flag |= im;
695 }
696 sline.p_lno[i] = sline.p_lno[j];
697 }
698 if let Some(trailer) = slines.get_mut(cnt + 1) {
701 trailer.p_lno[i] = trailer.p_lno[j];
702 }
703}
704
705fn prepare_combined_slines(
706 parent_texts: &[String],
707 result_text: &str,
708 ws: CombinedDiffWsOptions,
709) -> Option<(Vec<Sline>, usize, usize)> {
710 let num_parent = parent_texts.len();
711 if num_parent == 0 {
712 return None;
713 }
714 let (res_lines, cnt) = split_lines_with_incomplete(result_text);
715 if cnt == 0 && result_text.is_empty() && parent_texts.iter().all(String::is_empty) {
716 return None;
717 }
718 let mut slines: Vec<Sline> = (0..=cnt + 1)
719 .map(|idx| Sline {
720 lost: Vec::new(),
721 plost: Vec::new(),
722 flag: 0,
723 bol: if idx < cnt {
724 res_lines[idx].clone()
725 } else {
726 String::new()
727 },
728 p_lno: vec![0; num_parent],
729 })
730 .collect();
731 let parents: Vec<Vec<String>> = parent_texts
732 .iter()
733 .map(|p| p.lines().map(str::to_owned).collect())
734 .collect();
735 for i in 0..num_parent {
736 let mut reused = false;
737 for j in 0..i {
738 if parents[i] == parents[j] {
739 reuse_parent(&mut slines, cnt, i, j);
740 reused = true;
741 break;
742 }
743 }
744 if !reused {
745 combine_one_parent(&mut slines, cnt, &parents[i], i, num_parent, ws);
746 }
747 }
748 Some((slines, cnt, num_parent))
749}
750
751#[must_use]
753pub fn structured_combined_diff_body(
754 parent_texts: &[String],
755 result_text: &str,
756 context: usize,
757 dense: bool,
758 ws: CombinedDiffWsOptions,
759) -> CombinedStructuredTextDiff {
760 let Some((mut slines, cnt, num_parent)) =
761 prepare_combined_slines(parent_texts, result_text, ws)
762 else {
763 return CombinedStructuredTextDiff::default();
764 };
765 make_hunks(&mut slines, cnt, num_parent, dense, context);
766 dump_slines_structured(&slines, cnt, num_parent, context)
767}
768
769#[must_use]
771pub fn format_combined_structured_body(diff: &CombinedStructuredTextDiff) -> String {
772 let mut out = String::new();
773 for hunk in &diff.hunks {
774 out.push_str(&hunk.header);
775 out.push('\n');
776 for line in &hunk.lines {
777 for ch in &line.parents {
778 out.push(*ch);
779 }
780 out.push_str(&line.text);
781 out.push('\n');
782 }
783 }
784 out
785}
786
787#[must_use]
789pub fn format_combined_diff_body(
790 parent_texts: &[String],
791 result_text: &str,
792 context: usize,
793 dense: bool,
794 ws: CombinedDiffWsOptions,
795) -> String {
796 let Some((mut slines, cnt, num_parent)) =
797 prepare_combined_slines(parent_texts, result_text, ws)
798 else {
799 return String::new();
800 };
801 make_hunks(&mut slines, cnt, num_parent, dense, context);
802 dump_slines(&slines, cnt, num_parent, context)
803}