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) => {
34 for l in l {
35 println!("{}{}", "-".red(), l.red());
36 }
37 }
38 Equal(l) => {
39 skip(l);
40 }
41 Replace(l, r) => {
42 if l.len() == r.len() {
43 for (l, r) in l.iter().zip(r) {
44 print_line_diff(l, r);
45 }
46 } else {
47 for l in l {
48 println!("{}{}", "-".red(), l.red());
49 }
50 for r in r {
51 println!("{}{}", "+".green(), r.green());
52 }
53 }
54 }
55 Insert(r) => {
56 for r in r {
57 println!("{}{}", "+".green(), r.green());
58 }
59 }
60 }
61}
62
63fn print_line_diff(l: &str, r: &str) {
64 let diff = diff_words(l, r);
65 let diff = diff.diff();
66 if has_both_insertions_and_deletions(&diff)
67 || !colored::control::SHOULD_COLORIZE.should_colorize()
68 {
69 // The line both adds and removes chars, print both lines, but highlight their differences instead of
70 // drawing the entire line in red/green.
71 print!("{}", "-".red());
72 for char in &diff {
73 match *char {
74 Replace(l, _) | Remove(l) => {
75 for l in l {
76 print!("{}", l.to_string().on_red())
77 }
78 }
79 Insert(_) => {}
80 Equal(l) => {
81 for l in l {
82 print!("{l}")
83 }
84 }
85 }
86 }
87 println!();
88 print!("{}", "+".green());
89 for char in diff {
90 match char {
91 Remove(_) => {}
92 Replace(_, r) | Insert(r) => {
93 for r in r {
94 print!("{}", r.to_string().on_green())
95 }
96 }
97 Equal(r) => {
98 for r in r {
99 print!("{r}")
100 }
101 }
102 }
103 }
104 println!();
105 } else {
106 // The line only adds or only removes chars, print a single line highlighting their differences.
107 print!("{}", "~".yellow());
108 for char in diff {
109 match char {
110 Remove(l) => {
111 for l in l {
112 print!("{}", l.to_string().on_red())
113 }
114 }
115 Equal(w) => {
116 for w in w {
117 print!("{w}")
118 }
119 }
120 Insert(r) => {
121 for r in r {
122 print!("{}", r.to_string().on_green())
123 }
124 }
125 Replace(l, r) => {
126 for l in l {
127 print!("{}", l.to_string().on_red())
128 }
129 for r in r {
130 print!("{}", r.to_string().on_green())
131 }
132 }
133 }
134 }
135 println!();
136 }
137}
138
139fn has_both_insertions_and_deletions(diff: &[DiffOp<'_, &str>]) -> bool {
140 let mut seen_l: bool = false;
141 let mut seen_r: bool = false;
142 for char: &DiffOp<'_, &str> in diff {
143 let is_whitespace: impl Fn(&[&str]) -> bool = |s: &[&str]| s.iter().any(|s: &&str| s.chars().any(|s: char| s.is_whitespace()));
144 match char {
145 Insert(l: &&[&str]) if !is_whitespace(l) => seen_l = true,
146 Remove(r: &&[&str]) if !is_whitespace(r) => seen_r = true,
147 Replace(l: &&[&str], r: &&[&str]) if !is_whitespace(l) && !is_whitespace(r) => return true,
148 _ => {}
149 }
150 }
151 seen_l && seen_r
152}
153
154pub(crate) fn print_diff(expected: &[u8], actual: &[u8]) {
155 let expected_str: Cow<'_, str> = String::from_utf8_lossy(expected);
156 let actual_str: Cow<'_, str> = String::from_utf8_lossy(actual);
157
158 if expected_str.as_bytes() != expected || actual_str.as_bytes() != actual {
159 println!(
160 "{}",
161 "Non-UTF8 characters in output, diff may be imprecise.".red()
162 );
163 }
164
165 let pat: impl Fn(char) -> bool = |c: char| c.is_whitespace() && c != ' ' && c != '\n' && c != '\r';
166 let expected_str: String = expected_str.replace(from:pat, to:"░");
167 let actual_str: String = actual_str.replace(from:pat, to:"░");
168
169 for r: DiffOp<'_, &str> in diff_lines(&expected_str, &actual_str).diff() {
170 row(r);
171 }
172 println!()
173}
174

Provided by KDAB

Privacy Policy
Learn Rust with the experts
Find out more