1 | use colored::*; |
2 | use prettydiff::{basic::DiffOp, basic::DiffOp::*, diff_lines, diff_words}; |
3 | |
4 | /// How many lines of context are displayed around the actual diffs |
5 | const CONTEXT: usize = 2; |
6 | |
7 | fn 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 | |
31 | fn 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 | |
54 | fn 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 | |
130 | fn 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 | |
145 | pub 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 | |