use std::fmt::Debug;
use std::io::{self, Write};
use similar::{ChangeTag, TextDiff, udiff::UnifiedDiff};
const CONTEXT_LINES: usize = 3;
pub trait Diff: Debug {
fn diff(&self, new: &Self, out: &mut dyn Write) -> io::Result<()> {
text_diff(&format!("{self:#?}"), &format!("{new:#?}"), out)
}
}
pub fn text_diff(old: &str, new: &str, out: &mut dyn Write) -> io::Result<()> {
let style = Style::detect();
let textdiff = TextDiff::from_lines(old, new);
let mut udiff = UnifiedDiff::from_text_diff(&textdiff);
udiff.context_radius(CONTEXT_LINES);
for hunk in udiff.iter_hunks() {
write_styled_line(out, style.header, &hunk.header().to_string(), style.reset)?;
for change in hunk.iter_changes() {
let (sign, color) = match change.tag() {
ChangeTag::Delete => ('-', style.delete),
ChangeTag::Insert => ('+', style.insert),
ChangeTag::Equal => (' ', ""),
};
let value = change.value();
let (body, nl) = match value.strip_suffix('\n') {
Some(body) => (body, "\n"),
None => (value, ""),
};
if color.is_empty() {
write!(out, "{sign}{body}{nl}")?;
} else {
write!(out, "{color}{sign}{body}{}{nl}", style.reset)?;
}
}
}
Ok(())
}
fn write_styled_line(out: &mut dyn Write, color: &str, body: &str, reset: &str) -> io::Result<()> {
let body = body.strip_suffix('\n').unwrap_or(body);
if color.is_empty() {
writeln!(out, "{body}")
} else {
writeln!(out, "{color}{body}{reset}")
}
}
struct Style {
delete: &'static str,
insert: &'static str,
header: &'static str,
reset: &'static str,
}
impl Style {
fn detect() -> Self {
if std::env::var_os("NO_COLOR").is_some_and(|v| !v.is_empty()) {
Self {
delete: "",
insert: "",
header: "",
reset: "",
}
} else {
Self {
delete: "\x1b[31m", insert: "\x1b[32m", header: "\x1b[36;1m", reset: "\x1b[0m",
}
}
}
}