1use crate::{
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 ws_error: Option<WsErrorHighlight>,
203 pub ws_ignore: WsIgnore,
208 pub algorithm: DiffAlgorithm,
210 pub indent_heuristic: bool,
218 pub change_ignore: Option<&'a ChangeIgnore<'a>>,
223 pub line_ranges: Option<&'a [LineRange]>,
231 pub color_moved: Option<ColorMoved>,
234 pub anchors: &'a [Vec<u8>],
238}
239
240#[derive(Clone, Copy, Debug, PartialEq, Eq)]
243pub struct LineRange {
244 pub start: i64,
246 pub end: i64,
248}
249
250pub type ChangeIgnoreRegex<'a> = &'a dyn Fn(&[u8]) -> bool;
256
257pub struct ChangeIgnore<'a> {
258 pub ignore_blank_lines: bool,
260 pub regex_match: Option<ChangeIgnoreRegex<'a>>,
264}
265
266#[derive(Clone, Copy)]
270pub struct WsErrorHighlight {
271 pub rule: crate::ws::WsRule,
273 pub old: bool,
275 pub new: bool,
277 pub context: bool,
279}
280
281impl Default for HunkRenderOptions<'_, '_> {
282 fn default() -> Self {
283 Self {
284 context: DEFAULT_CONTEXT,
285 interhunk: 0,
286 heading: None,
287 colors: None,
288 word_diff: None,
289 ws_error: None,
290 ws_ignore: WsIgnore::default(),
291 algorithm: DiffAlgorithm::Myers,
292 indent_heuristic: true,
293 change_ignore: None,
294 line_ranges: None,
295 color_moved: None,
296 anchors: &[],
297 }
298 }
299}
300
301pub fn render_hunks(
314 out: &mut Vec<u8>,
315 old_content: Option<&[u8]>,
316 new_content: Option<&[u8]>,
317 options: &mut HunkRenderOptions<'_, '_>,
318) {
319 let (context, function_context) = decode_context(options.context);
320 if let Some(ranges) = options.line_ranges {
325 let max_span = ranges
326 .iter()
327 .map(|r| r.end - r.start)
328 .max()
329 .unwrap_or(0)
330 .max(0) as usize;
331 let saved_context = options.context;
332 options.context = replace_context_value(saved_context, context.max(max_span));
333 options.line_ranges = None;
334 let mut full = Vec::new();
335 render_hunks(&mut full, old_content, new_content, options);
336 options.context = saved_context;
337 options.line_ranges = Some(ranges);
338 filter_hunks_to_ranges(out, &full, ranges);
339 return;
340 }
341 let old = split_lines(old_content.unwrap_or_default());
342 let new = split_lines(new_content.unwrap_or_default());
343 let mut ops = if options.algorithm == DiffAlgorithm::Patience
347 && !options.anchors.is_empty()
348 && options.ws_ignore.is_empty()
349 {
350 patience_diff_lines_anchored(&old, &new, options.anchors)
351 } else {
352 myers_diff_lines_ws(&old, &new, options.ws_ignore, options.algorithm)
353 };
354
355 change_compact(
360 &mut ops,
361 &old,
362 &new,
363 options.ws_ignore,
364 options.indent_heuristic,
365 );
366
367 let mut tagged: Vec<TaggedLine<'_>> = Vec::new();
370 let mut old_idx = 0usize;
371 let mut new_idx = 0usize;
372 for op in ops {
373 match op {
374 DiffOp::Equal(n) => {
375 for _ in 0..n {
376 tagged.push(TaggedLine {
379 kind: LineKind::Context,
380 content: new[new_idx].content,
381 old_index: old_idx,
382 new_index: new_idx,
383 });
384 old_idx += 1;
385 new_idx += 1;
386 }
387 }
388 DiffOp::Delete(n) => {
389 for _ in 0..n {
390 tagged.push(TaggedLine {
391 kind: LineKind::Delete,
392 content: old[old_idx].content,
393 old_index: old_idx,
394 new_index: new_idx,
395 });
396 old_idx += 1;
397 }
398 }
399 DiffOp::Insert(n) => {
400 for _ in 0..n {
401 tagged.push(TaggedLine {
402 kind: LineKind::Insert,
403 content: new[new_idx].content,
404 old_index: old_idx,
405 new_index: new_idx,
406 });
407 new_idx += 1;
408 }
409 }
410 }
411 }
412
413 let changes = build_changes(&tagged);
417 if changes.is_empty() {
418 return;
419 }
420
421 let mut changes = changes;
425 if let Some(ci) = options.change_ignore {
426 mark_ignorable_changes(&mut changes, &old, &new, options.ws_ignore, ci);
427 }
428
429 let mut groups = group_changes_into_hunks(&changes, context, options.interhunk);
434 if function_context {
435 groups = expand_hunks_to_function_context(
436 &groups,
437 &tagged,
438 &old,
439 &new,
440 options.heading.as_deref_mut(),
441 );
442 }
443
444 let moved_styles = options
445 .color_moved
446 .filter(|_| options.colors.is_some() && options.word_diff.is_none())
447 .map(|color_moved| mark_color_as_moved(&tagged, color_moved));
448
449 for (first_change, last_change) in groups {
450 let (hunk_start, hunk_end) = if function_context {
451 (first_change, (last_change + 1).min(tagged.len()))
452 } else {
453 (
454 first_change.saturating_sub(context),
455 (last_change + context + 1).min(tagged.len()),
456 )
457 };
458 render_one_hunk(
459 out,
460 &tagged,
461 moved_styles.as_deref(),
462 &old,
463 hunk_start,
464 hunk_end,
465 options,
466 );
467 }
468}
469
470const MAX_INDENT: i32 = 200;
492const MAX_BLANKS: i32 = 20;
494
495const START_OF_FILE_PENALTY: i32 = 1;
497const END_OF_FILE_PENALTY: i32 = 21;
498const TOTAL_BLANK_WEIGHT: i32 = -30;
499const POST_BLANK_WEIGHT: i32 = 6;
500const RELATIVE_INDENT_PENALTY: i32 = -4;
501const RELATIVE_INDENT_WITH_BLANK_PENALTY: i32 = 10;
502const RELATIVE_OUTDENT_PENALTY: i32 = 24;
503const RELATIVE_OUTDENT_WITH_BLANK_PENALTY: i32 = 17;
504const RELATIVE_DEDENT_PENALTY: i32 = 23;
505const RELATIVE_DEDENT_WITH_BLANK_PENALTY: i32 = 17;
506const INDENT_WEIGHT: i32 = 60;
507const INDENT_HEURISTIC_MAX_SLIDING: i64 = 100;
508
509struct CompactFile {
514 recs: Vec<Vec<u8>>,
515 changed: Vec<bool>,
516}
517
518impl CompactFile {
519 fn nrec(&self) -> i64 {
520 self.recs.len() as i64
521 }
522
523 fn changed(&self, i: i64) -> bool {
526 if i < 0 || i >= self.nrec() {
527 false
528 } else {
529 self.changed[i as usize]
530 }
531 }
532
533 fn set_changed(&mut self, i: i64, v: bool) {
534 self.changed[i as usize] = v;
535 }
536}
537
538fn get_indent(rec: &[u8]) -> i32 {
542 let mut ret: i32 = 0;
543 for &c in rec {
544 if !xdl_isspace(c) {
545 return ret;
546 } else if c == b' ' {
547 ret += 1;
548 } else if c == b'\t' {
549 ret += 8 - ret % 8;
550 }
551 if ret >= MAX_INDENT {
553 return MAX_INDENT;
554 }
555 }
556 -1
558}
559
560fn xdl_isspace(c: u8) -> bool {
563 matches!(c, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')
564}
565
566#[derive(Default)]
568struct SplitMeasurement {
569 end_of_file: bool,
570 indent: i32,
571 pre_blank: i32,
572 pre_indent: i32,
573 post_blank: i32,
574 post_indent: i32,
575}
576
577#[derive(Default, Clone, Copy)]
579struct SplitScore {
580 effective_indent: i32,
581 penalty: i32,
582}
583
584fn measure_split(xdf: &CompactFile, split: i64) -> SplitMeasurement {
587 let mut m = SplitMeasurement::default();
588 if split >= xdf.nrec() {
589 m.end_of_file = true;
590 m.indent = -1;
591 } else {
592 m.end_of_file = false;
593 m.indent = get_indent(&xdf.recs[split as usize]);
594 }
595
596 m.pre_blank = 0;
597 m.pre_indent = -1;
598 let mut i = split - 1;
599 while i >= 0 {
600 m.pre_indent = get_indent(&xdf.recs[i as usize]);
601 if m.pre_indent != -1 {
602 break;
603 }
604 m.pre_blank += 1;
605 if m.pre_blank == MAX_BLANKS {
606 m.pre_indent = 0;
607 break;
608 }
609 i -= 1;
610 }
611
612 m.post_blank = 0;
613 m.post_indent = -1;
614 let mut i = split + 1;
615 while i < xdf.nrec() {
616 m.post_indent = get_indent(&xdf.recs[i as usize]);
617 if m.post_indent != -1 {
618 break;
619 }
620 m.post_blank += 1;
621 if m.post_blank == MAX_BLANKS {
622 m.post_indent = 0;
623 break;
624 }
625 i += 1;
626 }
627
628 m
629}
630
631fn score_add_split(m: &SplitMeasurement, s: &mut SplitScore) {
633 if m.pre_indent == -1 && m.pre_blank == 0 {
634 s.penalty += START_OF_FILE_PENALTY;
635 }
636 if m.end_of_file {
637 s.penalty += END_OF_FILE_PENALTY;
638 }
639
640 let post_blank = if m.indent == -1 { 1 + m.post_blank } else { 0 };
641 let total_blank = m.pre_blank + post_blank;
642
643 s.penalty += TOTAL_BLANK_WEIGHT * total_blank;
644 s.penalty += POST_BLANK_WEIGHT * post_blank;
645
646 let indent = if m.indent != -1 {
647 m.indent
648 } else {
649 m.post_indent
650 };
651 let any_blanks = total_blank != 0;
652
653 s.effective_indent += indent;
654
655 if indent == -1 || m.pre_indent == -1 {
656 } else if indent > m.pre_indent {
659 s.penalty += if any_blanks {
660 RELATIVE_INDENT_WITH_BLANK_PENALTY
661 } else {
662 RELATIVE_INDENT_PENALTY
663 };
664 } else if indent == m.pre_indent {
665 } else if m.post_indent != -1 && m.post_indent > indent {
667 s.penalty += if any_blanks {
668 RELATIVE_OUTDENT_WITH_BLANK_PENALTY
669 } else {
670 RELATIVE_OUTDENT_PENALTY
671 };
672 } else {
673 s.penalty += if any_blanks {
674 RELATIVE_DEDENT_WITH_BLANK_PENALTY
675 } else {
676 RELATIVE_DEDENT_PENALTY
677 };
678 }
679}
680
681fn score_cmp(s1: &SplitScore, s2: &SplitScore) -> i32 {
683 let cmp_indents = (s1.effective_indent > s2.effective_indent) as i32
684 - (s1.effective_indent < s2.effective_indent) as i32;
685 INDENT_WEIGHT * cmp_indents + (s1.penalty - s2.penalty)
686}
687
688struct XdlGroup {
691 start: i64,
692 end: i64,
693}
694
695fn recs_match(xdf: &CompactFile, a: i64, b: i64) -> bool {
698 xdf.recs[a as usize] == xdf.recs[b as usize]
699}
700
701fn group_init(xdf: &CompactFile) -> XdlGroup {
703 let mut end = 0i64;
704 while xdf.changed(end) {
705 end += 1;
706 }
707 XdlGroup { start: 0, end }
708}
709
710fn group_next(xdf: &CompactFile, g: &mut XdlGroup) -> bool {
712 if g.end == xdf.nrec() {
713 return false;
714 }
715 g.start = g.end + 1;
716 g.end = g.start;
717 while xdf.changed(g.end) {
718 g.end += 1;
719 }
720 true
721}
722
723fn group_previous(xdf: &CompactFile, g: &mut XdlGroup) -> bool {
725 if g.start == 0 {
726 return false;
727 }
728 g.end = g.start - 1;
729 g.start = g.end;
730 while xdf.changed(g.start - 1) {
731 g.start -= 1;
732 }
733 true
734}
735
736fn group_slide_down(xdf: &mut CompactFile, g: &mut XdlGroup) -> bool {
740 if g.end < xdf.nrec() && recs_match(xdf, g.start, g.end) {
741 xdf.set_changed(g.start, false);
742 xdf.set_changed(g.end, true);
743 g.start += 1;
744 g.end += 1;
745 while xdf.changed(g.end) {
746 g.end += 1;
747 }
748 true
749 } else {
750 false
751 }
752}
753
754fn group_slide_up(xdf: &mut CompactFile, g: &mut XdlGroup) -> bool {
758 if g.start > 0 && recs_match(xdf, g.start - 1, g.end - 1) {
759 g.start -= 1;
760 g.end -= 1;
761 xdf.set_changed(g.start, true);
762 xdf.set_changed(g.end, false);
763 while xdf.changed(g.start - 1) {
764 g.start -= 1;
765 }
766 true
767 } else {
768 false
769 }
770}
771
772fn compact_one(xdf: &mut CompactFile, xdfo: &mut CompactFile, indent_heuristic: bool) {
779 let mut g = group_init(xdf);
780 let mut go = group_init(xdfo);
781
782 loop {
783 if g.end == g.start {
785 if !group_next(xdf, &mut g) {
786 break;
787 }
788 if !group_next(xdfo, &mut go) {
789 break;
790 }
791 continue;
792 }
793
794 let mut groupsize;
795 let mut earliest_end;
796 let mut end_matching_other;
797
798 loop {
799 groupsize = g.end - g.start;
800 end_matching_other = -1i64;
801
802 while group_slide_up(xdf, &mut g) {
804 let ok = group_previous(xdfo, &mut go);
805 debug_assert!(ok, "group sync broken sliding up");
806 }
807 earliest_end = g.end;
809 if go.end > go.start {
810 end_matching_other = g.end;
811 }
812 loop {
814 if !group_slide_down(xdf, &mut g) {
815 break;
816 }
817 let ok = group_next(xdfo, &mut go);
818 debug_assert!(ok, "group sync broken sliding down");
819 if go.end > go.start {
820 end_matching_other = g.end;
821 }
822 }
823 if groupsize == g.end - g.start {
824 break;
825 }
826 }
827
828 if g.end == earliest_end {
831 } else if end_matching_other != -1 {
833 while go.end == go.start {
837 let ok = group_slide_up(xdf, &mut g);
838 debug_assert!(ok, "match disappeared");
839 let ok = group_previous(xdfo, &mut go);
840 debug_assert!(ok, "group sync broken sliding to match");
841 }
842 } else if indent_heuristic {
843 let mut best_shift = -1i64;
845 let mut best_score = SplitScore::default();
846
847 let mut shift = earliest_end;
848 if g.end - groupsize - 1 > shift {
849 shift = g.end - groupsize - 1;
850 }
851 if g.end - INDENT_HEURISTIC_MAX_SLIDING > shift {
852 shift = g.end - INDENT_HEURISTIC_MAX_SLIDING;
853 }
854 while shift <= g.end {
855 let mut score = SplitScore::default();
856 let m = measure_split(xdf, shift);
857 score_add_split(&m, &mut score);
858 let m = measure_split(xdf, shift - groupsize);
859 score_add_split(&m, &mut score);
860 if best_shift == -1 || score_cmp(&score, &best_score) <= 0 {
861 best_score = score;
862 best_shift = shift;
863 }
864 shift += 1;
865 }
866
867 while g.end > best_shift {
868 let ok = group_slide_up(xdf, &mut g);
869 debug_assert!(ok, "best shift unreached");
870 let ok = group_previous(xdfo, &mut go);
871 debug_assert!(ok, "group sync broken sliding to blank line");
872 }
873 }
874
875 if !group_next(xdf, &mut g) {
877 break;
878 }
879 if !group_next(xdfo, &mut go) {
880 break;
881 }
882 }
883}
884
885fn change_compact(
891 ops: &mut Vec<DiffOp>,
892 old: &[DiffLine<'_>],
893 new: &[DiffLine<'_>],
894 ws_ignore: WsIgnore,
895 indent_heuristic: bool,
896) {
897 if ops.iter().all(|op| matches!(op, DiffOp::Equal(_))) {
899 return;
900 }
901
902 let canon = |lines: &[DiffLine<'_>]| -> Vec<Vec<u8>> {
904 if ws_ignore.is_empty() {
905 lines.iter().map(|l| l.content.to_vec()).collect()
906 } else {
907 lines
908 .iter()
909 .map(|l| crate::canonicalize_line_for_match(l.content, ws_ignore))
910 .collect()
911 }
912 };
913
914 let mut xdf1 = CompactFile {
915 recs: canon(old),
916 changed: vec![false; old.len()],
917 };
918 let mut xdf2 = CompactFile {
919 recs: canon(new),
920 changed: vec![false; new.len()],
921 };
922
923 let mut oi = 0usize;
925 let mut ni = 0usize;
926 for op in ops.iter() {
927 match *op {
928 DiffOp::Equal(n) => {
929 oi += n;
930 ni += n;
931 }
932 DiffOp::Delete(n) => {
933 for _ in 0..n {
934 xdf1.changed[oi] = true;
935 oi += 1;
936 }
937 }
938 DiffOp::Insert(n) => {
939 for _ in 0..n {
940 xdf2.changed[ni] = true;
941 ni += 1;
942 }
943 }
944 }
945 }
946
947 compact_one(&mut xdf1, &mut xdf2, indent_heuristic);
949 compact_one(&mut xdf2, &mut xdf1, indent_heuristic);
950
951 let n_old = xdf1.changed.len();
954 let n_new = xdf2.changed.len();
955 let mut rebuilt: Vec<DiffOp> = Vec::with_capacity(ops.len());
956 let mut i = 0usize; let mut j = 0usize; while i < n_old || j < n_new {
959 let del = i < n_old && xdf1.changed[i];
960 let ins = j < n_new && xdf2.changed[j];
961 if del {
962 let mut run = 0usize;
963 while i < n_old && xdf1.changed[i] {
964 run += 1;
965 i += 1;
966 }
967 push_op(&mut rebuilt, DiffOp::Delete(run));
968 } else if ins {
969 let mut run = 0usize;
970 while j < n_new && xdf2.changed[j] {
971 run += 1;
972 j += 1;
973 }
974 push_op(&mut rebuilt, DiffOp::Insert(run));
975 } else {
976 let mut run = 0usize;
978 while i < n_old && j < n_new && !xdf1.changed[i] && !xdf2.changed[j] {
979 run += 1;
980 i += 1;
981 j += 1;
982 }
983 debug_assert!(run > 0, "change_compact stalled rebuilding script");
984 push_op(&mut rebuilt, DiffOp::Equal(run));
985 }
986 }
987
988 *ops = rebuilt;
989}
990
991fn push_op(out: &mut Vec<DiffOp>, op: DiffOp) {
993 match (out.last_mut(), op) {
994 (Some(DiffOp::Equal(prev)), DiffOp::Equal(n)) => *prev += n,
995 (Some(DiffOp::Delete(prev)), DiffOp::Delete(n)) => *prev += n,
996 (Some(DiffOp::Insert(prev)), DiffOp::Insert(n)) => *prev += n,
997 _ => out.push(op),
998 }
999}
1000
1001struct RangeFilter<'r> {
1008 ranges: &'r [LineRange],
1009 cur_range: usize,
1010 lno_post: i64,
1012 lno_pre: i64,
1013 func: Vec<u8>,
1016 rhunk: Vec<u8>,
1018 rhunk_old_begin: i64,
1019 rhunk_old_count: i64,
1020 rhunk_new_begin: i64,
1021 rhunk_new_count: i64,
1022 rhunk_active: bool,
1023 rhunk_has_changes: bool,
1024 pending_rm: Vec<u8>,
1026 pending_rm_count: i64,
1027 pending_rm_pre_begin: i64,
1028}
1029
1030impl RangeFilter<'_> {
1031 fn discard_pending_rm(&mut self) {
1032 self.pending_rm.clear();
1033 self.pending_rm_count = 0;
1034 }
1035
1036 fn flush_rhunk(&mut self, out: &mut Vec<u8>) {
1039 if !self.rhunk_active {
1040 return;
1041 }
1042 if self.pending_rm_count != 0 {
1043 self.rhunk.extend_from_slice(&self.pending_rm);
1044 self.rhunk_old_count += self.pending_rm_count;
1045 self.rhunk_has_changes = true;
1046 self.discard_pending_rm();
1047 }
1048 if !self.rhunk_has_changes {
1049 self.rhunk_active = false;
1050 self.rhunk.clear();
1051 return;
1052 }
1053 out.extend_from_slice(
1057 format!(
1058 "@@ -{},{} +{},{} @@",
1059 self.rhunk_old_begin,
1060 self.rhunk_old_count,
1061 self.rhunk_new_begin,
1062 self.rhunk_new_count
1063 )
1064 .as_bytes(),
1065 );
1066 if !self.func.is_empty() {
1067 out.push(b' ');
1068 out.extend_from_slice(&self.func);
1069 }
1070 out.push(b'\n');
1071 out.extend_from_slice(&self.rhunk);
1072 self.rhunk_active = false;
1073 self.rhunk.clear();
1074 }
1075
1076 fn body_line(&mut self, out: &mut Vec<u8>, marker: u8, line: &[u8]) {
1079 if marker == b'-' {
1080 if self.pending_rm_count == 0 {
1081 self.pending_rm_pre_begin = self.lno_pre;
1082 }
1083 self.lno_pre += 1;
1084 self.pending_rm.extend_from_slice(line);
1085 self.pending_rm_count += 1;
1086 return;
1087 }
1088 if marker == b'\\' {
1089 if self.pending_rm_count != 0 {
1090 self.pending_rm.extend_from_slice(line);
1091 } else if self.rhunk_active {
1092 self.rhunk.extend_from_slice(line);
1093 }
1094 return;
1095 }
1096 let lno_0 = self.lno_post - 1;
1098 let cur_pre = self.lno_pre;
1099 self.lno_post += 1;
1100 if marker == b' ' {
1101 self.lno_pre += 1;
1102 }
1103
1104 while self.cur_range < self.ranges.len() && lno_0 >= self.ranges[self.cur_range].end {
1105 if self.rhunk_active {
1106 self.flush_rhunk(out);
1107 }
1108 self.discard_pending_rm();
1109 self.cur_range += 1;
1110 }
1111 if self.cur_range >= self.ranges.len() {
1112 self.discard_pending_rm();
1113 return;
1114 }
1115 let cur = self.ranges[self.cur_range];
1116 if lno_0 < cur.start {
1117 self.discard_pending_rm();
1118 return;
1119 }
1120 if !self.rhunk_active {
1121 self.rhunk_active = true;
1122 self.rhunk_has_changes = false;
1123 self.rhunk_new_begin = lno_0 + 1;
1124 self.rhunk_old_begin = if self.pending_rm_count != 0 {
1125 self.pending_rm_pre_begin
1126 } else {
1127 cur_pre
1128 };
1129 self.rhunk_old_count = 0;
1130 self.rhunk_new_count = 0;
1131 self.rhunk.clear();
1132 }
1133 if self.pending_rm_count != 0 {
1134 self.rhunk.extend_from_slice(&self.pending_rm);
1135 self.rhunk_old_count += self.pending_rm_count;
1136 self.rhunk_has_changes = true;
1137 self.discard_pending_rm();
1138 }
1139 self.rhunk.extend_from_slice(line);
1140 self.rhunk_new_count += 1;
1141 if marker == b'+' {
1142 self.rhunk_has_changes = true;
1143 } else {
1144 self.rhunk_old_count += 1;
1145 }
1146 }
1147}
1148
1149fn filter_hunks_to_ranges(out: &mut Vec<u8>, full: &[u8], ranges: &[LineRange]) {
1156 if ranges.is_empty() {
1157 return;
1158 }
1159 let mut filter = RangeFilter {
1160 ranges,
1161 cur_range: 0,
1162 lno_post: 0,
1163 lno_pre: 0,
1164 func: Vec::new(),
1165 rhunk: Vec::new(),
1166 rhunk_old_begin: 0,
1167 rhunk_old_count: 0,
1168 rhunk_new_begin: 0,
1169 rhunk_new_count: 0,
1170 rhunk_active: false,
1171 rhunk_has_changes: false,
1172 pending_rm: Vec::new(),
1173 pending_rm_count: 0,
1174 pending_rm_pre_begin: 0,
1175 };
1176 for line in split_keep_newline(full) {
1177 if line.starts_with(b"@@ ") {
1178 if let Some((old_begin, new_begin, func)) = parse_hunk_header(line) {
1183 filter.lno_post = new_begin;
1184 filter.lno_pre = old_begin;
1185 filter.func = func;
1186 }
1187 continue;
1188 }
1189 let marker = line.first().copied().unwrap_or(b' ');
1190 filter.body_line(out, marker, line);
1191 }
1192 filter.flush_rhunk(out);
1193}
1194
1195fn split_keep_newline(buf: &[u8]) -> impl Iterator<Item = &[u8]> {
1198 let mut start = 0usize;
1199 std::iter::from_fn(move || {
1200 if start >= buf.len() {
1201 return None;
1202 }
1203 let rel = buf[start..].iter().position(|&b| b == b'\n');
1204 let end = match rel {
1205 Some(pos) => start + pos + 1,
1206 None => buf.len(),
1207 };
1208 let line = &buf[start..end];
1209 start = end;
1210 Some(line)
1211 })
1212}
1213
1214fn parse_hunk_header(line: &[u8]) -> Option<(i64, i64, Vec<u8>)> {
1219 let rest = line.strip_prefix(b"@@ -")?;
1221 let plus = rest.iter().position(|&b| b == b'+')?;
1222 let old_part = &rest[..plus];
1223 let after_plus = &rest[plus + 1..];
1225 let close = find_subslice(after_plus, b" @@")?;
1226 let new_part = &after_plus[..close];
1227 let old_begin = parse_range_begin(old_part.split(|&b| b == b' ').next().unwrap_or(old_part))?;
1228 let new_begin = parse_range_begin(new_part)?;
1229 let tail = &after_plus[close + 3..];
1231 let func = if let Some(f) = tail.strip_prefix(b" ") {
1232 let mut f = f.to_vec();
1233 if f.last() == Some(&b'\n') {
1234 f.pop();
1235 }
1236 f
1237 } else {
1238 Vec::new()
1239 };
1240 Some((old_begin, new_begin, func))
1241}
1242
1243fn parse_range_begin(field: &[u8]) -> Option<i64> {
1245 let begin = field.split(|&b| b == b',').next().unwrap_or(field);
1246 std::str::from_utf8(begin).ok()?.trim().parse::<i64>().ok()
1247}
1248
1249fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
1250 if needle.is_empty() || haystack.len() < needle.len() {
1251 return None;
1252 }
1253 (0..=haystack.len() - needle.len()).find(|&i| &haystack[i..i + needle.len()] == needle)
1254}
1255
1256#[derive(Clone, Copy)]
1261struct Change {
1262 i1: usize,
1264 chg1: usize,
1266 i2: usize,
1268 chg2: usize,
1270 tag_first: usize,
1272 tag_last: usize,
1274 ignore: bool,
1276}
1277
1278fn build_changes(tagged: &[TaggedLine<'_>]) -> Vec<Change> {
1281 let mut changes: Vec<Change> = Vec::new();
1282 let mut idx = 0usize;
1283 while idx < tagged.len() {
1284 if tagged[idx].kind == LineKind::Context {
1285 idx += 1;
1286 continue;
1287 }
1288 let tag_first = idx;
1289 let i1 = tagged[idx].old_index;
1290 let i2 = tagged[idx].new_index;
1291 let mut chg1 = 0usize;
1292 let mut chg2 = 0usize;
1293 while idx < tagged.len() && tagged[idx].kind != LineKind::Context {
1294 match tagged[idx].kind {
1295 LineKind::Delete => chg1 += 1,
1296 LineKind::Insert => chg2 += 1,
1297 LineKind::Context => unreachable!(),
1298 }
1299 idx += 1;
1300 }
1301 changes.push(Change {
1302 i1,
1303 chg1,
1304 i2,
1305 chg2,
1306 tag_first,
1307 tag_last: idx - 1,
1308 ignore: false,
1309 });
1310 }
1311 changes
1312}
1313
1314fn mark_ignorable_changes(
1319 changes: &mut [Change],
1320 old: &[DiffLine<'_>],
1321 new: &[DiffLine<'_>],
1322 ws_ignore: WsIgnore,
1323 ci: &ChangeIgnore<'_>,
1324) {
1325 for change in changes.iter_mut() {
1326 if ci.ignore_blank_lines {
1327 let blank = (change.i1..change.i1 + change.chg1)
1328 .all(|i| line_is_blank(old[i].content, ws_ignore))
1329 && (change.i2..change.i2 + change.chg2)
1330 .all(|i| line_is_blank(new[i].content, ws_ignore));
1331 change.ignore = blank;
1332 }
1333 if !change.ignore
1334 && let Some(regex_match) = ci.regex_match
1335 {
1336 let matched = (change.i1..change.i1 + change.chg1).all(|i| regex_match(old[i].content))
1337 && (change.i2..change.i2 + change.chg2).all(|i| regex_match(new[i].content));
1338 change.ignore = matched;
1339 }
1340 }
1341}
1342
1343fn group_changes_into_hunks(
1350 changes: &[Change],
1351 context: usize,
1352 interhunk: usize,
1353) -> Vec<(usize, usize)> {
1354 let max_common = context.saturating_add(context).saturating_add(interhunk);
1355 let max_ignorable = context;
1356
1357 let mut hunks: Vec<(usize, usize)> = Vec::new();
1358 let mut start = 0usize;
1361 while start < changes.len() {
1362 {
1370 let mut xchp = start;
1371 while xchp < changes.len() && changes[xchp].ignore {
1372 let cur = &changes[xchp];
1373 match changes.get(xchp + 1) {
1374 None => {
1375 start = changes.len();
1376 }
1377 Some(next) => {
1378 if next.i1 - (cur.i1 + cur.chg1) >= max_ignorable {
1379 start = xchp + 1;
1380 }
1381 }
1382 }
1383 xchp += 1;
1384 }
1385 }
1386 if start >= changes.len() {
1387 break;
1388 }
1389
1390 let mut last = start;
1393 let mut ignored = 0usize; let mut prev = start;
1395 let mut idx = start + 1;
1396 while idx < changes.len() {
1397 let xch = &changes[idx];
1398 let xchp = &changes[prev];
1399 let distance = xch.i1 - (xchp.i1 + xchp.chg1);
1400 if distance > max_common {
1401 break;
1402 }
1403 if distance < max_ignorable && (!xch.ignore || last == prev) {
1404 last = idx;
1405 ignored = 0;
1406 } else if distance < max_ignorable && xch.ignore {
1407 ignored += xch.chg2;
1408 } else if last != prev
1409 && xch.i1 + ignored - (changes[last].i1 + changes[last].chg1) > max_common
1410 {
1411 break;
1412 } else if !xch.ignore {
1413 last = idx;
1414 ignored = 0;
1415 } else {
1416 ignored += xch.chg2;
1417 }
1418 prev = idx;
1419 idx += 1;
1420 }
1421
1422 let first_change = &changes[start];
1423 let last_change = &changes[last];
1424 hunks.push((first_change.tag_first, last_change.tag_last));
1425 start = last + 1;
1426 }
1427
1428 hunks
1429}
1430
1431fn expand_hunks_to_function_context(
1432 groups: &[(usize, usize)],
1433 tagged: &[TaggedLine<'_>],
1434 old: &[DiffLine<'_>],
1435 new: &[DiffLine<'_>],
1436 mut heading: Option<&mut HeadingFn<'_>>,
1437) -> Vec<(usize, usize)> {
1438 let Some(classifier) = heading.as_mut() else {
1439 return groups.to_vec();
1440 };
1441 let mut expanded = Vec::with_capacity(groups.len());
1442 for &(start, end) in groups {
1443 let first = tagged[start];
1444 let last = tagged[end];
1445 let old_changed = tagged[start..=end]
1446 .iter()
1447 .any(|line| line.kind == LineKind::Delete);
1448 let (side, range) = if old_changed {
1449 (
1450 FunctionSide::Old,
1451 function_context_range(old, first.old_index, false, classifier),
1452 )
1453 } else {
1454 (
1455 FunctionSide::New,
1456 function_context_range(new, first.new_index, true, classifier),
1457 )
1458 };
1459 let Some((range_start, range_end)) = range else {
1460 expanded.push((start, end));
1461 continue;
1462 };
1463 let mut hunk_start = expand_tag_start(tagged, start, side, range_start);
1464 let mut hunk_end = expand_tag_end(tagged, end, side, range_end);
1465 if old_changed {
1466 if last.old_index >= range_end {
1467 hunk_end = end;
1468 }
1469 } else if last.new_index >= range_end {
1470 hunk_end = end;
1471 }
1472 if hunk_start > start {
1473 hunk_start = start;
1474 }
1475 if hunk_end < end {
1476 hunk_end = end;
1477 }
1478 if let Some(prev) = expanded.last_mut()
1479 && hunk_start <= prev.1 + 1
1480 {
1481 prev.1 = prev.1.max(hunk_end);
1482 continue;
1483 }
1484 expanded.push((hunk_start, hunk_end));
1485 }
1486 expanded
1487}
1488
1489#[derive(Clone, Copy)]
1490enum FunctionSide {
1491 Old,
1492 New,
1493}
1494
1495fn function_context_range(
1496 lines: &[DiffLine<'_>],
1497 anchor: usize,
1498 prefer_forward: bool,
1499 heading: &mut HeadingFn<'_>,
1500) -> Option<(usize, usize)> {
1501 if lines.is_empty() {
1502 return None;
1503 }
1504 let anchor = anchor.min(lines.len() - 1);
1505 let mut heading_idx = None;
1506 for idx in (0..=anchor).rev() {
1507 if heading(lines[idx].content).is_some() {
1508 heading_idx = Some(idx);
1509 break;
1510 }
1511 }
1512 if heading_idx.is_none() && prefer_forward {
1513 for (idx, line) in lines.iter().enumerate().skip(anchor) {
1514 if heading(line.content).is_some() {
1515 heading_idx = Some(idx);
1516 break;
1517 }
1518 }
1519 }
1520
1521 let (mut start, mut end) = if let Some(idx) = heading_idx {
1522 let mut start = idx;
1523 while start > 0 && !line_is_blank(lines[start - 1].content, WsIgnore::default()) {
1524 start -= 1;
1525 }
1526 let mut end = lines.len();
1527 for (next, line) in lines.iter().enumerate().skip(idx + 1) {
1528 if heading(line.content).is_some() {
1529 end = next;
1530 break;
1531 }
1532 }
1533 (start, end)
1534 } else {
1535 (0, lines.len())
1536 };
1537
1538 while start < end && line_is_blank(lines[start].content, WsIgnore::default()) {
1539 start += 1;
1540 }
1541 while end > start && line_is_blank(lines[end - 1].content, WsIgnore::default()) {
1542 end -= 1;
1543 }
1544 (start < end).then_some((start, end))
1545}
1546
1547fn expand_tag_start(
1548 tagged: &[TaggedLine<'_>],
1549 current: usize,
1550 side: FunctionSide,
1551 range_start: usize,
1552) -> usize {
1553 let mut start = current;
1554 while start > 0 {
1555 let prev = tagged[start - 1];
1556 let line_index = match side {
1557 FunctionSide::Old => prev.old_index,
1558 FunctionSide::New => prev.new_index,
1559 };
1560 if line_index < range_start {
1561 break;
1562 }
1563 start -= 1;
1564 }
1565 start
1566}
1567
1568fn expand_tag_end(
1569 tagged: &[TaggedLine<'_>],
1570 current: usize,
1571 side: FunctionSide,
1572 range_end: usize,
1573) -> usize {
1574 let mut end = current;
1575 while end + 1 < tagged.len() {
1576 let next = tagged[end + 1];
1577 let line_index = match side {
1578 FunctionSide::Old => next.old_index,
1579 FunctionSide::New => next.new_index,
1580 };
1581 if line_index >= range_end {
1582 break;
1583 }
1584 end += 1;
1585 }
1586 end
1587}
1588
1589const COLOR_MOVED_MIN_ALNUM_COUNT: usize = 20;
1590const INDENT_BLANKLINE: i32 = i32::MIN;
1591
1592#[derive(Clone, Copy, Default)]
1593struct MovedStyle {
1594 moved: bool,
1595 alt: bool,
1596 uninteresting: bool,
1597}
1598
1599#[derive(Clone)]
1600struct MovedEntry {
1601 tag_idx: usize,
1602 next_line: Option<usize>,
1603 next_match: Option<usize>,
1604 id: usize,
1605 indent_width: i32,
1606}
1607
1608#[derive(Clone, Copy, Default)]
1609struct MovedEntryList {
1610 add: Option<usize>,
1611 del: Option<usize>,
1612}
1613
1614#[derive(Clone, Copy)]
1615struct MovedBlock {
1616 match_entry: usize,
1617 wsd: i32,
1618}
1619
1620fn mark_color_as_moved(tagged: &[TaggedLine<'_>], color_moved: ColorMoved) -> Vec<MovedStyle> {
1621 let (entries, entry_for_tag, entry_list) = add_lines_to_move_detection(tagged, color_moved.ws);
1622 let mut styles = vec![MovedStyle::default(); tagged.len()];
1623 let mut pmb: Vec<MovedBlock> = Vec::new();
1624 let mut n = 0usize;
1625 let mut flipped_block = false;
1626 let mut block_length = 0usize;
1627 let mut moved_symbol: Option<LineKind> = None;
1628
1629 while n < tagged.len() {
1630 let line = tagged[n];
1631 let line_entry = entry_for_tag[n];
1632 let mut line_match = line_entry.and_then(|entry_idx| {
1633 let id = entries[entry_idx].id;
1634 match line.kind {
1635 LineKind::Insert => entry_list.get(id).and_then(|list| list.del),
1636 LineKind::Delete => entry_list.get(id).and_then(|list| list.add),
1637 LineKind::Context => None,
1638 }
1639 });
1640
1641 if line.kind == LineKind::Context {
1642 flipped_block = false;
1643 }
1644
1645 if !pmb.is_empty() && (line_match.is_none() || Some(line.kind) != moved_symbol) {
1646 if !adjust_last_block(&mut styles, tagged, color_moved.mode, n, block_length)
1647 && block_length > 1
1648 {
1649 line_match = None;
1650 n -= block_length;
1651 }
1652 pmb.clear();
1653 block_length = 0;
1654 flipped_block = false;
1655 }
1656
1657 let Some(line_match) = line_match else {
1658 moved_symbol = None;
1659 n += 1;
1660 continue;
1661 };
1662
1663 if color_moved.mode == ColorMovedMode::Plain {
1664 styles[n].moved = true;
1665 n += 1;
1666 continue;
1667 }
1668
1669 pmb_advance_or_null(
1670 &mut pmb,
1671 &entries,
1672 tagged,
1673 line_entry.expect("plus/minus line has move-detection entry"),
1674 color_moved.ws,
1675 );
1676
1677 if pmb.is_empty() {
1678 let contiguous =
1679 adjust_last_block(&mut styles, tagged, color_moved.mode, n, block_length);
1680 if !contiguous && block_length > 1 {
1681 n -= block_length;
1682 } else {
1683 fill_potential_moved_blocks(
1684 line_match,
1685 &entries,
1686 tagged,
1687 n,
1688 color_moved.ws,
1689 &mut pmb,
1690 );
1691 }
1692
1693 if contiguous && !pmb.is_empty() && moved_symbol == Some(line.kind) {
1694 flipped_block = !flipped_block;
1695 } else {
1696 flipped_block = false;
1697 }
1698
1699 moved_symbol = (!pmb.is_empty()).then_some(line.kind);
1700 block_length = 0;
1701 }
1702
1703 if !pmb.is_empty() {
1704 block_length += 1;
1705 styles[n].moved = true;
1706 if flipped_block && color_moved.mode != ColorMovedMode::Blocks {
1707 styles[n].alt = true;
1708 }
1709 }
1710 n += 1;
1711 }
1712
1713 adjust_last_block(&mut styles, tagged, color_moved.mode, n, block_length);
1714 if color_moved.mode == ColorMovedMode::DimmedZebra {
1715 dim_moved_lines(&mut styles, tagged);
1716 }
1717 styles
1718}
1719
1720fn add_lines_to_move_detection(
1721 tagged: &[TaggedLine<'_>],
1722 ws: ColorMovedWs,
1723) -> (Vec<MovedEntry>, Vec<Option<usize>>, Vec<MovedEntryList>) {
1724 let mut entries: Vec<MovedEntry> = Vec::new();
1725 let mut entry_for_tag = vec![None; tagged.len()];
1726 let mut entry_list = Vec::<MovedEntryList>::new();
1727 let mut interned = HashMap::<Vec<u8>, usize>::new();
1728 let mut prev_line: Option<usize> = None;
1729
1730 for (tag_idx, line) in tagged.iter().enumerate() {
1731 if line.kind != LineKind::Insert && line.kind != LineKind::Delete {
1732 prev_line = None;
1733 continue;
1734 }
1735 let indent = if ws.allow_indentation_change {
1736 moved_indent_data(line.content)
1737 } else {
1738 (0, 0)
1739 };
1740 let key = moved_line_key(line.content, indent.0, ws.ignore);
1741 let id = match interned.get(&key) {
1742 Some(id) => *id,
1743 None => {
1744 let id = interned.len();
1745 interned.insert(key, id);
1746 entry_list.push(MovedEntryList::default());
1747 id
1748 }
1749 };
1750
1751 let entry_idx = entries.len();
1752 if let Some(prev) = prev_line
1753 && tagged[entries[prev].tag_idx].kind == line.kind
1754 {
1755 entries[prev].next_line = Some(entry_idx);
1756 }
1757 let next_match = match line.kind {
1758 LineKind::Insert => entry_list[id].add,
1759 LineKind::Delete => entry_list[id].del,
1760 LineKind::Context => None,
1761 };
1762 entries.push(MovedEntry {
1763 tag_idx,
1764 next_line: None,
1765 next_match,
1766 id,
1767 indent_width: indent.1,
1768 });
1769 match line.kind {
1770 LineKind::Insert => entry_list[id].add = Some(entry_idx),
1771 LineKind::Delete => entry_list[id].del = Some(entry_idx),
1772 LineKind::Context => {}
1773 }
1774 entry_for_tag[tag_idx] = Some(entry_idx);
1775 prev_line = Some(entry_idx);
1776 }
1777
1778 (entries, entry_for_tag, entry_list)
1779}
1780
1781fn moved_line_key(line: &[u8], indent_off: usize, ignore: WsIgnore) -> Vec<u8> {
1782 let bytes = line.get(indent_off..).unwrap_or_default();
1783 if ignore.is_empty() {
1784 bytes.to_vec()
1785 } else {
1786 canonicalize_moved_line(bytes, ignore)
1787 }
1788}
1789
1790fn canonicalize_moved_line(line: &[u8], ignore: WsIgnore) -> Vec<u8> {
1791 let is_ws = |b: u8| matches!(b, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c);
1792 if ignore.all_space {
1793 return line.iter().copied().filter(|b| !is_ws(*b)).collect();
1794 }
1795 if ignore.space_change {
1796 let mut out = Vec::with_capacity(line.len());
1797 let mut i = 0usize;
1798 while i < line.len() {
1799 if is_ws(line[i]) {
1800 out.push(b' ');
1801 while i < line.len() && is_ws(line[i]) {
1802 i += 1;
1803 }
1804 } else {
1805 out.push(line[i]);
1806 i += 1;
1807 }
1808 }
1809 while out.last() == Some(&b' ') {
1810 out.pop();
1811 }
1812 return out;
1813 }
1814 if ignore.space_at_eol {
1815 let mut end = line.len();
1816 if end > 0 && line[end - 1] == b'\n' {
1817 end -= 1;
1818 }
1819 while end > 0 && matches!(line[end - 1], b' ' | b'\t') {
1820 end -= 1;
1821 }
1822 let mut out = line[..end].to_vec();
1823 if line.ends_with(b"\n") {
1824 out.push(b'\n');
1825 }
1826 return out;
1827 }
1828 if ignore.cr_at_eol {
1829 let mut out = line.to_vec();
1830 if out.ends_with(b"\r\n") {
1831 let len = out.len();
1832 out.remove(len - 2);
1833 }
1834 return out;
1835 }
1836 line.to_vec()
1837}
1838
1839fn moved_indent_data(line: &[u8]) -> (usize, i32) {
1840 let mut off = 0usize;
1841 let mut width = 0i32;
1842 while off < line.len() && matches!(line[off], b'\x0c' | b'\x0b' | b'\r') {
1843 off += 1;
1844 }
1845 while off < line.len() {
1846 match line[off] {
1847 b' ' => {
1848 width += 1;
1849 off += 1;
1850 }
1851 b'\t' => {
1852 width += 8 - (width % 8);
1853 off += 1;
1854 while off < line.len() && line[off] == b'\t' {
1855 width += 8;
1856 off += 1;
1857 }
1858 }
1859 _ => break,
1860 }
1861 }
1862 if line[off..].iter().all(|b| b.is_ascii_whitespace()) {
1863 (line.len(), INDENT_BLANKLINE)
1864 } else {
1865 (off, width)
1866 }
1867}
1868
1869fn pmb_advance_or_null(
1870 pmb: &mut Vec<MovedBlock>,
1871 entries: &[MovedEntry],
1872 tagged: &[TaggedLine<'_>],
1873 line_entry: usize,
1874 ws: ColorMovedWs,
1875) {
1876 let mut kept = Vec::with_capacity(pmb.len());
1877 for mut block in pmb.iter().copied() {
1878 let Some(cur) = entries[block.match_entry].next_line else {
1879 continue;
1880 };
1881 let matched = if ws.allow_indentation_change {
1882 !cmp_in_block_with_wsd(entries, cur, tagged, line_entry, &mut block)
1883 } else {
1884 entries[cur].id == entries[line_entry].id
1885 };
1886 if matched {
1887 block.match_entry = cur;
1888 kept.push(block);
1889 }
1890 }
1891 *pmb = kept;
1892}
1893
1894fn fill_potential_moved_blocks(
1895 mut line_match: usize,
1896 entries: &[MovedEntry],
1897 tagged: &[TaggedLine<'_>],
1898 tag_idx: usize,
1899 ws: ColorMovedWs,
1900 pmb: &mut Vec<MovedBlock>,
1901) {
1902 loop {
1903 let entry = &entries[line_match];
1904 let wsd = if ws.allow_indentation_change {
1905 compute_ws_delta(tagged[tag_idx].content, entry.indent_width)
1906 } else {
1907 0
1908 };
1909 pmb.push(MovedBlock {
1910 match_entry: line_match,
1911 wsd,
1912 });
1913 match entry.next_match {
1914 Some(next) => line_match = next,
1915 None => break,
1916 }
1917 }
1918}
1919
1920fn compute_ws_delta(line: &[u8], match_indent_width: i32) -> i32 {
1921 let (_, width) = moved_indent_data(line);
1922 if width == INDENT_BLANKLINE && match_indent_width == INDENT_BLANKLINE {
1923 INDENT_BLANKLINE
1924 } else {
1925 width - match_indent_width
1926 }
1927}
1928
1929fn cmp_in_block_with_wsd(
1930 entries: &[MovedEntry],
1931 cur: usize,
1932 tagged: &[TaggedLine<'_>],
1933 line_entry: usize,
1934 block: &mut MovedBlock,
1935) -> bool {
1936 let cur_entry = &entries[cur];
1937 if cur_entry.id != entries[line_entry].id {
1938 return true;
1939 }
1940 if cur_entry.indent_width == INDENT_BLANKLINE {
1941 return false;
1942 }
1943 let (_, line_width) = moved_indent_data(tagged[entries[line_entry].tag_idx].content);
1944 let delta = line_width - cur_entry.indent_width;
1945 if block.wsd == INDENT_BLANKLINE {
1946 block.wsd = delta;
1947 }
1948 delta != block.wsd
1949}
1950
1951fn adjust_last_block(
1952 styles: &mut [MovedStyle],
1953 tagged: &[TaggedLine<'_>],
1954 mode: ColorMovedMode,
1955 n: usize,
1956 block_length: usize,
1957) -> bool {
1958 if mode == ColorMovedMode::Plain {
1959 return block_length != 0;
1960 }
1961 let mut alnum_count = 0usize;
1962 for i in 1..=block_length {
1963 for byte in tagged[n - i].content {
1964 if byte.is_ascii_alphanumeric() {
1965 alnum_count += 1;
1966 if alnum_count >= COLOR_MOVED_MIN_ALNUM_COUNT {
1967 return true;
1968 }
1969 }
1970 }
1971 }
1972 for i in 1..=block_length {
1973 styles[n - i] = MovedStyle::default();
1974 }
1975 false
1976}
1977
1978fn dim_moved_lines(styles: &mut [MovedStyle], tagged: &[TaggedLine<'_>]) {
1979 for n in 0..tagged.len() {
1980 if tagged[n].kind != LineKind::Insert && tagged[n].kind != LineKind::Delete {
1981 continue;
1982 }
1983 if !styles[n].moved {
1984 continue;
1985 }
1986 let prev = (n > 0
1987 && (tagged[n - 1].kind == LineKind::Insert || tagged[n - 1].kind == LineKind::Delete))
1988 .then_some(n - 1);
1989 let next = (n + 1 < tagged.len()
1990 && (tagged[n + 1].kind == LineKind::Insert || tagged[n + 1].kind == LineKind::Delete))
1991 .then_some(n + 1);
1992
1993 if prev.is_some_and(|i| moved_zebra_mask(styles[i]) == moved_zebra_mask(styles[n]))
1994 && next.is_some_and(|i| moved_zebra_mask(styles[i]) == moved_zebra_mask(styles[n]))
1995 {
1996 styles[n].uninteresting = true;
1997 continue;
1998 }
1999 if prev.is_some_and(|i| styles[i].moved && styles[i].alt != styles[n].alt) {
2000 continue;
2001 }
2002 if next.is_some_and(|i| styles[i].moved && styles[i].alt != styles[n].alt) {
2003 continue;
2004 }
2005 styles[n].uninteresting = true;
2006 }
2007}
2008
2009fn moved_zebra_mask(style: MovedStyle) -> (bool, bool) {
2010 (style.moved, style.alt)
2011}
2012
2013fn render_one_hunk(
2017 out: &mut Vec<u8>,
2018 tagged: &[TaggedLine<'_>],
2019 moved_styles: Option<&[MovedStyle]>,
2020 old_lines: &[DiffLine<'_>],
2021 start: usize,
2022 end: usize,
2023 options: &mut HunkRenderOptions<'_, '_>,
2024) {
2025 let slice = &tagged[start..end];
2026 let mut old_count = 0usize;
2027 let mut new_count = 0usize;
2028 for line in slice {
2029 match line.kind {
2030 LineKind::Context => {
2031 old_count += 1;
2032 new_count += 1;
2033 }
2034 LineKind::Delete => old_count += 1,
2035 LineKind::Insert => new_count += 1,
2036 }
2037 }
2038 let old_start = if old_count == 0 {
2040 slice.first().map(|line| line.old_index).unwrap_or(0)
2041 } else {
2042 slice
2043 .iter()
2044 .find(|line| line.kind != LineKind::Insert)
2045 .map(|line| line.old_index + 1)
2046 .unwrap_or(1)
2047 };
2048 let new_start = if new_count == 0 {
2049 slice.first().map(|line| line.new_index).unwrap_or(0)
2050 } else {
2051 slice
2052 .iter()
2053 .find(|line| line.kind != LineKind::Delete)
2054 .map(|line| line.new_index + 1)
2055 .unwrap_or(1)
2056 };
2057
2058 let heading = hunk_section_heading(
2059 old_lines,
2060 slice.first().map(|line| line.old_index),
2061 options.heading.as_deref_mut(),
2062 );
2063 let frag = format!(
2064 "@@ -{} +{} @@",
2065 format_hunk_range(old_start, old_count),
2066 format_hunk_range(new_start, new_count)
2067 );
2068 match options.colors {
2069 Some(colors) => {
2073 out.extend_from_slice(colors.frag.as_bytes());
2074 out.extend_from_slice(frag.as_bytes());
2075 out.extend_from_slice(colors.reset.as_bytes());
2076 if let Some(heading) = &heading {
2077 out.extend_from_slice(colors.context.as_bytes());
2078 out.push(b' ');
2079 out.extend_from_slice(colors.reset.as_bytes());
2080 out.extend_from_slice(colors.func.as_bytes());
2081 out.extend_from_slice(heading);
2082 out.extend_from_slice(colors.reset.as_bytes());
2083 }
2084 out.push(b'\n');
2085 }
2086 None => {
2087 out.extend_from_slice(frag.as_bytes());
2088 if let Some(heading) = &heading {
2089 out.push(b' ');
2090 out.extend_from_slice(heading);
2091 }
2092 out.push(b'\n');
2093 }
2094 }
2095
2096 if let Some(word_diff) = options.word_diff.as_deref_mut() {
2097 for line in slice {
2101 match line.kind {
2102 LineKind::Delete => word_diff.push_minus(line.content),
2103 LineKind::Insert => word_diff.push_plus(line.content),
2104 LineKind::Context => {
2105 word_diff.flush(out);
2106 word_diff.emit_context_line(out, line.content);
2107 }
2108 }
2109 }
2110 word_diff.flush(out);
2111 return;
2112 }
2113
2114 for (offset, line) in slice.iter().enumerate() {
2115 let prefix = match line.kind {
2116 LineKind::Context => b' ',
2117 LineKind::Delete => b'-',
2118 LineKind::Insert => b'+',
2119 };
2120 match options.colors {
2121 Some(colors) => {
2122 let ws_rule = options.ws_error.and_then(|ws| {
2125 let enabled = match line.kind {
2126 LineKind::Context => ws.context,
2127 LineKind::Delete => ws.old,
2128 LineKind::Insert => ws.new,
2129 };
2130 enabled.then_some(ws.rule)
2131 });
2132 let moved = moved_styles
2133 .and_then(|styles| styles.get(start + offset))
2134 .copied()
2135 .filter(|style| style.moved);
2136 write_patch_line_colored(out, prefix, line.content, colors, ws_rule, moved);
2137 }
2138 None => write_patch_line(out, prefix, line.content),
2139 }
2140 }
2141}
2142
2143fn format_hunk_range(start: usize, count: usize) -> String {
2146 if count == 1 {
2147 start.to_string()
2148 } else {
2149 format!("{start},{count}")
2150 }
2151}
2152
2153fn hunk_section_heading(
2159 old_lines: &[DiffLine<'_>],
2160 first_old_index: Option<usize>,
2161 mut heading: Option<&mut HeadingFn<'_>>,
2162) -> Option<Vec<u8>> {
2163 let first = first_old_index?;
2164 let classifier = heading.as_mut()?;
2165 for idx in (0..first).rev() {
2167 if let Some(found) = classifier(old_lines[idx].content) {
2168 return Some(found);
2169 }
2170 }
2171 None
2172}
2173
2174fn write_patch_line(out: &mut Vec<u8>, prefix: u8, line: &[u8]) {
2178 out.push(prefix);
2179 out.extend_from_slice(line);
2180 if !line.ends_with(b"\n") {
2181 out.extend_from_slice(b"\n\\ No newline at end of file\n");
2182 }
2183}
2184
2185fn write_patch_line_colored(
2198 out: &mut Vec<u8>,
2199 prefix: u8,
2200 line: &[u8],
2201 colors: RenderColors<'_>,
2202 ws_rule: Option<crate::ws::WsRule>,
2203 moved: Option<MovedStyle>,
2204) {
2205 let (body, terminated) = match line.split_last() {
2206 Some((b'\n', body)) => (body, true),
2207 _ => (line, false),
2208 };
2209 let color = match (prefix, moved) {
2210 (b'-', Some(style)) if style.uninteresting && style.alt => colors.old_moved_alt_dim,
2211 (b'-', Some(style)) if style.uninteresting => colors.old_moved_dim,
2212 (b'-', Some(style)) if style.alt => colors.old_moved_alt,
2213 (b'-', Some(_)) => colors.old_moved,
2214 (b'+', Some(style)) if style.uninteresting && style.alt => colors.new_moved_alt_dim,
2215 (b'+', Some(style)) if style.uninteresting => colors.new_moved_dim,
2216 (b'+', Some(style)) if style.alt => colors.new_moved_alt,
2217 (b'+', Some(_)) => colors.new_moved,
2218 (b'-', _) => colors.old,
2219 (b'+', _) => colors.new,
2220 _ => colors.context,
2221 };
2222
2223 if let Some(rule) = ws_rule {
2224 if rule == 0 {
2225 out.extend_from_slice(color.as_bytes());
2226 out.push(prefix);
2227 out.extend_from_slice(body);
2228 out.extend_from_slice(colors.reset.as_bytes());
2229 out.push(b'\n');
2230 if !terminated {
2231 out.extend_from_slice(colors.context.as_bytes());
2232 out.extend_from_slice(b"\\ No newline at end of file");
2233 out.extend_from_slice(colors.reset.as_bytes());
2234 out.push(b'\n');
2235 }
2236 return;
2237 }
2238 out.extend_from_slice(color.as_bytes());
2241 out.push(prefix);
2242 out.extend_from_slice(colors.reset.as_bytes());
2243 let emit_colors = crate::ws::WsEmitColors {
2244 set: color,
2245 reset: colors.reset,
2246 ws: colors.whitespace,
2247 };
2248 crate::ws::ws_check_emit(body, rule, out, &emit_colors);
2249 out.push(b'\n');
2250 if !terminated {
2251 let marker_color = if rule & crate::ws::WS_INCOMPLETE_LINE != 0 {
2252 colors.whitespace
2253 } else {
2254 colors.context
2255 };
2256 out.extend_from_slice(marker_color.as_bytes());
2257 out.extend_from_slice(b"\\ No newline at end of file");
2258 out.extend_from_slice(colors.reset.as_bytes());
2259 out.push(b'\n');
2260 }
2261 return;
2262 }
2263
2264 if prefix == b'+' {
2265 out.extend_from_slice(color.as_bytes());
2266 out.push(prefix);
2267 out.extend_from_slice(colors.reset.as_bytes());
2268 if !body.is_empty() {
2269 out.extend_from_slice(color.as_bytes());
2270 out.extend_from_slice(body);
2271 out.extend_from_slice(colors.reset.as_bytes());
2272 }
2273 } else {
2274 out.extend_from_slice(color.as_bytes());
2275 out.push(prefix);
2276 out.extend_from_slice(body);
2277 out.extend_from_slice(colors.reset.as_bytes());
2278 }
2279 out.push(b'\n');
2280 if !terminated {
2281 out.extend_from_slice(colors.context.as_bytes());
2282 out.extend_from_slice(b"\\ No newline at end of file");
2283 out.extend_from_slice(colors.reset.as_bytes());
2284 out.push(b'\n');
2285 }
2286}
2287
2288struct CdLine {
2318 bol: Vec<u8>,
2320 lost: Vec<CdLost>,
2324 plost: Vec<Vec<u8>>,
2327 flag: u64,
2329 p_lno: Vec<u64>,
2332}
2333
2334struct CdLost {
2336 line: Vec<u8>,
2337 parent_map: u64,
2338}
2339
2340pub struct CombinedRenderOptions {
2342 pub dense: bool,
2345 pub context: usize,
2347 pub algorithm: DiffAlgorithm,
2349 pub ws_ignore: WsIgnore,
2352}
2353
2354impl Default for CombinedRenderOptions {
2355 fn default() -> Self {
2356 Self {
2357 dense: true,
2358 context: DEFAULT_CONTEXT,
2359 algorithm: DiffAlgorithm::Myers,
2360 ws_ignore: WsIgnore::default(),
2361 }
2362 }
2363}
2364
2365pub fn render_combined(out: &mut Vec<u8>, result: &[u8], parents: &[&[u8]]) -> bool {
2377 render_combined_with(out, result, parents, &CombinedRenderOptions::default())
2378}
2379
2380pub fn render_combined_with(
2382 out: &mut Vec<u8>,
2383 result: &[u8],
2384 parents: &[&[u8]],
2385 options: &CombinedRenderOptions,
2386) -> bool {
2387 let num_parent = parents.len();
2388 debug_assert!(num_parent >= 1);
2389
2390 let result_lines = split_lines(result);
2394 let cnt = result_lines.len();
2395
2396 let mut sline: Vec<CdLine> = Vec::with_capacity(cnt + 2);
2401 for line in &result_lines {
2402 sline.push(CdLine {
2403 bol: line.bytes_without_newline().to_vec(),
2404 lost: Vec::new(),
2405 plost: Vec::new(),
2406 flag: 0,
2407 p_lno: vec![0; num_parent],
2408 });
2409 }
2410 for _ in 0..2 {
2411 sline.push(CdLine {
2412 bol: Vec::new(),
2413 lost: Vec::new(),
2414 plost: Vec::new(),
2415 flag: 0,
2416 p_lno: vec![0; num_parent],
2417 });
2418 }
2419
2420 for n in 0..num_parent {
2424 let mut reused = None;
2425 for j in 0..n {
2426 if parents[j] == parents[n] {
2427 reused = Some(j);
2428 break;
2429 }
2430 }
2431 match reused {
2432 Some(j) => reuse_combine_diff(&mut sline, cnt, n, j),
2433 None => combine_one_parent(&mut sline, &result_lines, parents[n], n, options),
2434 }
2435 }
2436
2437 let show_hunks = make_hunks(&mut sline, cnt, num_parent, options.dense, options.context);
2438 if show_hunks {
2439 dump_sline(out, &sline, cnt, num_parent, options.context);
2440 }
2441 show_hunks
2442}
2443
2444fn combine_one_parent(
2447 sline: &mut [CdLine],
2448 result_lines: &[DiffLine<'_>],
2449 parent: &[u8],
2450 n: usize,
2451 options: &CombinedRenderOptions,
2452) {
2453 let cnt = result_lines.len();
2454 let nmask = 1u64 << n;
2455 let parent_lines = split_lines(parent);
2456 let ops = myers_diff_lines_ws(
2457 &parent_lines,
2458 result_lines,
2459 options.ws_ignore,
2460 options.algorithm,
2461 );
2462
2463 let mut old_idx: usize = 0; let mut new_idx: usize = 0; let mut i = 0;
2474 while i < ops.len() {
2475 match ops[i] {
2476 DiffOp::Equal(k) => {
2477 old_idx += k;
2478 new_idx += k;
2479 i += 1;
2480 }
2481 _ => {
2482 let hunk_old_start = old_idx; let hunk_new_start = new_idx; let mut dels: Vec<&[u8]> = Vec::new();
2487 while i < ops.len() {
2488 match ops[i] {
2489 DiffOp::Delete(k) => {
2490 for _ in 0..k {
2491 dels.push(parent_lines[old_idx].bytes_without_newline());
2492 old_idx += 1;
2493 }
2494 i += 1;
2495 }
2496 DiffOp::Insert(k) => {
2497 new_idx += k;
2498 i += 1;
2499 }
2500 DiffOp::Equal(_) => break,
2501 }
2502 }
2503 let _ = hunk_old_start;
2504
2505 for d in &dels {
2516 sline[hunk_new_start].plost.push(d.to_vec());
2517 }
2518 for line in sline.iter_mut().take(new_idx.min(cnt)).skip(hunk_new_start) {
2521 line.flag |= nmask;
2522 }
2523 }
2524 }
2525 }
2526
2527 let mut p_lno: u64 = 1;
2530 for (lno, line) in sline.iter_mut().enumerate().take(cnt + 1) {
2531 line.p_lno[n] = p_lno;
2532 if !line.plost.is_empty() {
2533 let plost = std::mem::take(&mut line.plost);
2534 coalesce_lost(&mut line.lost, plost, n, options);
2535 }
2536 for ll in &line.lost {
2538 if ll.parent_map & nmask != 0 {
2539 p_lno += 1; }
2541 }
2542 if lno < cnt && (line.flag & nmask) == 0 {
2543 p_lno += 1; }
2545 }
2546 sline[cnt + 1].p_lno[n] = p_lno; }
2548
2549fn coalesce_lost(
2554 base: &mut Vec<CdLost>,
2555 newlines: Vec<Vec<u8>>,
2556 n: usize,
2557 options: &CombinedRenderOptions,
2558) {
2559 let pmask = 1u64 << n;
2560 if newlines.is_empty() {
2561 return;
2562 }
2563 if base.is_empty() {
2564 for line in newlines {
2565 base.push(CdLost {
2566 line,
2567 parent_map: pmask,
2568 });
2569 }
2570 return;
2571 }
2572
2573 let m = base.len();
2577 let k = newlines.len();
2578 let mut lcs = vec![vec![0i32; k + 1]; m + 1];
2579 for i in 1..=m {
2580 for j in 1..=k {
2581 if combined_lines_match(&base[i - 1].line, &newlines[j - 1], options.ws_ignore) {
2582 lcs[i][j] = lcs[i - 1][j - 1] + 1;
2583 } else if lcs[i][j - 1] >= lcs[i - 1][j] {
2584 lcs[i][j] = lcs[i][j - 1];
2585 } else {
2586 lcs[i][j] = lcs[i - 1][j];
2587 }
2588 }
2589 }
2590
2591 let mut merged: Vec<CdLost> = Vec::with_capacity(m + k);
2593 let mut i = m;
2594 let mut j = k;
2595 while i > 0 || j > 0 {
2596 if i > 0
2597 && j > 0
2598 && combined_lines_match(&base[i - 1].line, &newlines[j - 1], options.ws_ignore)
2599 {
2600 let mut entry = std::mem::replace(
2601 &mut base[i - 1],
2602 CdLost {
2603 line: Vec::new(),
2604 parent_map: 0,
2605 },
2606 );
2607 entry.parent_map |= pmask;
2608 merged.push(entry);
2609 i -= 1;
2610 j -= 1;
2611 } else if j > 0 && (i == 0 || lcs[i][j - 1] >= lcs[i - 1][j]) {
2612 merged.push(CdLost {
2613 line: newlines[j - 1].clone(),
2614 parent_map: pmask,
2615 });
2616 j -= 1;
2617 } else {
2618 let entry = std::mem::replace(
2619 &mut base[i - 1],
2620 CdLost {
2621 line: Vec::new(),
2622 parent_map: 0,
2623 },
2624 );
2625 merged.push(entry);
2626 i -= 1;
2627 }
2628 }
2629 merged.reverse();
2630 *base = merged;
2631}
2632
2633fn combined_lines_match(a: &[u8], b: &[u8], ws: WsIgnore) -> bool {
2637 if ws.all_space || ws.space_change || ws.space_at_eol {
2638 let at = strip_trailing_ws(a);
2639 let bt = strip_trailing_ws(b);
2640 if !ws.all_space && !ws.space_change {
2641 return at == bt;
2642 }
2643 return ws_squash_eq(at, bt, ws.space_change);
2644 }
2645 a == b
2646}
2647
2648fn strip_trailing_ws(s: &[u8]) -> &[u8] {
2649 let mut end = s.len();
2650 while end > 0 && (s[end - 1] == b' ' || s[end - 1] == b'\t') {
2651 end -= 1;
2652 }
2653 &s[..end]
2654}
2655
2656fn ws_squash_eq(a: &[u8], b: &[u8], change_only: bool) -> bool {
2659 let is_ws = |c: u8| c == b' ' || c == b'\t';
2660 let (mut ia, mut ib) = (0usize, 0usize);
2661 while ia < a.len() && ib < b.len() {
2662 let (ca, cb) = (a[ia], b[ib]);
2663 if is_ws(ca) || is_ws(cb) {
2664 if change_only && (!is_ws(ca) || !is_ws(cb)) {
2665 return false;
2666 }
2667 if change_only {
2670 while ia < a.len() && is_ws(a[ia]) {
2671 ia += 1;
2672 }
2673 while ib < b.len() && is_ws(b[ib]) {
2674 ib += 1;
2675 }
2676 continue;
2677 } else {
2678 if is_ws(ca) {
2679 ia += 1;
2680 continue;
2681 }
2682 if is_ws(cb) {
2683 ib += 1;
2684 continue;
2685 }
2686 }
2687 }
2688 if ca != cb {
2689 return false;
2690 }
2691 ia += 1;
2692 ib += 1;
2693 }
2694 while ia < a.len() && is_ws(a[ia]) {
2696 ia += 1;
2697 }
2698 while ib < b.len() && is_ws(b[ib]) {
2699 ib += 1;
2700 }
2701 ia == a.len() && ib == b.len()
2702}
2703
2704fn reuse_combine_diff(sline: &mut [CdLine], cnt: usize, i: usize, j: usize) {
2708 let imask = 1u64 << i;
2709 let jmask = 1u64 << j;
2710 for line in sline.iter_mut().take(cnt + 1) {
2711 line.p_lno[i] = line.p_lno[j];
2712 for ll in &mut line.lost {
2713 if ll.parent_map & jmask != 0 {
2714 ll.parent_map |= imask;
2715 }
2716 }
2717 if line.flag & jmask != 0 {
2718 line.flag |= imask;
2719 }
2720 }
2721 sline[cnt + 1].p_lno[i] = sline[cnt + 1].p_lno[j];
2723}
2724
2725fn cd_interesting(sline: &CdLine, all_mask: u64) -> bool {
2728 (sline.flag & all_mask) != 0 || !sline.lost.is_empty()
2729}
2730
2731fn adjust_hunk_tail(sline: &[CdLine], all_mask: u64, hunk_begin: usize, mut i: usize) -> usize {
2733 if hunk_begin < i && (sline[i - 1].flag & all_mask) == 0 {
2734 i -= 1;
2735 }
2736 i
2737}
2738
2739fn find_next(
2741 sline: &[CdLine],
2742 mark: u64,
2743 mut i: usize,
2744 cnt: usize,
2745 look_for_uninteresting: bool,
2746) -> usize {
2747 while i <= cnt {
2748 let marked = (sline[i].flag & mark) != 0;
2749 if look_for_uninteresting {
2750 if !marked {
2751 return i;
2752 }
2753 } else if marked {
2754 return i;
2755 }
2756 i += 1;
2757 }
2758 i
2759}
2760
2761fn give_context(sline: &mut [CdLine], cnt: usize, num_parent: usize, context: usize) -> bool {
2764 let all_mask = (1u64 << num_parent) - 1;
2765 let mark = 1u64 << num_parent;
2766 let no_pre_delete = 2u64 << num_parent;
2767
2768 let mut i = find_next(sline, mark, 0, cnt, false);
2769 if cnt < i {
2770 return false;
2771 }
2772
2773 while i <= cnt {
2774 let mut j = i.saturating_sub(context);
2775 while j < i {
2777 if (sline[j].flag & mark) == 0 {
2778 sline[j].flag |= no_pre_delete;
2779 }
2780 sline[j].flag |= mark;
2781 j += 1;
2782 }
2783
2784 loop {
2785 j = find_next(sline, mark, i, cnt, true);
2787 if cnt < j {
2788 return true;
2790 }
2791 let k = find_next(sline, mark, j, cnt, false);
2793 let j2 = adjust_hunk_tail(sline, all_mask, i, j);
2794
2795 if k < j2 + context {
2796 let mut jj = j2;
2798 while jj < k {
2799 sline[jj].flag |= mark;
2800 jj += 1;
2801 }
2802 i = k;
2803 continue;
2804 }
2805
2806 i = k;
2808 let kk = if j2 + context < cnt + 1 {
2809 j2 + context
2810 } else {
2811 cnt + 1
2812 };
2813 let mut jj = j2;
2814 while jj < kk {
2815 sline[jj].flag |= mark;
2816 jj += 1;
2817 }
2818 break;
2819 }
2820 }
2821 true
2822}
2823
2824fn make_hunks(
2827 sline: &mut [CdLine],
2828 cnt: usize,
2829 num_parent: usize,
2830 dense: bool,
2831 context: usize,
2832) -> bool {
2833 let all_mask = (1u64 << num_parent) - 1;
2834 let mark = 1u64 << num_parent;
2835
2836 for line in sline.iter_mut().take(cnt + 1) {
2837 if cd_interesting(line, all_mask) {
2838 line.flag |= mark;
2839 } else {
2840 line.flag &= !mark;
2841 }
2842 }
2843 if !dense {
2844 return give_context(sline, cnt, num_parent, context);
2845 }
2846
2847 let mut i = 0;
2851 while i <= cnt {
2852 while i <= cnt && (sline[i].flag & mark) == 0 {
2853 i += 1;
2854 }
2855 if cnt < i {
2856 break;
2857 }
2858 let hunk_begin = i;
2859 let mut j = i + 1;
2860 while j <= cnt {
2861 if (sline[j].flag & mark) == 0 {
2862 let mut la = adjust_hunk_tail(sline, all_mask, hunk_begin, j);
2864 la = if la + context < cnt + 1 {
2865 la + context
2866 } else {
2867 cnt + 1
2868 };
2869 let mut contin = false;
2870 while la > 0 && j < la {
2871 la -= 1;
2872 if (sline[la].flag & mark) != 0 {
2873 contin = true;
2874 break;
2875 }
2876 }
2877 if !contin {
2878 break;
2879 }
2880 j = la;
2881 }
2882 j += 1;
2883 }
2884 let hunk_end = j;
2885
2886 let mut same_diff: u64 = 0;
2889 let mut has_interesting = false;
2890 let mut jj = i;
2891 while jj < hunk_end && !has_interesting {
2892 let this_diff = sline[jj].flag & all_mask;
2893 if this_diff != 0 {
2894 if same_diff == 0 {
2895 same_diff = this_diff;
2896 } else if same_diff != this_diff {
2897 has_interesting = true;
2898 break;
2899 }
2900 }
2901 for ll in &sline[jj].lost {
2902 if has_interesting {
2903 break;
2904 }
2905 let td = ll.parent_map;
2906 if same_diff == 0 {
2907 same_diff = td;
2908 } else if same_diff != td {
2909 has_interesting = true;
2910 }
2911 }
2912 jj += 1;
2913 }
2914
2915 if !has_interesting && same_diff != all_mask {
2916 for line in sline.iter_mut().take(hunk_end).skip(hunk_begin) {
2918 line.flag &= !mark;
2919 }
2920 }
2921 i = hunk_end;
2922 }
2923
2924 give_context(sline, cnt, num_parent, context)
2925}
2926
2927fn show_parent_lno(
2929 out: &mut Vec<u8>,
2930 sline: &[CdLine],
2931 l0: usize,
2932 l1: usize,
2933 n: usize,
2934 null_context: u64,
2935) {
2936 let a = sline[l0].p_lno[n];
2937 let b = sline[l1].p_lno[n];
2938 out.extend_from_slice(format!(" -{},{}", a, b - a - null_context).as_bytes());
2939}
2940
2941fn hunk_comment_line(bol: &[u8]) -> bool {
2944 if bol.is_empty() {
2945 return false;
2946 }
2947 let ch = bol[0];
2948 ch.is_ascii_alphabetic() || ch == b'_' || ch == b'$'
2949}
2950
2951fn show_line_to_eol(out: &mut Vec<u8>, line: &[u8]) {
2954 let saw_cr = line.last() == Some(&b'\r');
2955 if saw_cr {
2956 out.extend_from_slice(&line[..line.len() - 1]);
2957 out.push(b'\r');
2958 } else {
2959 out.extend_from_slice(line);
2960 }
2961 out.push(b'\n');
2962}
2963
2964fn dump_sline(out: &mut Vec<u8>, sline: &[CdLine], cnt: usize, num_parent: usize, context: usize) {
2966 let mark = 1u64 << num_parent;
2967 let no_pre_delete = 2u64 << num_parent;
2968 let mut lno: usize = 0;
2969
2970 loop {
2971 let mut hunk_comment: Option<&[u8]> = None;
2972 while lno <= cnt && (sline[lno].flag & mark) == 0 {
2973 if hunk_comment_line(&sline[lno].bol) {
2974 hunk_comment = Some(&sline[lno].bol);
2975 }
2976 lno += 1;
2977 }
2978 if cnt < lno {
2979 break;
2980 }
2981 let mut hunk_end = lno + 1;
2982 while hunk_end <= cnt {
2983 if (sline[hunk_end].flag & mark) == 0 {
2984 break;
2985 }
2986 hunk_end += 1;
2987 }
2988
2989 let mut rlines = (hunk_end - lno) as u64;
2990 if cnt < hunk_end {
2991 rlines -= 1; }
2993
2994 let mut null_context: u64 = 0;
2995 if context == 0 {
2996 for sl in sline.iter().take(hunk_end).skip(lno) {
2999 if (sl.flag & (mark - 1)) == 0 {
3000 null_context += 1;
3001 }
3002 }
3003 rlines -= null_context;
3004 }
3005
3006 for _ in 0..=num_parent {
3009 out.push(b'@');
3010 }
3011 for i in 0..num_parent {
3012 show_parent_lno(out, sline, lno, hunk_end, i, null_context);
3013 }
3014 out.extend_from_slice(format!(" +{},{} ", lno + 1, rlines).as_bytes());
3015 for _ in 0..=num_parent {
3016 out.push(b'@');
3017 }
3018
3019 if let Some(comment) = hunk_comment {
3020 let mut comment_end = 0;
3021 for (idx, &ch) in comment.iter().take(40).enumerate() {
3022 if ch == b'\n' {
3023 break;
3024 }
3025 if !ch.is_ascii_whitespace() {
3026 comment_end = idx + 1;
3027 }
3028 }
3029 if comment_end != 0 {
3030 out.push(b' ');
3031 out.extend_from_slice(&comment[..comment_end]);
3032 }
3033 }
3034 out.push(b'\n');
3035
3036 while lno < hunk_end {
3038 let sl = &sline[lno];
3039 lno += 1;
3040 if (sl.flag & no_pre_delete) == 0 {
3042 for ll in &sl.lost {
3043 for j in 0..num_parent {
3044 if ll.parent_map & (1u64 << j) != 0 {
3045 out.push(b'-');
3046 } else {
3047 out.push(b' ');
3048 }
3049 }
3050 show_line_to_eol(out, &ll.line);
3051 }
3052 }
3053 if cnt < lno {
3054 break;
3055 }
3056 if (sl.flag & (mark - 1)) == 0 {
3057 if context == 0 {
3059 continue;
3060 }
3061 }
3062 let mut p_mask = 1u64;
3063 for _ in 0..num_parent {
3064 if p_mask & sl.flag != 0 {
3065 out.push(b'+');
3066 } else {
3067 out.push(b' ');
3068 }
3069 p_mask <<= 1;
3070 }
3071 show_line_to_eol(out, &sl.bol);
3072 }
3073 }
3074}
3075
3076#[cfg(test)]
3077mod tests {
3078 use super::*;
3079
3080 fn render_plain(old: Option<&[u8]>, new: Option<&[u8]>) -> Vec<u8> {
3081 let mut out = Vec::new();
3082 let mut options = HunkRenderOptions::default();
3083 render_hunks(&mut out, old, new, &mut options);
3084 out
3085 }
3086
3087 #[test]
3088 fn identical_content_renders_nothing() {
3089 assert!(render_plain(Some(b"a\nb\n"), Some(b"a\nb\n")).is_empty());
3090 }
3091
3092 #[test]
3093 fn single_line_change_basic_hunk() {
3094 let out = render_plain(Some(b"alpha\nbeta\ngamma\n"), Some(b"alpha\nBETA\ngamma\n"));
3095 assert_eq!(
3096 out,
3097 b"@@ -1,3 +1,3 @@\n alpha\n-beta\n+BETA\n gamma\n".to_vec(),
3098 );
3099 }
3100
3101 #[test]
3102 fn count_omitted_when_one() {
3103 let out = render_plain(Some(b"old\n"), Some(b"new\n"));
3105 assert_eq!(out, b"@@ -1 +1 @@\n-old\n+new\n".to_vec());
3106 }
3107
3108 #[test]
3109 fn no_newline_marker_on_old_side() {
3110 let out = render_plain(Some(b"only line no newline"), None);
3111 assert_eq!(
3112 out,
3113 b"@@ -1 +0,0 @@\n-only line no newline\n\\ No newline at end of file\n".to_vec(),
3114 );
3115 }
3116
3117 #[test]
3118 fn no_newline_marker_on_new_side() {
3119 let out = render_plain(Some(b"beta\n"), Some(b"beta-notail"));
3120 assert_eq!(
3121 out,
3122 b"@@ -1 +1 @@\n-beta\n+beta-notail\n\\ No newline at end of file\n".to_vec(),
3123 );
3124 }
3125
3126 #[test]
3127 fn pure_insertion_into_empty() {
3128 let out = render_plain(None, Some(b"x\ny\n"));
3129 assert_eq!(out, b"@@ -0,0 +1,2 @@\n+x\n+y\n".to_vec());
3130 }
3131
3132 #[test]
3133 fn distant_changes_split_into_two_hunks() {
3134 let old: &[u8] = b"a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n";
3135 let new: &[u8] = b"A\nb\nc\nd\ne\nf\ng\nh\ni\nJ\n";
3136 let out = render_plain(Some(old), Some(new));
3137 let text = String::from_utf8(out).expect("rendered output is valid UTF-8");
3139 assert_eq!(text.matches("@@ ").count(), 2, "expected two hunks: {text}");
3140 }
3141
3142 #[test]
3143 fn heading_callback_supplies_section() {
3144 let old: &[u8] = b"fn foo() {\n a\n b\n c\n d\n e\n f\n g\n}\n";
3148 let new: &[u8] = b"fn foo() {\n a\n b\n c\n d\n CHANGED\n f\n g\n}\n";
3149 let mut out = Vec::new();
3150 let mut heading_fn = |line: &[u8]| -> Option<Vec<u8>> {
3153 if line.first().is_some_and(u8::is_ascii_alphabetic) {
3154 Some(line.strip_suffix(b"\n").unwrap_or(line).to_vec())
3155 } else {
3156 None
3157 }
3158 };
3159 let mut options = HunkRenderOptions {
3160 heading: Some(&mut heading_fn),
3161 ..Default::default()
3162 };
3163 render_hunks(&mut out, Some(old), Some(new), &mut options);
3164 let text = String::from_utf8(out).expect("rendered output is valid UTF-8");
3165 assert!(
3166 text.starts_with("@@ -3,7 +3,7 @@ fn foo() {\n"),
3167 "expected funcname heading: {text}",
3168 );
3169 }
3170
3171 fn render_cc(result: &[u8], parents: &[&[u8]], dense: bool) -> String {
3172 let mut out = Vec::new();
3173 let opts = CombinedRenderOptions {
3174 dense,
3175 ..Default::default()
3176 };
3177 render_combined_with(&mut out, result, parents, &opts);
3178 String::from_utf8(out).expect("combined output is valid UTF-8")
3179 }
3180
3181 #[test]
3182 fn combined_two_parent_dense_header_and_columns() {
3183 let p0 = b"A\nB\nC\nD\nE\nF\n";
3188 let p1 = b"A\nB\n1\n2\n";
3189 let result = b"A\nB\nC\nD\nE\nF\n1\n2\n";
3190 let text = render_cc(result, &[p0, p1], true);
3191 assert_eq!(
3192 text, "@@@ -1,6 -1,4 +1,8 @@@\n A\n B\n +C\n +D\n +E\n +F\n+ 1\n+ 2\n",
3193 "combined dense output:\n{text}",
3194 );
3195 }
3196
3197 #[test]
3198 fn combined_identical_to_one_parent_dense_drops_hunk() {
3199 let p0 = b"x\ny\n";
3204 let p1 = b"x\nCHANGED\n";
3205 let result = b"x\ny\n"; assert_eq!(render_cc(result, &[p0, p1], true), "");
3207 assert!(render_cc(result, &[p0, p1], false).starts_with("@@@"));
3209 }
3210
3211 #[test]
3212 fn combined_reuse_identical_parents() {
3213 let parent = b"a\nb\n";
3217 let result = b"a\nb\nc\n";
3218 let text = render_cc(result, &[parent, parent], true);
3219 assert_eq!(
3220 text, "@@@ -1,2 -1,2 +1,3 @@@\n a\n b\n++c\n",
3221 "reuse output:\n{text}",
3222 );
3223 }
3224}