1use crate::line_diff::{
31 DiffAlgorithm, DiffLine, DiffOp, WsIgnore, line_is_blank, myers_diff_lines_ws,
32 patience_diff_lines_anchored, split_lines,
33};
34use std::collections::HashMap;
35
36pub const DEFAULT_CONTEXT: usize = 3;
38const FUNCTION_CONTEXT_FLAG: usize = 1usize << (usize::BITS - 1);
39const CONTEXT_VALUE_MASK: usize = !FUNCTION_CONTEXT_FLAG;
40
41pub fn enable_function_context(context: usize) -> usize {
44 (context & CONTEXT_VALUE_MASK) | FUNCTION_CONTEXT_FLAG
45}
46
47fn decode_context(context: usize) -> (usize, bool) {
48 (
49 context & CONTEXT_VALUE_MASK,
50 context & FUNCTION_CONTEXT_FLAG != 0,
51 )
52}
53
54fn replace_context_value(encoded: usize, context: usize) -> usize {
55 (encoded & !CONTEXT_VALUE_MASK) | (context & CONTEXT_VALUE_MASK)
56}
57
58#[derive(Clone, Copy, PartialEq, Eq, Debug)]
60pub enum LineKind {
61 Context,
63 Delete,
65 Insert,
67}
68
69#[derive(Clone, Copy)]
72pub struct TaggedLine<'a> {
73 pub kind: LineKind,
75 pub content: &'a [u8],
77 pub old_index: usize,
79 pub new_index: usize,
81}
82
83#[derive(Clone, Copy)]
90pub struct RenderColors<'a> {
91 pub frag: &'a str,
93 pub func: &'a str,
95 pub old: &'a str,
97 pub new: &'a str,
99 pub context: &'a str,
101 pub reset: &'a str,
103 pub whitespace: &'a str,
106 pub old_moved: &'a str,
108 pub old_moved_alt: &'a str,
110 pub old_moved_dim: &'a str,
112 pub old_moved_alt_dim: &'a str,
114 pub new_moved: &'a str,
116 pub new_moved_alt: &'a str,
118 pub new_moved_dim: &'a str,
120 pub new_moved_alt_dim: &'a str,
122}
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq)]
126pub enum ColorMovedMode {
127 Plain,
129 Blocks,
131 Zebra,
133 DimmedZebra,
135}
136
137#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
139pub struct ColorMovedWs {
140 pub ignore: WsIgnore,
142 pub allow_indentation_change: bool,
144}
145
146#[derive(Clone, Copy, Debug, PartialEq, Eq)]
148pub struct ColorMoved {
149 pub mode: ColorMovedMode,
151 pub ws: ColorMovedWs,
153}
154
155pub type HeadingFn<'a> = dyn FnMut(&[u8]) -> Option<Vec<u8>> + 'a;
163
164pub trait HunkWordDiff {
173 fn push_minus(&mut self, content: &[u8]);
175 fn push_plus(&mut self, content: &[u8]);
177 fn flush(&mut self, out: &mut Vec<u8>);
179 fn emit_context_line(&mut self, out: &mut Vec<u8>, content: &[u8]);
181}
182
183pub struct HunkRenderOptions<'a, 'h> {
188 pub context: usize,
191 pub interhunk: usize,
193 pub heading: Option<&'a mut HeadingFn<'h>>,
195 pub colors: Option<RenderColors<'a>>,
197 pub word_diff: Option<&'a mut dyn HunkWordDiff>,
199 pub line_indicators: LineIndicators,
201 pub ws_error: Option<WsErrorHighlight>,
205 pub ws_ignore: WsIgnore,
210 pub algorithm: DiffAlgorithm,
212 pub indent_heuristic: bool,
220 pub change_ignore: Option<&'a ChangeIgnore<'a>>,
225 pub line_ranges: Option<&'a [LineRange]>,
233 pub color_moved: Option<ColorMoved>,
236 pub anchors: &'a [Vec<u8>],
240}
241
242#[derive(Clone, Copy, Debug, PartialEq, Eq)]
243pub struct LineIndicators {
244 pub context: u8,
245 pub old: u8,
246 pub new: u8,
247}
248
249impl Default for LineIndicators {
250 fn default() -> Self {
251 Self {
252 context: b' ',
253 old: b'-',
254 new: b'+',
255 }
256 }
257}
258
259#[derive(Clone, Copy, Debug, PartialEq, Eq)]
262pub struct LineRange {
263 pub start: i64,
265 pub end: i64,
267}
268
269pub type ChangeIgnoreRegex<'a> = &'a dyn Fn(&[u8]) -> bool;
275
276pub struct ChangeIgnore<'a> {
277 pub ignore_blank_lines: bool,
279 pub regex_match: Option<ChangeIgnoreRegex<'a>>,
283}
284
285#[derive(Clone, Copy)]
289pub struct WsErrorHighlight {
290 pub rule: crate::ws::WsRule,
292 pub old: bool,
294 pub new: bool,
296 pub context: bool,
298}
299
300impl Default for HunkRenderOptions<'_, '_> {
301 fn default() -> Self {
302 Self {
303 context: DEFAULT_CONTEXT,
304 interhunk: 0,
305 heading: None,
306 colors: None,
307 word_diff: None,
308 line_indicators: LineIndicators::default(),
309 ws_error: None,
310 ws_ignore: WsIgnore::default(),
311 algorithm: DiffAlgorithm::Myers,
312 indent_heuristic: true,
313 change_ignore: None,
314 line_ranges: None,
315 color_moved: None,
316 anchors: &[],
317 }
318 }
319}
320
321pub fn render_hunks(
334 out: &mut Vec<u8>,
335 old_content: Option<&[u8]>,
336 new_content: Option<&[u8]>,
337 options: &mut HunkRenderOptions<'_, '_>,
338) {
339 let (context, function_context) = decode_context(options.context);
340 if let Some(ranges) = options.line_ranges {
345 let max_span = ranges
346 .iter()
347 .map(|r| r.end - r.start)
348 .max()
349 .unwrap_or(0)
350 .max(0) as usize;
351 let saved_context = options.context;
352 let line_indicators = options.line_indicators;
353 let word_diff = options.word_diff.is_some();
354 options.context = replace_context_value(saved_context, context.max(max_span));
355 options.line_ranges = None;
356 let mut full = Vec::new();
357 render_hunks(&mut full, old_content, new_content, options);
358 options.context = saved_context;
359 options.line_ranges = Some(ranges);
360 filter_hunks_to_ranges(out, &full, ranges, line_indicators, word_diff);
361 return;
362 }
363 let old = split_lines(old_content.unwrap_or_default());
364 let new = split_lines(new_content.unwrap_or_default());
365 let mut ops = if options.algorithm == DiffAlgorithm::Patience
369 && !options.anchors.is_empty()
370 && options.ws_ignore.is_empty()
371 {
372 patience_diff_lines_anchored(&old, &new, options.anchors)
373 } else {
374 myers_diff_lines_ws(&old, &new, options.ws_ignore, options.algorithm)
375 };
376
377 change_compact(
382 &mut ops,
383 &old,
384 &new,
385 options.ws_ignore,
386 options.indent_heuristic,
387 );
388
389 let mut tagged: Vec<TaggedLine<'_>> = Vec::new();
392 let mut old_idx = 0usize;
393 let mut new_idx = 0usize;
394 for op in ops {
395 match op {
396 DiffOp::Equal(n) => {
397 for _ in 0..n {
398 tagged.push(TaggedLine {
401 kind: LineKind::Context,
402 content: new[new_idx].content,
403 old_index: old_idx,
404 new_index: new_idx,
405 });
406 old_idx += 1;
407 new_idx += 1;
408 }
409 }
410 DiffOp::Delete(n) => {
411 for _ in 0..n {
412 tagged.push(TaggedLine {
413 kind: LineKind::Delete,
414 content: old[old_idx].content,
415 old_index: old_idx,
416 new_index: new_idx,
417 });
418 old_idx += 1;
419 }
420 }
421 DiffOp::Insert(n) => {
422 for _ in 0..n {
423 tagged.push(TaggedLine {
424 kind: LineKind::Insert,
425 content: new[new_idx].content,
426 old_index: old_idx,
427 new_index: new_idx,
428 });
429 new_idx += 1;
430 }
431 }
432 }
433 }
434
435 let changes = build_changes(&tagged);
439 if changes.is_empty() {
440 return;
441 }
442
443 let mut changes = changes;
447 if let Some(ci) = options.change_ignore {
448 mark_ignorable_changes(&mut changes, &old, &new, options.ws_ignore, ci);
449 }
450
451 let mut groups = group_changes_into_hunks(&changes, context, options.interhunk);
456 if function_context {
457 groups = expand_hunks_to_function_context(
458 &groups,
459 &tagged,
460 &old,
461 &new,
462 options.heading.as_deref_mut(),
463 );
464 }
465
466 let moved_styles = options
467 .color_moved
468 .filter(|_| options.colors.is_some() && options.word_diff.is_none())
469 .map(|color_moved| mark_color_as_moved(&tagged, color_moved));
470
471 for (first_change, last_change) in groups {
472 let (hunk_start, hunk_end) = if function_context {
473 (first_change, (last_change + 1).min(tagged.len()))
474 } else {
475 (
476 first_change.saturating_sub(context),
477 (last_change + context + 1).min(tagged.len()),
478 )
479 };
480 render_one_hunk(
481 out,
482 &tagged,
483 moved_styles.as_deref(),
484 &old,
485 hunk_start,
486 hunk_end,
487 options,
488 );
489 }
490}
491
492const MAX_INDENT: i32 = 200;
514const MAX_BLANKS: i32 = 20;
516
517const START_OF_FILE_PENALTY: i32 = 1;
519const END_OF_FILE_PENALTY: i32 = 21;
520const TOTAL_BLANK_WEIGHT: i32 = -30;
521const POST_BLANK_WEIGHT: i32 = 6;
522const RELATIVE_INDENT_PENALTY: i32 = -4;
523const RELATIVE_INDENT_WITH_BLANK_PENALTY: i32 = 10;
524const RELATIVE_OUTDENT_PENALTY: i32 = 24;
525const RELATIVE_OUTDENT_WITH_BLANK_PENALTY: i32 = 17;
526const RELATIVE_DEDENT_PENALTY: i32 = 23;
527const RELATIVE_DEDENT_WITH_BLANK_PENALTY: i32 = 17;
528const INDENT_WEIGHT: i32 = 60;
529const INDENT_HEURISTIC_MAX_SLIDING: i64 = 100;
530
531struct CompactFile {
536 recs: Vec<Vec<u8>>,
537 changed: Vec<bool>,
538}
539
540impl CompactFile {
541 fn nrec(&self) -> i64 {
542 self.recs.len() as i64
543 }
544
545 fn changed(&self, i: i64) -> bool {
548 if i < 0 || i >= self.nrec() {
549 false
550 } else {
551 self.changed[i as usize]
552 }
553 }
554
555 fn set_changed(&mut self, i: i64, v: bool) {
556 self.changed[i as usize] = v;
557 }
558}
559
560fn get_indent(rec: &[u8]) -> i32 {
564 let mut ret: i32 = 0;
565 for &c in rec {
566 if !xdl_isspace(c) {
567 return ret;
568 } else if c == b' ' {
569 ret += 1;
570 } else if c == b'\t' {
571 ret += 8 - ret % 8;
572 }
573 if ret >= MAX_INDENT {
575 return MAX_INDENT;
576 }
577 }
578 -1
580}
581
582fn xdl_isspace(c: u8) -> bool {
585 matches!(c, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')
586}
587
588#[derive(Default)]
590struct SplitMeasurement {
591 end_of_file: bool,
592 indent: i32,
593 pre_blank: i32,
594 pre_indent: i32,
595 post_blank: i32,
596 post_indent: i32,
597}
598
599#[derive(Default, Clone, Copy)]
601struct SplitScore {
602 effective_indent: i32,
603 penalty: i32,
604}
605
606fn measure_split(xdf: &CompactFile, split: i64) -> SplitMeasurement {
609 let mut m = SplitMeasurement::default();
610 if split >= xdf.nrec() {
611 m.end_of_file = true;
612 m.indent = -1;
613 } else {
614 m.end_of_file = false;
615 m.indent = get_indent(&xdf.recs[split as usize]);
616 }
617
618 m.pre_blank = 0;
619 m.pre_indent = -1;
620 let mut i = split - 1;
621 while i >= 0 {
622 m.pre_indent = get_indent(&xdf.recs[i as usize]);
623 if m.pre_indent != -1 {
624 break;
625 }
626 m.pre_blank += 1;
627 if m.pre_blank == MAX_BLANKS {
628 m.pre_indent = 0;
629 break;
630 }
631 i -= 1;
632 }
633
634 m.post_blank = 0;
635 m.post_indent = -1;
636 let mut i = split + 1;
637 while i < xdf.nrec() {
638 m.post_indent = get_indent(&xdf.recs[i as usize]);
639 if m.post_indent != -1 {
640 break;
641 }
642 m.post_blank += 1;
643 if m.post_blank == MAX_BLANKS {
644 m.post_indent = 0;
645 break;
646 }
647 i += 1;
648 }
649
650 m
651}
652
653fn score_add_split(m: &SplitMeasurement, s: &mut SplitScore) {
655 if m.pre_indent == -1 && m.pre_blank == 0 {
656 s.penalty += START_OF_FILE_PENALTY;
657 }
658 if m.end_of_file {
659 s.penalty += END_OF_FILE_PENALTY;
660 }
661
662 let post_blank = if m.indent == -1 { 1 + m.post_blank } else { 0 };
663 let total_blank = m.pre_blank + post_blank;
664
665 s.penalty += TOTAL_BLANK_WEIGHT * total_blank;
666 s.penalty += POST_BLANK_WEIGHT * post_blank;
667
668 let indent = if m.indent != -1 {
669 m.indent
670 } else {
671 m.post_indent
672 };
673 let any_blanks = total_blank != 0;
674
675 s.effective_indent += indent;
676
677 if indent == -1 || m.pre_indent == -1 {
678 } else if indent > m.pre_indent {
681 s.penalty += if any_blanks {
682 RELATIVE_INDENT_WITH_BLANK_PENALTY
683 } else {
684 RELATIVE_INDENT_PENALTY
685 };
686 } else if indent == m.pre_indent {
687 } else if m.post_indent != -1 && m.post_indent > indent {
689 s.penalty += if any_blanks {
690 RELATIVE_OUTDENT_WITH_BLANK_PENALTY
691 } else {
692 RELATIVE_OUTDENT_PENALTY
693 };
694 } else {
695 s.penalty += if any_blanks {
696 RELATIVE_DEDENT_WITH_BLANK_PENALTY
697 } else {
698 RELATIVE_DEDENT_PENALTY
699 };
700 }
701}
702
703fn score_cmp(s1: &SplitScore, s2: &SplitScore) -> i32 {
705 let cmp_indents = (s1.effective_indent > s2.effective_indent) as i32
706 - (s1.effective_indent < s2.effective_indent) as i32;
707 INDENT_WEIGHT * cmp_indents + (s1.penalty - s2.penalty)
708}
709
710struct XdlGroup {
713 start: i64,
714 end: i64,
715}
716
717fn recs_match(xdf: &CompactFile, a: i64, b: i64) -> bool {
720 xdf.recs[a as usize] == xdf.recs[b as usize]
721}
722
723fn group_init(xdf: &CompactFile) -> XdlGroup {
725 let mut end = 0i64;
726 while xdf.changed(end) {
727 end += 1;
728 }
729 XdlGroup { start: 0, end }
730}
731
732fn group_next(xdf: &CompactFile, g: &mut XdlGroup) -> bool {
734 if g.end == xdf.nrec() {
735 return false;
736 }
737 g.start = g.end + 1;
738 g.end = g.start;
739 while xdf.changed(g.end) {
740 g.end += 1;
741 }
742 true
743}
744
745fn group_previous(xdf: &CompactFile, g: &mut XdlGroup) -> bool {
747 if g.start == 0 {
748 return false;
749 }
750 g.end = g.start - 1;
751 g.start = g.end;
752 while xdf.changed(g.start - 1) {
753 g.start -= 1;
754 }
755 true
756}
757
758fn group_slide_down(xdf: &mut CompactFile, g: &mut XdlGroup) -> bool {
762 if g.end < xdf.nrec() && recs_match(xdf, g.start, g.end) {
763 xdf.set_changed(g.start, false);
764 xdf.set_changed(g.end, true);
765 g.start += 1;
766 g.end += 1;
767 while xdf.changed(g.end) {
768 g.end += 1;
769 }
770 true
771 } else {
772 false
773 }
774}
775
776fn group_slide_up(xdf: &mut CompactFile, g: &mut XdlGroup) -> bool {
780 if g.start > 0 && recs_match(xdf, g.start - 1, g.end - 1) {
781 g.start -= 1;
782 g.end -= 1;
783 xdf.set_changed(g.start, true);
784 xdf.set_changed(g.end, false);
785 while xdf.changed(g.start - 1) {
786 g.start -= 1;
787 }
788 true
789 } else {
790 false
791 }
792}
793
794fn compact_one(xdf: &mut CompactFile, xdfo: &mut CompactFile, indent_heuristic: bool) {
801 let mut g = group_init(xdf);
802 let mut go = group_init(xdfo);
803
804 loop {
805 if g.end == g.start {
807 if !group_next(xdf, &mut g) {
808 break;
809 }
810 if !group_next(xdfo, &mut go) {
811 break;
812 }
813 continue;
814 }
815
816 let mut groupsize;
817 let mut earliest_end;
818 let mut end_matching_other;
819
820 loop {
821 groupsize = g.end - g.start;
822 end_matching_other = -1i64;
823
824 while group_slide_up(xdf, &mut g) {
826 let ok = group_previous(xdfo, &mut go);
827 debug_assert!(ok, "group sync broken sliding up");
828 }
829 earliest_end = g.end;
831 if go.end > go.start {
832 end_matching_other = g.end;
833 }
834 loop {
836 if !group_slide_down(xdf, &mut g) {
837 break;
838 }
839 let ok = group_next(xdfo, &mut go);
840 debug_assert!(ok, "group sync broken sliding down");
841 if go.end > go.start {
842 end_matching_other = g.end;
843 }
844 }
845 if groupsize == g.end - g.start {
846 break;
847 }
848 }
849
850 if g.end == earliest_end {
853 } else if end_matching_other != -1 {
855 while go.end == go.start {
859 let ok = group_slide_up(xdf, &mut g);
860 debug_assert!(ok, "match disappeared");
861 let ok = group_previous(xdfo, &mut go);
862 debug_assert!(ok, "group sync broken sliding to match");
863 }
864 } else if indent_heuristic {
865 let mut best_shift = -1i64;
867 let mut best_score = SplitScore::default();
868
869 let mut shift = earliest_end;
870 if g.end - groupsize - 1 > shift {
871 shift = g.end - groupsize - 1;
872 }
873 if g.end - INDENT_HEURISTIC_MAX_SLIDING > shift {
874 shift = g.end - INDENT_HEURISTIC_MAX_SLIDING;
875 }
876 while shift <= g.end {
877 let mut score = SplitScore::default();
878 let m = measure_split(xdf, shift);
879 score_add_split(&m, &mut score);
880 let m = measure_split(xdf, shift - groupsize);
881 score_add_split(&m, &mut score);
882 if best_shift == -1 || score_cmp(&score, &best_score) <= 0 {
883 best_score = score;
884 best_shift = shift;
885 }
886 shift += 1;
887 }
888
889 while g.end > best_shift {
890 let ok = group_slide_up(xdf, &mut g);
891 debug_assert!(ok, "best shift unreached");
892 let ok = group_previous(xdfo, &mut go);
893 debug_assert!(ok, "group sync broken sliding to blank line");
894 }
895 }
896
897 if !group_next(xdf, &mut g) {
899 break;
900 }
901 if !group_next(xdfo, &mut go) {
902 break;
903 }
904 }
905}
906
907fn change_compact(
913 ops: &mut Vec<DiffOp>,
914 old: &[DiffLine<'_>],
915 new: &[DiffLine<'_>],
916 ws_ignore: WsIgnore,
917 indent_heuristic: bool,
918) {
919 if ops.iter().all(|op| matches!(op, DiffOp::Equal(_))) {
921 return;
922 }
923
924 let canon = |lines: &[DiffLine<'_>]| -> Vec<Vec<u8>> {
926 if ws_ignore.is_empty() {
927 lines.iter().map(|l| l.content.to_vec()).collect()
928 } else {
929 lines
930 .iter()
931 .map(|l| crate::canonicalize_line_for_match(l.content, ws_ignore))
932 .collect()
933 }
934 };
935
936 let mut xdf1 = CompactFile {
937 recs: canon(old),
938 changed: vec![false; old.len()],
939 };
940 let mut xdf2 = CompactFile {
941 recs: canon(new),
942 changed: vec![false; new.len()],
943 };
944
945 let mut oi = 0usize;
947 let mut ni = 0usize;
948 for op in ops.iter() {
949 match *op {
950 DiffOp::Equal(n) => {
951 oi += n;
952 ni += n;
953 }
954 DiffOp::Delete(n) => {
955 for _ in 0..n {
956 xdf1.changed[oi] = true;
957 oi += 1;
958 }
959 }
960 DiffOp::Insert(n) => {
961 for _ in 0..n {
962 xdf2.changed[ni] = true;
963 ni += 1;
964 }
965 }
966 }
967 }
968
969 compact_one(&mut xdf1, &mut xdf2, indent_heuristic);
971 compact_one(&mut xdf2, &mut xdf1, indent_heuristic);
972
973 let n_old = xdf1.changed.len();
976 let n_new = xdf2.changed.len();
977 let mut rebuilt: Vec<DiffOp> = Vec::with_capacity(ops.len());
978 let mut i = 0usize; let mut j = 0usize; while i < n_old || j < n_new {
981 let del = i < n_old && xdf1.changed[i];
982 let ins = j < n_new && xdf2.changed[j];
983 if del {
984 let mut run = 0usize;
985 while i < n_old && xdf1.changed[i] {
986 run += 1;
987 i += 1;
988 }
989 push_op(&mut rebuilt, DiffOp::Delete(run));
990 } else if ins {
991 let mut run = 0usize;
992 while j < n_new && xdf2.changed[j] {
993 run += 1;
994 j += 1;
995 }
996 push_op(&mut rebuilt, DiffOp::Insert(run));
997 } else {
998 let mut run = 0usize;
1000 while i < n_old && j < n_new && !xdf1.changed[i] && !xdf2.changed[j] {
1001 run += 1;
1002 i += 1;
1003 j += 1;
1004 }
1005 debug_assert!(run > 0, "change_compact stalled rebuilding script");
1006 push_op(&mut rebuilt, DiffOp::Equal(run));
1007 }
1008 }
1009
1010 *ops = rebuilt;
1011}
1012
1013fn push_op(out: &mut Vec<DiffOp>, op: DiffOp) {
1015 match (out.last_mut(), op) {
1016 (Some(DiffOp::Equal(prev)), DiffOp::Equal(n)) => *prev += n,
1017 (Some(DiffOp::Delete(prev)), DiffOp::Delete(n)) => *prev += n,
1018 (Some(DiffOp::Insert(prev)), DiffOp::Insert(n)) => *prev += n,
1019 _ => out.push(op),
1020 }
1021}
1022
1023struct RangeFilter<'r> {
1030 ranges: &'r [LineRange],
1031 cur_range: usize,
1032 lno_post: i64,
1034 lno_pre: i64,
1035 hunk_old_count: i64,
1037 hunk_new_count: i64,
1038 func: Vec<u8>,
1041 rhunk: Vec<u8>,
1043 rhunk_old_begin: i64,
1044 rhunk_old_count: i64,
1045 rhunk_new_begin: i64,
1046 rhunk_new_count: i64,
1047 rhunk_active: bool,
1048 rhunk_has_changes: bool,
1049 pending_rm: Vec<u8>,
1051 pending_rm_count: i64,
1052 pending_rm_pre_begin: i64,
1053}
1054
1055#[derive(Clone, Copy, PartialEq, Eq)]
1056enum RangeBodyKind {
1057 Context,
1058 Delete,
1059 Insert,
1060 Change,
1061 NoNewline,
1062}
1063
1064impl RangeFilter<'_> {
1065 fn discard_pending_rm(&mut self) {
1066 self.pending_rm.clear();
1067 self.pending_rm_count = 0;
1068 }
1069
1070 fn flush_rhunk(&mut self, out: &mut Vec<u8>) {
1073 if !self.rhunk_active {
1074 return;
1075 }
1076 if self.pending_rm_count != 0 {
1077 self.rhunk.extend_from_slice(&self.pending_rm);
1078 self.rhunk_old_count += self.pending_rm_count;
1079 self.rhunk_has_changes = true;
1080 self.discard_pending_rm();
1081 }
1082 if !self.rhunk_has_changes {
1083 self.rhunk_active = false;
1084 self.rhunk.clear();
1085 return;
1086 }
1087 out.extend_from_slice(
1091 format!(
1092 "@@ -{},{} +{},{} @@",
1093 self.rhunk_old_begin,
1094 self.rhunk_old_count,
1095 self.rhunk_new_begin,
1096 self.rhunk_new_count
1097 )
1098 .as_bytes(),
1099 );
1100 if !self.func.is_empty() {
1101 out.push(b' ');
1102 out.extend_from_slice(&self.func);
1103 }
1104 out.push(b'\n');
1105 out.extend_from_slice(&self.rhunk);
1106 self.rhunk_active = false;
1107 self.rhunk.clear();
1108 }
1109
1110 fn body_line(&mut self, out: &mut Vec<u8>, kind: RangeBodyKind, line: &[u8]) {
1114 if kind == RangeBodyKind::Delete {
1115 if self.pending_rm_count == 0 {
1116 self.pending_rm_pre_begin = self.lno_pre;
1117 }
1118 self.lno_pre += 1;
1119 self.pending_rm.extend_from_slice(line);
1120 self.pending_rm_count += 1;
1121 return;
1122 }
1123 if kind == RangeBodyKind::NoNewline {
1124 if self.pending_rm_count != 0 {
1125 self.pending_rm.extend_from_slice(line);
1126 } else if self.rhunk_active {
1127 self.rhunk.extend_from_slice(line);
1128 }
1129 return;
1130 }
1131 let lno_0 = self.lno_post - 1;
1135 let cur_pre = self.lno_pre;
1136 self.lno_post += 1;
1137 if matches!(kind, RangeBodyKind::Context | RangeBodyKind::Change) {
1138 self.lno_pre += 1;
1139 }
1140
1141 while self.cur_range < self.ranges.len() && lno_0 >= self.ranges[self.cur_range].end {
1142 if self.rhunk_active {
1143 self.flush_rhunk(out);
1144 }
1145 self.discard_pending_rm();
1146 self.cur_range += 1;
1147 }
1148 if self.cur_range >= self.ranges.len() {
1149 self.discard_pending_rm();
1150 return;
1151 }
1152 let cur = self.ranges[self.cur_range];
1153 if lno_0 < cur.start {
1154 self.discard_pending_rm();
1155 return;
1156 }
1157 if !self.rhunk_active {
1158 self.rhunk_active = true;
1159 self.rhunk_has_changes = false;
1160 self.rhunk_new_begin = lno_0 + 1;
1161 self.rhunk_old_begin = if self.pending_rm_count != 0 {
1162 self.pending_rm_pre_begin
1163 } else {
1164 cur_pre
1165 };
1166 self.rhunk_old_count = 0;
1167 self.rhunk_new_count = 0;
1168 self.rhunk.clear();
1169 }
1170 if self.pending_rm_count != 0 {
1171 self.rhunk.extend_from_slice(&self.pending_rm);
1172 self.rhunk_old_count += self.pending_rm_count;
1173 self.rhunk_has_changes = true;
1174 self.discard_pending_rm();
1175 }
1176 self.rhunk.extend_from_slice(line);
1177 self.rhunk_new_count += 1;
1178 if matches!(kind, RangeBodyKind::Insert | RangeBodyKind::Change) {
1179 self.rhunk_has_changes = true;
1180 }
1181 if matches!(kind, RangeBodyKind::Context | RangeBodyKind::Change) {
1182 self.rhunk_old_count += 1;
1183 }
1184 }
1185}
1186
1187fn skip_ansi_prefix(mut line: &[u8]) -> &[u8] {
1188 loop {
1189 let Some(rest) = line.strip_prefix(b"\x1b[") else {
1190 return line;
1191 };
1192 let Some(end) = rest.iter().position(|&b| (0x40..=0x7e).contains(&b)) else {
1193 return line;
1194 };
1195 line = &rest[end + 1..];
1196 }
1197}
1198
1199fn strip_ansi(line: &[u8]) -> Vec<u8> {
1200 let mut stripped = Vec::with_capacity(line.len());
1201 let mut idx = 0usize;
1202 while idx < line.len() {
1203 if line[idx] == b'\x1b'
1204 && line.get(idx + 1) == Some(&b'[')
1205 && let Some(end) = line[idx + 2..]
1206 .iter()
1207 .position(|&b| (0x40..=0x7e).contains(&b))
1208 {
1209 idx += end + 3;
1210 continue;
1211 }
1212 stripped.push(line[idx]);
1213 idx += 1;
1214 }
1215 stripped
1216}
1217
1218fn classify_word_diff_line(visible: &[u8]) -> RangeBodyKind {
1219 if visible.starts_with(b"\\") {
1220 return RangeBodyKind::NoNewline;
1221 }
1222 let has_old = find_subslice(visible, b"[-").is_some();
1223 let has_new = find_subslice(visible, b"{+").is_some();
1224 match (has_old, has_new) {
1225 (true, false) if visible.starts_with(b"[-") => return RangeBodyKind::Delete,
1226 (false, true) if visible.starts_with(b"{+") => return RangeBodyKind::Insert,
1227 (true, _) | (_, true) => return RangeBodyKind::Change,
1228 _ => {}
1229 }
1230 match visible.first().copied() {
1232 Some(b'-') => RangeBodyKind::Delete,
1233 Some(b'+') => RangeBodyKind::Insert,
1234 Some(b' ') => RangeBodyKind::Context,
1235 Some(b'~') => RangeBodyKind::NoNewline,
1236 _ => RangeBodyKind::Context,
1237 }
1238}
1239
1240fn classify_range_body_line(
1241 line: &[u8],
1242 indicators: LineIndicators,
1243 word_diff: bool,
1244 hunk_old_count: i64,
1245 hunk_new_count: i64,
1246) -> RangeBodyKind {
1247 let visible = skip_ansi_prefix(line);
1248 if word_diff {
1249 if hunk_old_count == 0 {
1250 return RangeBodyKind::Insert;
1251 }
1252 if hunk_new_count == 0 {
1253 return RangeBodyKind::Delete;
1254 }
1255 return classify_word_diff_line(visible);
1256 }
1257 match visible.first().copied() {
1258 Some(b'\\') => RangeBodyKind::NoNewline,
1259 Some(marker) if marker == indicators.old => RangeBodyKind::Delete,
1260 Some(marker) if marker == indicators.new => RangeBodyKind::Insert,
1261 Some(marker) if marker == indicators.context => RangeBodyKind::Context,
1262 _ => RangeBodyKind::Context,
1263 }
1264}
1265
1266fn filter_hunks_to_ranges(
1272 out: &mut Vec<u8>,
1273 full: &[u8],
1274 ranges: &[LineRange],
1275 line_indicators: LineIndicators,
1276 word_diff: bool,
1277) {
1278 if ranges.is_empty() {
1279 return;
1280 }
1281 let mut filter = RangeFilter {
1282 ranges,
1283 cur_range: 0,
1284 lno_post: 0,
1285 lno_pre: 0,
1286 hunk_old_count: 0,
1287 hunk_new_count: 0,
1288 func: Vec::new(),
1289 rhunk: Vec::new(),
1290 rhunk_old_begin: 0,
1291 rhunk_old_count: 0,
1292 rhunk_new_begin: 0,
1293 rhunk_new_count: 0,
1294 rhunk_active: false,
1295 rhunk_has_changes: false,
1296 pending_rm: Vec::new(),
1297 pending_rm_count: 0,
1298 pending_rm_pre_begin: 0,
1299 };
1300 for line in split_keep_newline(full) {
1301 let parse_line = if line.starts_with(b"@@ ") {
1302 line.to_vec()
1303 } else {
1304 strip_ansi(line)
1305 };
1306 if parse_line.starts_with(b"@@ ") {
1307 if let Some((old_begin, old_count, new_begin, new_count, func)) =
1312 parse_hunk_header(&parse_line)
1313 {
1314 filter.lno_post = new_begin;
1315 filter.lno_pre = old_begin;
1316 filter.hunk_old_count = old_count;
1317 filter.hunk_new_count = new_count;
1318 filter.func = func;
1319 }
1320 continue;
1321 }
1322 let kind = classify_range_body_line(
1323 line,
1324 line_indicators,
1325 word_diff,
1326 filter.hunk_old_count,
1327 filter.hunk_new_count,
1328 );
1329 filter.body_line(out, kind, line);
1330 }
1331 filter.flush_rhunk(out);
1332}
1333
1334fn split_keep_newline(buf: &[u8]) -> impl Iterator<Item = &[u8]> {
1337 let mut start = 0usize;
1338 std::iter::from_fn(move || {
1339 if start >= buf.len() {
1340 return None;
1341 }
1342 let rel = buf[start..].iter().position(|&b| b == b'\n');
1343 let end = match rel {
1344 Some(pos) => start + pos + 1,
1345 None => buf.len(),
1346 };
1347 let line = &buf[start..end];
1348 start = end;
1349 Some(line)
1350 })
1351}
1352
1353fn parse_hunk_header(line: &[u8]) -> Option<(i64, i64, i64, i64, Vec<u8>)> {
1358 let rest = line.strip_prefix(b"@@ -")?;
1360 let plus = rest.iter().position(|&b| b == b'+')?;
1361 let old_part = &rest[..plus];
1362 let after_plus = &rest[plus + 1..];
1364 let close = find_subslice(after_plus, b" @@")?;
1365 let new_part = &after_plus[..close];
1366 let (old_begin, old_count) = parse_range_side(old_part)?;
1367 let (new_begin, new_count) = parse_range_side(new_part)?;
1368 let tail = &after_plus[close + 3..];
1370 let func = if let Some(f) = tail.strip_prefix(b" ") {
1371 let mut f = f.to_vec();
1372 if f.last() == Some(&b'\n') {
1373 f.pop();
1374 }
1375 f
1376 } else {
1377 Vec::new()
1378 };
1379 Some((old_begin, old_count, new_begin, new_count, func))
1380}
1381
1382fn parse_range_side(field: &[u8]) -> Option<(i64, i64)> {
1385 let field = field.split(|&b| b == b' ').next().unwrap_or(field);
1386 let mut parts = field.splitn(2, |&b| b == b',');
1387 let begin = std::str::from_utf8(parts.next()?)
1388 .ok()?
1389 .trim()
1390 .parse::<i64>()
1391 .ok()?;
1392 let count = match parts.next() {
1393 Some(count) => std::str::from_utf8(count)
1394 .ok()?
1395 .trim()
1396 .parse::<i64>()
1397 .ok()?,
1398 None => 1,
1399 };
1400 Some((begin, count))
1401}
1402
1403fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
1404 if needle.is_empty() || haystack.len() < needle.len() {
1405 return None;
1406 }
1407 (0..=haystack.len() - needle.len()).find(|&i| &haystack[i..i + needle.len()] == needle)
1408}
1409
1410#[derive(Clone, Copy)]
1415struct Change {
1416 i1: usize,
1418 chg1: usize,
1420 i2: usize,
1422 chg2: usize,
1424 tag_first: usize,
1426 tag_last: usize,
1428 ignore: bool,
1430}
1431
1432fn build_changes(tagged: &[TaggedLine<'_>]) -> Vec<Change> {
1435 let mut changes: Vec<Change> = Vec::new();
1436 let mut idx = 0usize;
1437 while idx < tagged.len() {
1438 if tagged[idx].kind == LineKind::Context {
1439 idx += 1;
1440 continue;
1441 }
1442 let tag_first = idx;
1443 let i1 = tagged[idx].old_index;
1444 let i2 = tagged[idx].new_index;
1445 let mut chg1 = 0usize;
1446 let mut chg2 = 0usize;
1447 while idx < tagged.len() && tagged[idx].kind != LineKind::Context {
1448 match tagged[idx].kind {
1449 LineKind::Delete => chg1 += 1,
1450 LineKind::Insert => chg2 += 1,
1451 LineKind::Context => unreachable!(),
1452 }
1453 idx += 1;
1454 }
1455 changes.push(Change {
1456 i1,
1457 chg1,
1458 i2,
1459 chg2,
1460 tag_first,
1461 tag_last: idx - 1,
1462 ignore: false,
1463 });
1464 }
1465 changes
1466}
1467
1468fn mark_ignorable_changes(
1473 changes: &mut [Change],
1474 old: &[DiffLine<'_>],
1475 new: &[DiffLine<'_>],
1476 ws_ignore: WsIgnore,
1477 ci: &ChangeIgnore<'_>,
1478) {
1479 for change in changes.iter_mut() {
1480 if ci.ignore_blank_lines {
1481 let blank = (change.i1..change.i1 + change.chg1)
1482 .all(|i| line_is_blank(old[i].content, ws_ignore))
1483 && (change.i2..change.i2 + change.chg2)
1484 .all(|i| line_is_blank(new[i].content, ws_ignore));
1485 change.ignore = blank;
1486 }
1487 if !change.ignore
1488 && let Some(regex_match) = ci.regex_match
1489 {
1490 let matched = (change.i1..change.i1 + change.chg1).all(|i| regex_match(old[i].content))
1491 && (change.i2..change.i2 + change.chg2).all(|i| regex_match(new[i].content));
1492 change.ignore = matched;
1493 }
1494 }
1495}
1496
1497fn group_changes_into_hunks(
1504 changes: &[Change],
1505 context: usize,
1506 interhunk: usize,
1507) -> Vec<(usize, usize)> {
1508 let max_common = context.saturating_add(context).saturating_add(interhunk);
1509 let max_ignorable = context;
1510
1511 let mut hunks: Vec<(usize, usize)> = Vec::new();
1512 let mut start = 0usize;
1515 while start < changes.len() {
1516 {
1524 let mut xchp = start;
1525 while xchp < changes.len() && changes[xchp].ignore {
1526 let cur = &changes[xchp];
1527 match changes.get(xchp + 1) {
1528 None => {
1529 start = changes.len();
1530 }
1531 Some(next) => {
1532 if next.i1 - (cur.i1 + cur.chg1) >= max_ignorable {
1533 start = xchp + 1;
1534 }
1535 }
1536 }
1537 xchp += 1;
1538 }
1539 }
1540 if start >= changes.len() {
1541 break;
1542 }
1543
1544 let mut last = start;
1547 let mut ignored = 0usize; let mut prev = start;
1549 let mut idx = start + 1;
1550 while idx < changes.len() {
1551 let xch = &changes[idx];
1552 let xchp = &changes[prev];
1553 let distance = xch.i1 - (xchp.i1 + xchp.chg1);
1554 if distance > max_common {
1555 break;
1556 }
1557 if distance < max_ignorable && (!xch.ignore || last == prev) {
1558 last = idx;
1559 ignored = 0;
1560 } else if distance < max_ignorable && xch.ignore {
1561 ignored += xch.chg2;
1562 } else if last != prev
1563 && xch.i1 + ignored - (changes[last].i1 + changes[last].chg1) > max_common
1564 {
1565 break;
1566 } else if !xch.ignore {
1567 last = idx;
1568 ignored = 0;
1569 } else {
1570 ignored += xch.chg2;
1571 }
1572 prev = idx;
1573 idx += 1;
1574 }
1575
1576 let first_change = &changes[start];
1577 let last_change = &changes[last];
1578 hunks.push((first_change.tag_first, last_change.tag_last));
1579 start = last + 1;
1580 }
1581
1582 hunks
1583}
1584
1585fn expand_hunks_to_function_context(
1586 groups: &[(usize, usize)],
1587 tagged: &[TaggedLine<'_>],
1588 old: &[DiffLine<'_>],
1589 new: &[DiffLine<'_>],
1590 mut heading: Option<&mut HeadingFn<'_>>,
1591) -> Vec<(usize, usize)> {
1592 let Some(classifier) = heading.as_mut() else {
1593 return groups.to_vec();
1594 };
1595 let mut expanded = Vec::with_capacity(groups.len());
1596 for &(start, end) in groups {
1597 let first = tagged[start];
1598 let last = tagged[end];
1599 let old_changed = tagged[start..=end]
1600 .iter()
1601 .any(|line| line.kind == LineKind::Delete);
1602 let (side, range) = if old_changed {
1603 (
1604 FunctionSide::Old,
1605 function_context_range(old, first.old_index, false, classifier),
1606 )
1607 } else {
1608 (
1609 FunctionSide::New,
1610 function_context_range(new, first.new_index, true, classifier),
1611 )
1612 };
1613 let Some((range_start, range_end)) = range else {
1614 expanded.push((start, end));
1615 continue;
1616 };
1617 let mut hunk_start = expand_tag_start(tagged, start, side, range_start);
1618 let mut hunk_end = expand_tag_end(tagged, end, side, range_end);
1619 if old_changed {
1620 if last.old_index >= range_end {
1621 hunk_end = end;
1622 }
1623 } else if last.new_index >= range_end {
1624 hunk_end = end;
1625 }
1626 if hunk_start > start {
1627 hunk_start = start;
1628 }
1629 if hunk_end < end {
1630 hunk_end = end;
1631 }
1632 if let Some(prev) = expanded.last_mut()
1633 && hunk_start <= prev.1 + 1
1634 {
1635 prev.1 = prev.1.max(hunk_end);
1636 continue;
1637 }
1638 expanded.push((hunk_start, hunk_end));
1639 }
1640 expanded
1641}
1642
1643#[derive(Clone, Copy)]
1644enum FunctionSide {
1645 Old,
1646 New,
1647}
1648
1649fn function_context_range(
1650 lines: &[DiffLine<'_>],
1651 anchor: usize,
1652 prefer_forward: bool,
1653 heading: &mut HeadingFn<'_>,
1654) -> Option<(usize, usize)> {
1655 if lines.is_empty() {
1656 return None;
1657 }
1658 let anchor = anchor.min(lines.len() - 1);
1659 let mut heading_idx = None;
1660 for idx in (0..=anchor).rev() {
1661 if heading(lines[idx].content).is_some() {
1662 heading_idx = Some(idx);
1663 break;
1664 }
1665 }
1666 if heading_idx.is_none() && prefer_forward {
1667 for (idx, line) in lines.iter().enumerate().skip(anchor) {
1668 if heading(line.content).is_some() {
1669 heading_idx = Some(idx);
1670 break;
1671 }
1672 }
1673 }
1674
1675 let (mut start, mut end) = if let Some(idx) = heading_idx {
1676 let mut start = idx;
1677 while start > 0 && !line_is_blank(lines[start - 1].content, WsIgnore::default()) {
1678 start -= 1;
1679 }
1680 let mut end = lines.len();
1681 for (next, line) in lines.iter().enumerate().skip(idx + 1) {
1682 if heading(line.content).is_some() {
1683 end = next;
1684 break;
1685 }
1686 }
1687 (start, end)
1688 } else {
1689 (0, lines.len())
1690 };
1691
1692 while start < end && line_is_blank(lines[start].content, WsIgnore::default()) {
1693 start += 1;
1694 }
1695 while end > start && line_is_blank(lines[end - 1].content, WsIgnore::default()) {
1696 end -= 1;
1697 }
1698 (start < end).then_some((start, end))
1699}
1700
1701fn expand_tag_start(
1702 tagged: &[TaggedLine<'_>],
1703 current: usize,
1704 side: FunctionSide,
1705 range_start: usize,
1706) -> usize {
1707 let mut start = current;
1708 while start > 0 {
1709 let prev = tagged[start - 1];
1710 let line_index = match side {
1711 FunctionSide::Old => prev.old_index,
1712 FunctionSide::New => prev.new_index,
1713 };
1714 if line_index < range_start {
1715 break;
1716 }
1717 start -= 1;
1718 }
1719 start
1720}
1721
1722fn expand_tag_end(
1723 tagged: &[TaggedLine<'_>],
1724 current: usize,
1725 side: FunctionSide,
1726 range_end: usize,
1727) -> usize {
1728 let mut end = current;
1729 while end + 1 < tagged.len() {
1730 let next = tagged[end + 1];
1731 let line_index = match side {
1732 FunctionSide::Old => next.old_index,
1733 FunctionSide::New => next.new_index,
1734 };
1735 if line_index >= range_end {
1736 break;
1737 }
1738 end += 1;
1739 }
1740 end
1741}
1742
1743const COLOR_MOVED_MIN_ALNUM_COUNT: usize = 20;
1744const INDENT_BLANKLINE: i32 = i32::MIN;
1745
1746#[derive(Clone, Copy, Default)]
1747struct MovedStyle {
1748 moved: bool,
1749 alt: bool,
1750 uninteresting: bool,
1751}
1752
1753#[derive(Clone)]
1754struct MovedEntry {
1755 tag_idx: usize,
1756 next_line: Option<usize>,
1757 next_match: Option<usize>,
1758 id: usize,
1759 indent_width: i32,
1760}
1761
1762#[derive(Clone, Copy, Default)]
1763struct MovedEntryList {
1764 add: Option<usize>,
1765 del: Option<usize>,
1766}
1767
1768#[derive(Clone, Copy)]
1769struct MovedBlock {
1770 match_entry: usize,
1771 wsd: i32,
1772}
1773
1774fn mark_color_as_moved(tagged: &[TaggedLine<'_>], color_moved: ColorMoved) -> Vec<MovedStyle> {
1775 let (entries, entry_for_tag, entry_list) = add_lines_to_move_detection(tagged, color_moved.ws);
1776 let mut styles = vec![MovedStyle::default(); tagged.len()];
1777 let mut pmb: Vec<MovedBlock> = Vec::new();
1778 let mut n = 0usize;
1779 let mut flipped_block = false;
1780 let mut block_length = 0usize;
1781 let mut moved_symbol: Option<LineKind> = None;
1782
1783 while n < tagged.len() {
1784 let line = tagged[n];
1785 let line_entry = entry_for_tag[n];
1786 let mut line_match = line_entry.and_then(|entry_idx| {
1787 let id = entries[entry_idx].id;
1788 match line.kind {
1789 LineKind::Insert => entry_list.get(id).and_then(|list| list.del),
1790 LineKind::Delete => entry_list.get(id).and_then(|list| list.add),
1791 LineKind::Context => None,
1792 }
1793 });
1794
1795 if line.kind == LineKind::Context {
1796 flipped_block = false;
1797 }
1798
1799 if !pmb.is_empty() && (line_match.is_none() || Some(line.kind) != moved_symbol) {
1800 if !adjust_last_block(&mut styles, tagged, color_moved.mode, n, block_length)
1801 && block_length > 1
1802 {
1803 line_match = None;
1804 n -= block_length;
1805 }
1806 pmb.clear();
1807 block_length = 0;
1808 flipped_block = false;
1809 }
1810
1811 let Some(line_match) = line_match else {
1812 moved_symbol = None;
1813 n += 1;
1814 continue;
1815 };
1816
1817 if color_moved.mode == ColorMovedMode::Plain {
1818 styles[n].moved = true;
1819 n += 1;
1820 continue;
1821 }
1822
1823 pmb_advance_or_null(
1824 &mut pmb,
1825 &entries,
1826 tagged,
1827 line_entry.expect("plus/minus line has move-detection entry"),
1828 color_moved.ws,
1829 );
1830
1831 if pmb.is_empty() {
1832 let contiguous =
1833 adjust_last_block(&mut styles, tagged, color_moved.mode, n, block_length);
1834 if !contiguous && block_length > 1 {
1835 n -= block_length;
1836 } else {
1837 fill_potential_moved_blocks(
1838 line_match,
1839 &entries,
1840 tagged,
1841 n,
1842 color_moved.ws,
1843 &mut pmb,
1844 );
1845 }
1846
1847 if contiguous && !pmb.is_empty() && moved_symbol == Some(line.kind) {
1848 flipped_block = !flipped_block;
1849 } else {
1850 flipped_block = false;
1851 }
1852
1853 moved_symbol = (!pmb.is_empty()).then_some(line.kind);
1854 block_length = 0;
1855 }
1856
1857 if !pmb.is_empty() {
1858 block_length += 1;
1859 styles[n].moved = true;
1860 if flipped_block && color_moved.mode != ColorMovedMode::Blocks {
1861 styles[n].alt = true;
1862 }
1863 }
1864 n += 1;
1865 }
1866
1867 adjust_last_block(&mut styles, tagged, color_moved.mode, n, block_length);
1868 if color_moved.mode == ColorMovedMode::DimmedZebra {
1869 dim_moved_lines(&mut styles, tagged);
1870 }
1871 styles
1872}
1873
1874fn add_lines_to_move_detection(
1875 tagged: &[TaggedLine<'_>],
1876 ws: ColorMovedWs,
1877) -> (Vec<MovedEntry>, Vec<Option<usize>>, Vec<MovedEntryList>) {
1878 let mut entries: Vec<MovedEntry> = Vec::new();
1879 let mut entry_for_tag = vec![None; tagged.len()];
1880 let mut entry_list = Vec::<MovedEntryList>::new();
1881 let mut interned = HashMap::<Vec<u8>, usize>::new();
1882 let mut prev_line: Option<usize> = None;
1883
1884 for (tag_idx, line) in tagged.iter().enumerate() {
1885 if line.kind != LineKind::Insert && line.kind != LineKind::Delete {
1886 prev_line = None;
1887 continue;
1888 }
1889 let indent = if ws.allow_indentation_change {
1890 moved_indent_data(line.content)
1891 } else {
1892 (0, 0)
1893 };
1894 let key = moved_line_key(line.content, indent.0, ws.ignore);
1895 let id = match interned.get(&key) {
1896 Some(id) => *id,
1897 None => {
1898 let id = interned.len();
1899 interned.insert(key, id);
1900 entry_list.push(MovedEntryList::default());
1901 id
1902 }
1903 };
1904
1905 let entry_idx = entries.len();
1906 if let Some(prev) = prev_line
1907 && tagged[entries[prev].tag_idx].kind == line.kind
1908 {
1909 entries[prev].next_line = Some(entry_idx);
1910 }
1911 let next_match = match line.kind {
1912 LineKind::Insert => entry_list[id].add,
1913 LineKind::Delete => entry_list[id].del,
1914 LineKind::Context => None,
1915 };
1916 entries.push(MovedEntry {
1917 tag_idx,
1918 next_line: None,
1919 next_match,
1920 id,
1921 indent_width: indent.1,
1922 });
1923 match line.kind {
1924 LineKind::Insert => entry_list[id].add = Some(entry_idx),
1925 LineKind::Delete => entry_list[id].del = Some(entry_idx),
1926 LineKind::Context => {}
1927 }
1928 entry_for_tag[tag_idx] = Some(entry_idx);
1929 prev_line = Some(entry_idx);
1930 }
1931
1932 (entries, entry_for_tag, entry_list)
1933}
1934
1935fn moved_line_key(line: &[u8], indent_off: usize, ignore: WsIgnore) -> Vec<u8> {
1936 let bytes = line.get(indent_off..).unwrap_or_default();
1937 if ignore.is_empty() {
1938 bytes.to_vec()
1939 } else {
1940 canonicalize_moved_line(bytes, ignore)
1941 }
1942}
1943
1944fn canonicalize_moved_line(line: &[u8], ignore: WsIgnore) -> Vec<u8> {
1945 let is_ws = |b: u8| matches!(b, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c);
1946 if ignore.all_space {
1947 return line.iter().copied().filter(|b| !is_ws(*b)).collect();
1948 }
1949 if ignore.space_change {
1950 let mut out = Vec::with_capacity(line.len());
1951 let mut i = 0usize;
1952 while i < line.len() {
1953 if is_ws(line[i]) {
1954 out.push(b' ');
1955 while i < line.len() && is_ws(line[i]) {
1956 i += 1;
1957 }
1958 } else {
1959 out.push(line[i]);
1960 i += 1;
1961 }
1962 }
1963 while out.last() == Some(&b' ') {
1964 out.pop();
1965 }
1966 return out;
1967 }
1968 if ignore.space_at_eol {
1969 let mut end = line.len();
1970 if end > 0 && line[end - 1] == b'\n' {
1971 end -= 1;
1972 }
1973 while end > 0 && matches!(line[end - 1], b' ' | b'\t') {
1974 end -= 1;
1975 }
1976 let mut out = line[..end].to_vec();
1977 if line.ends_with(b"\n") {
1978 out.push(b'\n');
1979 }
1980 return out;
1981 }
1982 if ignore.cr_at_eol {
1983 let mut out = line.to_vec();
1984 if out.ends_with(b"\r\n") {
1985 let len = out.len();
1986 out.remove(len - 2);
1987 }
1988 return out;
1989 }
1990 line.to_vec()
1991}
1992
1993fn moved_indent_data(line: &[u8]) -> (usize, i32) {
1994 let mut off = 0usize;
1995 let mut width = 0i32;
1996 while off < line.len() && matches!(line[off], b'\x0c' | b'\x0b' | b'\r') {
1997 off += 1;
1998 }
1999 while off < line.len() {
2000 match line[off] {
2001 b' ' => {
2002 width += 1;
2003 off += 1;
2004 }
2005 b'\t' => {
2006 width += 8 - (width % 8);
2007 off += 1;
2008 while off < line.len() && line[off] == b'\t' {
2009 width += 8;
2010 off += 1;
2011 }
2012 }
2013 _ => break,
2014 }
2015 }
2016 if line[off..].iter().all(|b| b.is_ascii_whitespace()) {
2017 (line.len(), INDENT_BLANKLINE)
2018 } else {
2019 (off, width)
2020 }
2021}
2022
2023fn pmb_advance_or_null(
2024 pmb: &mut Vec<MovedBlock>,
2025 entries: &[MovedEntry],
2026 tagged: &[TaggedLine<'_>],
2027 line_entry: usize,
2028 ws: ColorMovedWs,
2029) {
2030 let mut kept = Vec::with_capacity(pmb.len());
2031 for mut block in pmb.iter().copied() {
2032 let Some(cur) = entries[block.match_entry].next_line else {
2033 continue;
2034 };
2035 let matched = if ws.allow_indentation_change {
2036 !cmp_in_block_with_wsd(entries, cur, tagged, line_entry, &mut block)
2037 } else {
2038 entries[cur].id == entries[line_entry].id
2039 };
2040 if matched {
2041 block.match_entry = cur;
2042 kept.push(block);
2043 }
2044 }
2045 *pmb = kept;
2046}
2047
2048fn fill_potential_moved_blocks(
2049 mut line_match: usize,
2050 entries: &[MovedEntry],
2051 tagged: &[TaggedLine<'_>],
2052 tag_idx: usize,
2053 ws: ColorMovedWs,
2054 pmb: &mut Vec<MovedBlock>,
2055) {
2056 loop {
2057 let entry = &entries[line_match];
2058 let wsd = if ws.allow_indentation_change {
2059 compute_ws_delta(tagged[tag_idx].content, entry.indent_width)
2060 } else {
2061 0
2062 };
2063 pmb.push(MovedBlock {
2064 match_entry: line_match,
2065 wsd,
2066 });
2067 match entry.next_match {
2068 Some(next) => line_match = next,
2069 None => break,
2070 }
2071 }
2072}
2073
2074fn compute_ws_delta(line: &[u8], match_indent_width: i32) -> i32 {
2075 let (_, width) = moved_indent_data(line);
2076 if width == INDENT_BLANKLINE && match_indent_width == INDENT_BLANKLINE {
2077 INDENT_BLANKLINE
2078 } else {
2079 width - match_indent_width
2080 }
2081}
2082
2083fn cmp_in_block_with_wsd(
2084 entries: &[MovedEntry],
2085 cur: usize,
2086 tagged: &[TaggedLine<'_>],
2087 line_entry: usize,
2088 block: &mut MovedBlock,
2089) -> bool {
2090 let cur_entry = &entries[cur];
2091 if cur_entry.id != entries[line_entry].id {
2092 return true;
2093 }
2094 if cur_entry.indent_width == INDENT_BLANKLINE {
2095 return false;
2096 }
2097 let (_, line_width) = moved_indent_data(tagged[entries[line_entry].tag_idx].content);
2098 let delta = line_width - cur_entry.indent_width;
2099 if block.wsd == INDENT_BLANKLINE {
2100 block.wsd = delta;
2101 }
2102 delta != block.wsd
2103}
2104
2105fn adjust_last_block(
2106 styles: &mut [MovedStyle],
2107 tagged: &[TaggedLine<'_>],
2108 mode: ColorMovedMode,
2109 n: usize,
2110 block_length: usize,
2111) -> bool {
2112 if mode == ColorMovedMode::Plain {
2113 return block_length != 0;
2114 }
2115 let mut alnum_count = 0usize;
2116 for i in 1..=block_length {
2117 for byte in tagged[n - i].content {
2118 if byte.is_ascii_alphanumeric() {
2119 alnum_count += 1;
2120 if alnum_count >= COLOR_MOVED_MIN_ALNUM_COUNT {
2121 return true;
2122 }
2123 }
2124 }
2125 }
2126 for i in 1..=block_length {
2127 styles[n - i] = MovedStyle::default();
2128 }
2129 false
2130}
2131
2132fn dim_moved_lines(styles: &mut [MovedStyle], tagged: &[TaggedLine<'_>]) {
2133 for n in 0..tagged.len() {
2134 if tagged[n].kind != LineKind::Insert && tagged[n].kind != LineKind::Delete {
2135 continue;
2136 }
2137 if !styles[n].moved {
2138 continue;
2139 }
2140 let prev = (n > 0
2141 && (tagged[n - 1].kind == LineKind::Insert || tagged[n - 1].kind == LineKind::Delete))
2142 .then_some(n - 1);
2143 let next = (n + 1 < tagged.len()
2144 && (tagged[n + 1].kind == LineKind::Insert || tagged[n + 1].kind == LineKind::Delete))
2145 .then_some(n + 1);
2146
2147 if prev.is_some_and(|i| moved_zebra_mask(styles[i]) == moved_zebra_mask(styles[n]))
2148 && next.is_some_and(|i| moved_zebra_mask(styles[i]) == moved_zebra_mask(styles[n]))
2149 {
2150 styles[n].uninteresting = true;
2151 continue;
2152 }
2153 if prev.is_some_and(|i| styles[i].moved && styles[i].alt != styles[n].alt) {
2154 continue;
2155 }
2156 if next.is_some_and(|i| styles[i].moved && styles[i].alt != styles[n].alt) {
2157 continue;
2158 }
2159 styles[n].uninteresting = true;
2160 }
2161}
2162
2163fn moved_zebra_mask(style: MovedStyle) -> (bool, bool) {
2164 (style.moved, style.alt)
2165}
2166
2167fn render_one_hunk(
2171 out: &mut Vec<u8>,
2172 tagged: &[TaggedLine<'_>],
2173 moved_styles: Option<&[MovedStyle]>,
2174 old_lines: &[DiffLine<'_>],
2175 start: usize,
2176 end: usize,
2177 options: &mut HunkRenderOptions<'_, '_>,
2178) {
2179 let slice = &tagged[start..end];
2180 let mut old_count = 0usize;
2181 let mut new_count = 0usize;
2182 for line in slice {
2183 match line.kind {
2184 LineKind::Context => {
2185 old_count += 1;
2186 new_count += 1;
2187 }
2188 LineKind::Delete => old_count += 1,
2189 LineKind::Insert => new_count += 1,
2190 }
2191 }
2192 let old_start = if old_count == 0 {
2194 slice.first().map(|line| line.old_index).unwrap_or(0)
2195 } else {
2196 slice
2197 .iter()
2198 .find(|line| line.kind != LineKind::Insert)
2199 .map(|line| line.old_index + 1)
2200 .unwrap_or(1)
2201 };
2202 let new_start = if new_count == 0 {
2203 slice.first().map(|line| line.new_index).unwrap_or(0)
2204 } else {
2205 slice
2206 .iter()
2207 .find(|line| line.kind != LineKind::Delete)
2208 .map(|line| line.new_index + 1)
2209 .unwrap_or(1)
2210 };
2211
2212 let heading = hunk_section_heading(
2213 old_lines,
2214 slice.first().map(|line| line.old_index),
2215 options.heading.as_deref_mut(),
2216 );
2217 let frag = format!(
2218 "@@ -{} +{} @@",
2219 format_hunk_range(old_start, old_count),
2220 format_hunk_range(new_start, new_count)
2221 );
2222 match options.colors {
2223 Some(colors) => {
2227 out.extend_from_slice(colors.frag.as_bytes());
2228 out.extend_from_slice(frag.as_bytes());
2229 out.extend_from_slice(colors.reset.as_bytes());
2230 if let Some(heading) = &heading {
2231 out.extend_from_slice(colors.context.as_bytes());
2232 out.push(b' ');
2233 out.extend_from_slice(colors.reset.as_bytes());
2234 out.extend_from_slice(colors.func.as_bytes());
2235 out.extend_from_slice(heading);
2236 out.extend_from_slice(colors.reset.as_bytes());
2237 }
2238 out.push(b'\n');
2239 }
2240 None => {
2241 out.extend_from_slice(frag.as_bytes());
2242 if let Some(heading) = &heading {
2243 out.push(b' ');
2244 out.extend_from_slice(heading);
2245 }
2246 out.push(b'\n');
2247 }
2248 }
2249
2250 if let Some(word_diff) = options.word_diff.as_deref_mut() {
2251 for line in slice {
2255 match line.kind {
2256 LineKind::Delete => word_diff.push_minus(line.content),
2257 LineKind::Insert => word_diff.push_plus(line.content),
2258 LineKind::Context => {
2259 word_diff.flush(out);
2260 word_diff.emit_context_line(out, line.content);
2261 }
2262 }
2263 }
2264 word_diff.flush(out);
2265 return;
2266 }
2267
2268 for (offset, line) in slice.iter().enumerate() {
2269 let prefix = match line.kind {
2270 LineKind::Context => options.line_indicators.context,
2271 LineKind::Delete => options.line_indicators.old,
2272 LineKind::Insert => options.line_indicators.new,
2273 };
2274 match options.colors {
2275 Some(colors) => {
2276 let ws_rule = options.ws_error.and_then(|ws| {
2279 let enabled = match line.kind {
2280 LineKind::Context => ws.context,
2281 LineKind::Delete => ws.old,
2282 LineKind::Insert => ws.new,
2283 };
2284 enabled.then_some(ws.rule)
2285 });
2286 let moved = moved_styles
2287 .and_then(|styles| styles.get(start + offset))
2288 .copied()
2289 .filter(|style| style.moved);
2290 write_patch_line_colored(out, prefix, line.content, colors, ws_rule, moved);
2291 }
2292 None => write_patch_line(out, prefix, line.content),
2293 }
2294 }
2295}
2296
2297fn format_hunk_range(start: usize, count: usize) -> String {
2300 if count == 1 {
2301 start.to_string()
2302 } else {
2303 format!("{start},{count}")
2304 }
2305}
2306
2307fn hunk_section_heading(
2313 old_lines: &[DiffLine<'_>],
2314 first_old_index: Option<usize>,
2315 mut heading: Option<&mut HeadingFn<'_>>,
2316) -> Option<Vec<u8>> {
2317 let first = first_old_index?;
2318 let classifier = heading.as_mut()?;
2319 for idx in (0..first).rev() {
2321 if let Some(found) = classifier(old_lines[idx].content) {
2322 return Some(found);
2323 }
2324 }
2325 None
2326}
2327
2328fn write_patch_line(out: &mut Vec<u8>, prefix: u8, line: &[u8]) {
2332 out.push(prefix);
2333 out.extend_from_slice(line);
2334 if !line.ends_with(b"\n") {
2335 out.extend_from_slice(b"\n\\ No newline at end of file\n");
2336 }
2337}
2338
2339fn write_patch_line_colored(
2352 out: &mut Vec<u8>,
2353 prefix: u8,
2354 line: &[u8],
2355 colors: RenderColors<'_>,
2356 ws_rule: Option<crate::ws::WsRule>,
2357 moved: Option<MovedStyle>,
2358) {
2359 let (body, terminated) = match line.split_last() {
2360 Some((b'\n', body)) => (body, true),
2361 _ => (line, false),
2362 };
2363 let color = match (prefix, moved) {
2364 (b'-', Some(style)) if style.uninteresting && style.alt => colors.old_moved_alt_dim,
2365 (b'-', Some(style)) if style.uninteresting => colors.old_moved_dim,
2366 (b'-', Some(style)) if style.alt => colors.old_moved_alt,
2367 (b'-', Some(_)) => colors.old_moved,
2368 (b'+', Some(style)) if style.uninteresting && style.alt => colors.new_moved_alt_dim,
2369 (b'+', Some(style)) if style.uninteresting => colors.new_moved_dim,
2370 (b'+', Some(style)) if style.alt => colors.new_moved_alt,
2371 (b'+', Some(_)) => colors.new_moved,
2372 (b'-', _) => colors.old,
2373 (b'+', _) => colors.new,
2374 _ => colors.context,
2375 };
2376
2377 if let Some(rule) = ws_rule {
2378 if rule == 0 {
2379 out.extend_from_slice(color.as_bytes());
2380 out.push(prefix);
2381 out.extend_from_slice(body);
2382 out.extend_from_slice(colors.reset.as_bytes());
2383 out.push(b'\n');
2384 if !terminated {
2385 out.extend_from_slice(colors.context.as_bytes());
2386 out.extend_from_slice(b"\\ No newline at end of file");
2387 out.extend_from_slice(colors.reset.as_bytes());
2388 out.push(b'\n');
2389 }
2390 return;
2391 }
2392 out.extend_from_slice(color.as_bytes());
2395 out.push(prefix);
2396 out.extend_from_slice(colors.reset.as_bytes());
2397 let emit_colors = crate::ws::WsEmitColors {
2398 set: color,
2399 reset: colors.reset,
2400 ws: colors.whitespace,
2401 };
2402 crate::ws::ws_check_emit(body, rule, out, &emit_colors);
2403 out.push(b'\n');
2404 if !terminated {
2405 let marker_color = if rule & crate::ws::WS_INCOMPLETE_LINE != 0 {
2406 colors.whitespace
2407 } else {
2408 colors.context
2409 };
2410 out.extend_from_slice(marker_color.as_bytes());
2411 out.extend_from_slice(b"\\ No newline at end of file");
2412 out.extend_from_slice(colors.reset.as_bytes());
2413 out.push(b'\n');
2414 }
2415 return;
2416 }
2417
2418 if prefix == b'+' {
2419 out.extend_from_slice(color.as_bytes());
2420 out.push(prefix);
2421 out.extend_from_slice(colors.reset.as_bytes());
2422 if !body.is_empty() {
2423 out.extend_from_slice(color.as_bytes());
2424 out.extend_from_slice(body);
2425 out.extend_from_slice(colors.reset.as_bytes());
2426 }
2427 } else {
2428 out.extend_from_slice(color.as_bytes());
2429 out.push(prefix);
2430 out.extend_from_slice(body);
2431 out.extend_from_slice(colors.reset.as_bytes());
2432 }
2433 out.push(b'\n');
2434 if !terminated {
2435 out.extend_from_slice(colors.context.as_bytes());
2436 out.extend_from_slice(b"\\ No newline at end of file");
2437 out.extend_from_slice(colors.reset.as_bytes());
2438 out.push(b'\n');
2439 }
2440}
2441
2442struct CdLine {
2472 bol: Vec<u8>,
2474 lost: Vec<CdLost>,
2478 plost: Vec<Vec<u8>>,
2481 flag: u64,
2483 p_lno: Vec<u64>,
2486}
2487
2488struct CdLost {
2490 line: Vec<u8>,
2491 parent_map: u64,
2492}
2493
2494pub struct CombinedRenderOptions {
2496 pub dense: bool,
2499 pub context: usize,
2501 pub algorithm: DiffAlgorithm,
2503 pub ws_ignore: WsIgnore,
2506}
2507
2508impl Default for CombinedRenderOptions {
2509 fn default() -> Self {
2510 Self {
2511 dense: true,
2512 context: DEFAULT_CONTEXT,
2513 algorithm: DiffAlgorithm::Myers,
2514 ws_ignore: WsIgnore::default(),
2515 }
2516 }
2517}
2518
2519pub fn render_combined(out: &mut Vec<u8>, result: &[u8], parents: &[&[u8]]) -> bool {
2531 render_combined_with(out, result, parents, &CombinedRenderOptions::default())
2532}
2533
2534pub fn render_combined_with(
2536 out: &mut Vec<u8>,
2537 result: &[u8],
2538 parents: &[&[u8]],
2539 options: &CombinedRenderOptions,
2540) -> bool {
2541 let num_parent = parents.len();
2542 debug_assert!(num_parent >= 1);
2543
2544 let result_lines = split_lines(result);
2548 let cnt = result_lines.len();
2549
2550 let mut sline: Vec<CdLine> = Vec::with_capacity(cnt + 2);
2555 for line in &result_lines {
2556 sline.push(CdLine {
2557 bol: line.bytes_without_newline().to_vec(),
2558 lost: Vec::new(),
2559 plost: Vec::new(),
2560 flag: 0,
2561 p_lno: vec![0; num_parent],
2562 });
2563 }
2564 for _ in 0..2 {
2565 sline.push(CdLine {
2566 bol: Vec::new(),
2567 lost: Vec::new(),
2568 plost: Vec::new(),
2569 flag: 0,
2570 p_lno: vec![0; num_parent],
2571 });
2572 }
2573
2574 for n in 0..num_parent {
2578 let mut reused = None;
2579 for j in 0..n {
2580 if parents[j] == parents[n] {
2581 reused = Some(j);
2582 break;
2583 }
2584 }
2585 match reused {
2586 Some(j) => reuse_combine_diff(&mut sline, cnt, n, j),
2587 None => combine_one_parent(&mut sline, &result_lines, parents[n], n, options),
2588 }
2589 }
2590
2591 let show_hunks = make_hunks(&mut sline, cnt, num_parent, options.dense, options.context);
2592 if show_hunks {
2593 dump_sline(out, &sline, cnt, num_parent, options.context);
2594 }
2595 show_hunks
2596}
2597
2598fn combine_one_parent(
2601 sline: &mut [CdLine],
2602 result_lines: &[DiffLine<'_>],
2603 parent: &[u8],
2604 n: usize,
2605 options: &CombinedRenderOptions,
2606) {
2607 let cnt = result_lines.len();
2608 let nmask = 1u64 << n;
2609 let parent_lines = split_lines(parent);
2610 let ops = myers_diff_lines_ws(
2611 &parent_lines,
2612 result_lines,
2613 options.ws_ignore,
2614 options.algorithm,
2615 );
2616
2617 let mut old_idx: usize = 0; let mut new_idx: usize = 0; let mut i = 0;
2628 while i < ops.len() {
2629 match ops[i] {
2630 DiffOp::Equal(k) => {
2631 old_idx += k;
2632 new_idx += k;
2633 i += 1;
2634 }
2635 _ => {
2636 let hunk_old_start = old_idx; let hunk_new_start = new_idx; let mut dels: Vec<&[u8]> = Vec::new();
2641 while i < ops.len() {
2642 match ops[i] {
2643 DiffOp::Delete(k) => {
2644 for _ in 0..k {
2645 dels.push(parent_lines[old_idx].bytes_without_newline());
2646 old_idx += 1;
2647 }
2648 i += 1;
2649 }
2650 DiffOp::Insert(k) => {
2651 new_idx += k;
2652 i += 1;
2653 }
2654 DiffOp::Equal(_) => break,
2655 }
2656 }
2657 let _ = hunk_old_start;
2658
2659 for d in &dels {
2670 sline[hunk_new_start].plost.push(d.to_vec());
2671 }
2672 for line in sline.iter_mut().take(new_idx.min(cnt)).skip(hunk_new_start) {
2675 line.flag |= nmask;
2676 }
2677 }
2678 }
2679 }
2680
2681 let mut p_lno: u64 = 1;
2684 for (lno, line) in sline.iter_mut().enumerate().take(cnt + 1) {
2685 line.p_lno[n] = p_lno;
2686 if !line.plost.is_empty() {
2687 let plost = std::mem::take(&mut line.plost);
2688 coalesce_lost(&mut line.lost, plost, n, options);
2689 }
2690 for ll in &line.lost {
2692 if ll.parent_map & nmask != 0 {
2693 p_lno += 1; }
2695 }
2696 if lno < cnt && (line.flag & nmask) == 0 {
2697 p_lno += 1; }
2699 }
2700 sline[cnt + 1].p_lno[n] = p_lno; }
2702
2703fn coalesce_lost(
2708 base: &mut Vec<CdLost>,
2709 newlines: Vec<Vec<u8>>,
2710 n: usize,
2711 options: &CombinedRenderOptions,
2712) {
2713 let pmask = 1u64 << n;
2714 if newlines.is_empty() {
2715 return;
2716 }
2717 if base.is_empty() {
2718 for line in newlines {
2719 base.push(CdLost {
2720 line,
2721 parent_map: pmask,
2722 });
2723 }
2724 return;
2725 }
2726
2727 let m = base.len();
2731 let k = newlines.len();
2732 let mut lcs = vec![vec![0i32; k + 1]; m + 1];
2733 for i in 1..=m {
2734 for j in 1..=k {
2735 if combined_lines_match(&base[i - 1].line, &newlines[j - 1], options.ws_ignore) {
2736 lcs[i][j] = lcs[i - 1][j - 1] + 1;
2737 } else if lcs[i][j - 1] >= lcs[i - 1][j] {
2738 lcs[i][j] = lcs[i][j - 1];
2739 } else {
2740 lcs[i][j] = lcs[i - 1][j];
2741 }
2742 }
2743 }
2744
2745 let mut merged: Vec<CdLost> = Vec::with_capacity(m + k);
2747 let mut i = m;
2748 let mut j = k;
2749 while i > 0 || j > 0 {
2750 if i > 0
2751 && j > 0
2752 && combined_lines_match(&base[i - 1].line, &newlines[j - 1], options.ws_ignore)
2753 {
2754 let mut entry = std::mem::replace(
2755 &mut base[i - 1],
2756 CdLost {
2757 line: Vec::new(),
2758 parent_map: 0,
2759 },
2760 );
2761 entry.parent_map |= pmask;
2762 merged.push(entry);
2763 i -= 1;
2764 j -= 1;
2765 } else if j > 0 && (i == 0 || lcs[i][j - 1] >= lcs[i - 1][j]) {
2766 merged.push(CdLost {
2767 line: newlines[j - 1].clone(),
2768 parent_map: pmask,
2769 });
2770 j -= 1;
2771 } else {
2772 let entry = std::mem::replace(
2773 &mut base[i - 1],
2774 CdLost {
2775 line: Vec::new(),
2776 parent_map: 0,
2777 },
2778 );
2779 merged.push(entry);
2780 i -= 1;
2781 }
2782 }
2783 merged.reverse();
2784 *base = merged;
2785}
2786
2787fn combined_lines_match(a: &[u8], b: &[u8], ws: WsIgnore) -> bool {
2791 if ws.all_space || ws.space_change || ws.space_at_eol {
2792 let at = strip_trailing_ws(a);
2793 let bt = strip_trailing_ws(b);
2794 if !ws.all_space && !ws.space_change {
2795 return at == bt;
2796 }
2797 return ws_squash_eq(at, bt, ws.space_change);
2798 }
2799 a == b
2800}
2801
2802fn strip_trailing_ws(s: &[u8]) -> &[u8] {
2803 let mut end = s.len();
2804 while end > 0 && (s[end - 1] == b' ' || s[end - 1] == b'\t') {
2805 end -= 1;
2806 }
2807 &s[..end]
2808}
2809
2810fn ws_squash_eq(a: &[u8], b: &[u8], change_only: bool) -> bool {
2813 let is_ws = |c: u8| c == b' ' || c == b'\t';
2814 let (mut ia, mut ib) = (0usize, 0usize);
2815 while ia < a.len() && ib < b.len() {
2816 let (ca, cb) = (a[ia], b[ib]);
2817 if is_ws(ca) || is_ws(cb) {
2818 if change_only && (!is_ws(ca) || !is_ws(cb)) {
2819 return false;
2820 }
2821 if change_only {
2824 while ia < a.len() && is_ws(a[ia]) {
2825 ia += 1;
2826 }
2827 while ib < b.len() && is_ws(b[ib]) {
2828 ib += 1;
2829 }
2830 continue;
2831 } else {
2832 if is_ws(ca) {
2833 ia += 1;
2834 continue;
2835 }
2836 if is_ws(cb) {
2837 ib += 1;
2838 continue;
2839 }
2840 }
2841 }
2842 if ca != cb {
2843 return false;
2844 }
2845 ia += 1;
2846 ib += 1;
2847 }
2848 while ia < a.len() && is_ws(a[ia]) {
2850 ia += 1;
2851 }
2852 while ib < b.len() && is_ws(b[ib]) {
2853 ib += 1;
2854 }
2855 ia == a.len() && ib == b.len()
2856}
2857
2858fn reuse_combine_diff(sline: &mut [CdLine], cnt: usize, i: usize, j: usize) {
2862 let imask = 1u64 << i;
2863 let jmask = 1u64 << j;
2864 for line in sline.iter_mut().take(cnt + 1) {
2865 line.p_lno[i] = line.p_lno[j];
2866 for ll in &mut line.lost {
2867 if ll.parent_map & jmask != 0 {
2868 ll.parent_map |= imask;
2869 }
2870 }
2871 if line.flag & jmask != 0 {
2872 line.flag |= imask;
2873 }
2874 }
2875 sline[cnt + 1].p_lno[i] = sline[cnt + 1].p_lno[j];
2877}
2878
2879fn cd_interesting(sline: &CdLine, all_mask: u64) -> bool {
2882 (sline.flag & all_mask) != 0 || !sline.lost.is_empty()
2883}
2884
2885fn adjust_hunk_tail(sline: &[CdLine], all_mask: u64, hunk_begin: usize, mut i: usize) -> usize {
2887 if hunk_begin < i && (sline[i - 1].flag & all_mask) == 0 {
2888 i -= 1;
2889 }
2890 i
2891}
2892
2893fn find_next(
2895 sline: &[CdLine],
2896 mark: u64,
2897 mut i: usize,
2898 cnt: usize,
2899 look_for_uninteresting: bool,
2900) -> usize {
2901 while i <= cnt {
2902 let marked = (sline[i].flag & mark) != 0;
2903 if look_for_uninteresting {
2904 if !marked {
2905 return i;
2906 }
2907 } else if marked {
2908 return i;
2909 }
2910 i += 1;
2911 }
2912 i
2913}
2914
2915fn give_context(sline: &mut [CdLine], cnt: usize, num_parent: usize, context: usize) -> bool {
2918 let all_mask = (1u64 << num_parent) - 1;
2919 let mark = 1u64 << num_parent;
2920 let no_pre_delete = 2u64 << num_parent;
2921
2922 let mut i = find_next(sline, mark, 0, cnt, false);
2923 if cnt < i {
2924 return false;
2925 }
2926
2927 while i <= cnt {
2928 let mut j = i.saturating_sub(context);
2929 while j < i {
2931 if (sline[j].flag & mark) == 0 {
2932 sline[j].flag |= no_pre_delete;
2933 }
2934 sline[j].flag |= mark;
2935 j += 1;
2936 }
2937
2938 loop {
2939 j = find_next(sline, mark, i, cnt, true);
2941 if cnt < j {
2942 return true;
2944 }
2945 let k = find_next(sline, mark, j, cnt, false);
2947 let j2 = adjust_hunk_tail(sline, all_mask, i, j);
2948
2949 if k < j2 + context {
2950 let mut jj = j2;
2952 while jj < k {
2953 sline[jj].flag |= mark;
2954 jj += 1;
2955 }
2956 i = k;
2957 continue;
2958 }
2959
2960 i = k;
2962 let kk = if j2 + context < cnt + 1 {
2963 j2 + context
2964 } else {
2965 cnt + 1
2966 };
2967 let mut jj = j2;
2968 while jj < kk {
2969 sline[jj].flag |= mark;
2970 jj += 1;
2971 }
2972 break;
2973 }
2974 }
2975 true
2976}
2977
2978fn make_hunks(
2981 sline: &mut [CdLine],
2982 cnt: usize,
2983 num_parent: usize,
2984 dense: bool,
2985 context: usize,
2986) -> bool {
2987 let all_mask = (1u64 << num_parent) - 1;
2988 let mark = 1u64 << num_parent;
2989
2990 for line in sline.iter_mut().take(cnt + 1) {
2991 if cd_interesting(line, all_mask) {
2992 line.flag |= mark;
2993 } else {
2994 line.flag &= !mark;
2995 }
2996 }
2997 if !dense {
2998 return give_context(sline, cnt, num_parent, context);
2999 }
3000
3001 let mut i = 0;
3005 while i <= cnt {
3006 while i <= cnt && (sline[i].flag & mark) == 0 {
3007 i += 1;
3008 }
3009 if cnt < i {
3010 break;
3011 }
3012 let hunk_begin = i;
3013 let mut j = i + 1;
3014 while j <= cnt {
3015 if (sline[j].flag & mark) == 0 {
3016 let mut la = adjust_hunk_tail(sline, all_mask, hunk_begin, j);
3018 la = if la + context < cnt + 1 {
3019 la + context
3020 } else {
3021 cnt + 1
3022 };
3023 let mut contin = false;
3024 while la > 0 && j < la {
3025 la -= 1;
3026 if (sline[la].flag & mark) != 0 {
3027 contin = true;
3028 break;
3029 }
3030 }
3031 if !contin {
3032 break;
3033 }
3034 j = la;
3035 }
3036 j += 1;
3037 }
3038 let hunk_end = j;
3039
3040 let mut same_diff: u64 = 0;
3043 let mut has_interesting = false;
3044 let mut jj = i;
3045 while jj < hunk_end && !has_interesting {
3046 let this_diff = sline[jj].flag & all_mask;
3047 if this_diff != 0 {
3048 if same_diff == 0 {
3049 same_diff = this_diff;
3050 } else if same_diff != this_diff {
3051 has_interesting = true;
3052 break;
3053 }
3054 }
3055 for ll in &sline[jj].lost {
3056 if has_interesting {
3057 break;
3058 }
3059 let td = ll.parent_map;
3060 if same_diff == 0 {
3061 same_diff = td;
3062 } else if same_diff != td {
3063 has_interesting = true;
3064 }
3065 }
3066 jj += 1;
3067 }
3068
3069 if !has_interesting && same_diff != all_mask {
3070 for line in sline.iter_mut().take(hunk_end).skip(hunk_begin) {
3072 line.flag &= !mark;
3073 }
3074 }
3075 i = hunk_end;
3076 }
3077
3078 give_context(sline, cnt, num_parent, context)
3079}
3080
3081fn show_parent_lno(
3083 out: &mut Vec<u8>,
3084 sline: &[CdLine],
3085 l0: usize,
3086 l1: usize,
3087 n: usize,
3088 null_context: u64,
3089) {
3090 let a = sline[l0].p_lno[n];
3091 let b = sline[l1].p_lno[n];
3092 out.extend_from_slice(format!(" -{},{}", a, b - a - null_context).as_bytes());
3093}
3094
3095fn hunk_comment_line(bol: &[u8]) -> bool {
3098 if bol.is_empty() {
3099 return false;
3100 }
3101 let ch = bol[0];
3102 ch.is_ascii_alphabetic() || ch == b'_' || ch == b'$'
3103}
3104
3105fn show_line_to_eol(out: &mut Vec<u8>, line: &[u8]) {
3108 let saw_cr = line.last() == Some(&b'\r');
3109 if saw_cr {
3110 out.extend_from_slice(&line[..line.len() - 1]);
3111 out.push(b'\r');
3112 } else {
3113 out.extend_from_slice(line);
3114 }
3115 out.push(b'\n');
3116}
3117
3118fn dump_sline(out: &mut Vec<u8>, sline: &[CdLine], cnt: usize, num_parent: usize, context: usize) {
3120 let mark = 1u64 << num_parent;
3121 let no_pre_delete = 2u64 << num_parent;
3122 let mut lno: usize = 0;
3123
3124 loop {
3125 let mut hunk_comment: Option<&[u8]> = None;
3126 while lno <= cnt && (sline[lno].flag & mark) == 0 {
3127 if hunk_comment_line(&sline[lno].bol) {
3128 hunk_comment = Some(&sline[lno].bol);
3129 }
3130 lno += 1;
3131 }
3132 if cnt < lno {
3133 break;
3134 }
3135 let mut hunk_end = lno + 1;
3136 while hunk_end <= cnt {
3137 if (sline[hunk_end].flag & mark) == 0 {
3138 break;
3139 }
3140 hunk_end += 1;
3141 }
3142
3143 let mut rlines = (hunk_end - lno) as u64;
3144 if cnt < hunk_end {
3145 rlines -= 1; }
3147
3148 let mut null_context: u64 = 0;
3149 if context == 0 {
3150 for sl in sline.iter().take(hunk_end).skip(lno) {
3153 if (sl.flag & (mark - 1)) == 0 {
3154 null_context += 1;
3155 }
3156 }
3157 rlines -= null_context;
3158 }
3159
3160 for _ in 0..=num_parent {
3163 out.push(b'@');
3164 }
3165 for i in 0..num_parent {
3166 show_parent_lno(out, sline, lno, hunk_end, i, null_context);
3167 }
3168 out.extend_from_slice(format!(" +{},{} ", lno + 1, rlines).as_bytes());
3169 for _ in 0..=num_parent {
3170 out.push(b'@');
3171 }
3172
3173 if let Some(comment) = hunk_comment {
3174 let mut comment_end = 0;
3175 for (idx, &ch) in comment.iter().take(40).enumerate() {
3176 if ch == b'\n' {
3177 break;
3178 }
3179 if !ch.is_ascii_whitespace() {
3180 comment_end = idx + 1;
3181 }
3182 }
3183 if comment_end != 0 {
3184 out.push(b' ');
3185 out.extend_from_slice(&comment[..comment_end]);
3186 }
3187 }
3188 out.push(b'\n');
3189
3190 while lno < hunk_end {
3192 let sl = &sline[lno];
3193 lno += 1;
3194 if (sl.flag & no_pre_delete) == 0 {
3196 for ll in &sl.lost {
3197 for j in 0..num_parent {
3198 if ll.parent_map & (1u64 << j) != 0 {
3199 out.push(b'-');
3200 } else {
3201 out.push(b' ');
3202 }
3203 }
3204 show_line_to_eol(out, &ll.line);
3205 }
3206 }
3207 if cnt < lno {
3208 break;
3209 }
3210 if (sl.flag & (mark - 1)) == 0 {
3211 if context == 0 {
3213 continue;
3214 }
3215 }
3216 let mut p_mask = 1u64;
3217 for _ in 0..num_parent {
3218 if p_mask & sl.flag != 0 {
3219 out.push(b'+');
3220 } else {
3221 out.push(b' ');
3222 }
3223 p_mask <<= 1;
3224 }
3225 show_line_to_eol(out, &sl.bol);
3226 }
3227 }
3228}
3229
3230#[cfg(test)]
3231mod tests {
3232 use super::*;
3233
3234 fn render_plain(old: Option<&[u8]>, new: Option<&[u8]>) -> Vec<u8> {
3235 let mut out = Vec::new();
3236 let mut options = HunkRenderOptions::default();
3237 render_hunks(&mut out, old, new, &mut options);
3238 out
3239 }
3240
3241 #[test]
3242 fn identical_content_renders_nothing() {
3243 assert!(render_plain(Some(b"a\nb\n"), Some(b"a\nb\n")).is_empty());
3244 }
3245
3246 #[test]
3247 fn single_line_change_basic_hunk() {
3248 let out = render_plain(Some(b"alpha\nbeta\ngamma\n"), Some(b"alpha\nBETA\ngamma\n"));
3249 assert_eq!(
3250 out,
3251 b"@@ -1,3 +1,3 @@\n alpha\n-beta\n+BETA\n gamma\n".to_vec(),
3252 );
3253 }
3254
3255 #[test]
3256 fn count_omitted_when_one() {
3257 let out = render_plain(Some(b"old\n"), Some(b"new\n"));
3259 assert_eq!(out, b"@@ -1 +1 @@\n-old\n+new\n".to_vec());
3260 }
3261
3262 #[test]
3263 fn no_newline_marker_on_old_side() {
3264 let out = render_plain(Some(b"only line no newline"), None);
3265 assert_eq!(
3266 out,
3267 b"@@ -1 +0,0 @@\n-only line no newline\n\\ No newline at end of file\n".to_vec(),
3268 );
3269 }
3270
3271 #[test]
3272 fn no_newline_marker_on_new_side() {
3273 let out = render_plain(Some(b"beta\n"), Some(b"beta-notail"));
3274 assert_eq!(
3275 out,
3276 b"@@ -1 +1 @@\n-beta\n+beta-notail\n\\ No newline at end of file\n".to_vec(),
3277 );
3278 }
3279
3280 #[test]
3281 fn pure_insertion_into_empty() {
3282 let out = render_plain(None, Some(b"x\ny\n"));
3283 assert_eq!(out, b"@@ -0,0 +1,2 @@\n+x\n+y\n".to_vec());
3284 }
3285
3286 #[test]
3287 fn distant_changes_split_into_two_hunks() {
3288 let old: &[u8] = b"a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n";
3289 let new: &[u8] = b"A\nb\nc\nd\ne\nf\ng\nh\ni\nJ\n";
3290 let out = render_plain(Some(old), Some(new));
3291 let text = String::from_utf8(out).expect("rendered output is valid UTF-8");
3293 assert_eq!(text.matches("@@ ").count(), 2, "expected two hunks: {text}");
3294 }
3295
3296 #[test]
3297 fn heading_callback_supplies_section() {
3298 let old: &[u8] = b"fn foo() {\n a\n b\n c\n d\n e\n f\n g\n}\n";
3302 let new: &[u8] = b"fn foo() {\n a\n b\n c\n d\n CHANGED\n f\n g\n}\n";
3303 let mut out = Vec::new();
3304 let mut heading_fn = |line: &[u8]| -> Option<Vec<u8>> {
3307 if line.first().is_some_and(u8::is_ascii_alphabetic) {
3308 Some(line.strip_suffix(b"\n").unwrap_or(line).to_vec())
3309 } else {
3310 None
3311 }
3312 };
3313 let mut options = HunkRenderOptions {
3314 heading: Some(&mut heading_fn),
3315 ..Default::default()
3316 };
3317 render_hunks(&mut out, Some(old), Some(new), &mut options);
3318 let text = String::from_utf8(out).expect("rendered output is valid UTF-8");
3319 assert!(
3320 text.starts_with("@@ -3,7 +3,7 @@ fn foo() {\n"),
3321 "expected funcname heading: {text}",
3322 );
3323 }
3324
3325 fn render_cc(result: &[u8], parents: &[&[u8]], dense: bool) -> String {
3326 let mut out = Vec::new();
3327 let opts = CombinedRenderOptions {
3328 dense,
3329 ..Default::default()
3330 };
3331 render_combined_with(&mut out, result, parents, &opts);
3332 String::from_utf8(out).expect("combined output is valid UTF-8")
3333 }
3334
3335 #[test]
3336 fn combined_two_parent_dense_header_and_columns() {
3337 let p0 = b"A\nB\nC\nD\nE\nF\n";
3342 let p1 = b"A\nB\n1\n2\n";
3343 let result = b"A\nB\nC\nD\nE\nF\n1\n2\n";
3344 let text = render_cc(result, &[p0, p1], true);
3345 assert_eq!(
3346 text, "@@@ -1,6 -1,4 +1,8 @@@\n A\n B\n +C\n +D\n +E\n +F\n+ 1\n+ 2\n",
3347 "combined dense output:\n{text}",
3348 );
3349 }
3350
3351 #[test]
3352 fn combined_identical_to_one_parent_dense_drops_hunk() {
3353 let p0 = b"x\ny\n";
3358 let p1 = b"x\nCHANGED\n";
3359 let result = b"x\ny\n"; assert_eq!(render_cc(result, &[p0, p1], true), "");
3361 assert!(render_cc(result, &[p0, p1], false).starts_with("@@@"));
3363 }
3364
3365 #[test]
3366 fn combined_reuse_identical_parents() {
3367 let parent = b"a\nb\n";
3371 let result = b"a\nb\nc\n";
3372 let text = render_cc(result, &[parent, parent], true);
3373 assert_eq!(
3374 text, "@@@ -1,2 -1,2 +1,3 @@@\n a\n b\n++c\n",
3375 "reuse output:\n{text}",
3376 );
3377 }
3378}