1use colored::*;
2use prettydiff::{basic::DiffOp, basic::DiffOp::*, diff_lines, diff_words};
3
4/// How many lines of context are displayed around the actual diffs
5const CONTEXT: usize = 2;
6
7fn skip(skipped_lines: &[&str]) {
8 // When the amount of skipped lines is exactly `CONTEXT * 2`, we already
9 // print all the context and don't actually skip anything.
10 match skipped_lines.len().checked_sub(CONTEXT * 2) {
11 Some(skipped: usize @ 2..) => {
12 // Print an initial `CONTEXT` amount of lines.
13 for line: &&str in &skipped_lines[..CONTEXT] {
14 println!(" {line}");
15 }
16 println!("... {skipped} lines skipped ...");
17 // Print `... n lines skipped ...` followed by the last `CONTEXT` lines.
18 for line: &&str in &skipped_lines[skipped + CONTEXT..] {
19 println!(" {line}");
20 }
21 }
22 _ => {
23 // Print all the skipped lines if the amount of context desired is less than the amount of lines
24 for line: &&str in skipped_lines {
25 println!(" {line}");
26 }
27 }
28 }
29}
30
31fn row(row: DiffOp<'_, &str>) {
32 match row {
33 Remove(l: &[&str]) => {
34 for l: &&str in l {
35 println!("{}{}", "-".red(), l.red());
36 }
37 }
38 Equal(l: &[&str]) => {
39 skip(skipped_lines:l);
40 }
41 Replace(l: &[&str], r: &[&str]) => {
42 for (l: &&str, r: &&str) in l.iter().zip(r) {
43 print_line_diff(l, r);
44 }
45 }
46 Insert(r: &[&str]) => {
47 for r: &&str in r {
48 println!("{}{}", "+".green(), r.green());
49 }
50 }
51 }
52}
53
54fn print_line_diff(l: &str, r: &str) {
55 let diff = diff_words(l, r);
56 let diff = diff.diff();
57 if has_both_insertions_and_deletions(&diff)
58 || !colored::control::SHOULD_COLORIZE.should_colorize()
59 {
60 // The line both adds and removes chars, print both lines, but highlight their differences instead of
61 // drawing the entire line in red/green.
62 print!("{}", "-".red());
63 for char in &diff {
64 match *char {
65 Replace(l, _) | Remove(l) => {
66 for l in l {
67 print!("{}", l.to_string().on_red())
68 }
69 }
70 Insert(_) => {}
71 Equal(l) => {
72 for l in l {
73 print!("{l}")
74 }
75 }
76 }
77 }
78 println!();
79 print!("{}", "+".green());
80 for char in diff {
81 match char {
82 Remove(_) => {}
83 Replace(_, r) | Insert(r) => {
84 for r in r {
85 print!("{}", r.to_string().on_green())
86 }
87 }
88 Equal(r) => {
89 for r in r {
90 print!("{r}")
91 }
92 }
93 }
94 }
95 println!();
96 } else {
97 // The line only adds or only removes chars, print a single line highlighting their differences.
98 print!("{}", "~".yellow());
99 for char in diff {
100 match char {
101 Remove(l) => {
102 for l in l {
103 print!("{}", l.to_string().on_red())
104 }
105 }
106 Equal(w) => {
107 for w in w {
108 print!("{w}")
109 }
110 }
111 Insert(r) => {
112 for r in r {
113 print!("{}", r.to_string().on_green())
114 }
115 }
116 Replace(l, r) => {
117 for l in l {
118 print!("{}", l.to_string().on_red())
119 }
120 for r in r {
121 print!("{}", r.to_string().on_green())
122 }
123 }
124 }
125 }
126 println!();
127 }
128}
129
130fn has_both_insertions_and_deletions(diff: &[DiffOp<'_, &str>]) -> bool {
131 let mut seen_l: bool = false;
132 let mut seen_r: bool = false;
133 for char: &DiffOp<'_, &str> in diff {
134 let is_whitespace: impl Fn(&[&str]) -> bool = |s: &[&str]| s.iter().any(|s: &&str| s.chars().any(|s: char| s.is_whitespace()));
135 match char {
136 Insert(l: &&[&str]) if !is_whitespace(l) => seen_l = true,
137 Remove(r: &&[&str]) if !is_whitespace(r) => seen_r = true,
138 Replace(l: &&[&str], r: &&[&str]) if !is_whitespace(l) && !is_whitespace(r) => return true,
139 _ => {}
140 }
141 }
142 seen_l && seen_r
143}
144
145pub fn print_diff(expected: &[u8], actual: &[u8]) {
146 let expected_str: Cow<'_, str> = String::from_utf8_lossy(expected);
147 let actual_str: Cow<'_, str> = String::from_utf8_lossy(actual);
148
149 if expected_str.as_bytes() != expected || actual_str.as_bytes() != actual {
150 println!(
151 "{}",
152 "Non-UTF8 characters in output, diff may be imprecise.".red()
153 );
154 }
155
156 let pat: impl Fn(char) -> bool = |c: char| c.is_whitespace() && c != ' ' && c != '\n' && c != '\r';
157 let expected_str: String = expected_str.replace(from:pat, to:"░");
158 let actual_str: String = actual_str.replace(from:pat, to:"░");
159
160 for r: DiffOp<'_, &str> in diff_lines(&expected_str, &actual_str).diff() {
161 row(r);
162 }
163 println!()
164}
165