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