blob: b0346c250b3426643304813bcedeb93c6219f1a2 [file] [log] [blame]
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] {
println!(" {line}");
}
println!("... {skipped} lines skipped ...");
// Print `... n lines skipped ...` followed by the last `CONTEXT` lines.
for line in &skipped_lines[skipped + CONTEXT..] {
println!(" {line}");
}
}
_ => {
// Print all the skipped lines if the amount of context desired is less than the amount of lines
for line in skipped_lines {
println!(" {line}");
}
}
}
}
fn row(row: DiffOp<'_, &str>) {
match row {
Remove(l) => {
for l in l {
println!("{}{}", "-".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 {
println!("{}{}", "+".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)
|| !colored::control::SHOULD_COLORIZE.should_colorize()
{
// The line both adds and removes chars, print both lines, but highlight their differences instead of
// drawing the entire line in red/green.
print!("{}", "-".red());
for char in &diff {
match *char {
Replace(l, _) | Remove(l) => {
for l in l {
print!("{}", l.to_string().on_red())
}
}
Insert(_) => {}
Equal(l) => {
for l in l {
print!("{l}")
}
}
}
}
println!();
print!("{}", "+".green());
for char in diff {
match char {
Remove(_) => {}
Replace(_, r) | Insert(r) => {
for r in r {
print!("{}", r.to_string().on_green())
}
}
Equal(r) => {
for r in r {
print!("{r}")
}
}
}
}
println!();
} else {
// The line only adds or only removes chars, print a single line highlighting their differences.
print!("{}", "~".yellow());
for char in diff {
match char {
Remove(l) => {
for l in l {
print!("{}", l.to_string().on_red())
}
}
Equal(w) => {
for w in w {
print!("{w}")
}
}
Insert(r) => {
for r in r {
print!("{}", r.to_string().on_green())
}
}
Replace(l, r) => {
for l in l {
print!("{}", l.to_string().on_red())
}
for r in r {
print!("{}", r.to_string().on_green())
}
}
}
}
println!();
}
}
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 {
println!(
"{}",
"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);
}
println!()
}