use std::fmt::{Display, Write};
use confusables::Confusable;
use facet_pretty::{PrettyPrinter, tokyo_night};
use facet_reflect::Peek;
use owo_colors::OwoColorize;
use crate::{Diff, ReplaceGroup, Updates, UpdatesGroup, Value};
fn deleted(s: &str) -> String {
format!("{}", s.color(tokyo_night::DELETION))
}
fn inserted(s: &str) -> String {
format!("{}", s.color(tokyo_night::INSERTION))
}
fn muted(s: &str) -> String {
format!("{}", s.color(tokyo_night::MUTED))
}
fn field(s: &str) -> String {
format!("{}", s.color(tokyo_night::FIELD_NAME))
}
fn punct(s: &str) -> String {
format!("{}", s.color(tokyo_night::COMMENT))
}
struct PadAdapter<'a, 'b: 'a> {
fmt: &'a mut std::fmt::Formatter<'b>,
on_newline: bool,
indent: &'static str,
}
impl<'a, 'b> PadAdapter<'a, 'b> {
const fn new_indented(fmt: &'a mut std::fmt::Formatter<'b>) -> Self {
Self {
fmt,
on_newline: true,
indent: " ",
}
}
}
impl<'a, 'b> Write for PadAdapter<'a, 'b> {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
for line in s.split_inclusive('\n') {
if self.on_newline {
self.fmt.write_str(self.indent)?;
}
self.on_newline = line.ends_with('\n');
self.fmt.write_str(line)?;
}
Ok(())
}
fn write_char(&mut self, c: char) -> std::fmt::Result {
if self.on_newline {
self.fmt.write_str(self.indent)?;
}
self.on_newline = c == '\n';
self.fmt.write_char(c)
}
}
fn peek_eq<'mem, 'facet>(a: Peek<'mem, 'facet>, b: Peek<'mem, 'facet>) -> bool {
a.shape().id == b.shape().id && a.shape().is_partial_eq() && a == b
}
impl<'mem, 'facet> Display for Diff<'mem, 'facet> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Diff::Equal { value: _ } => {
write!(f, "{}", muted("(structurally equal)"))
}
Diff::Replace { from, to } => {
let printer = PrettyPrinter::default()
.with_colors(facet_pretty::ColorMode::Never)
.with_minimal_option_names(true);
if let (Some(from_str), Some(to_str)) = (from.as_str(), to.as_str())
&& (from_str.is_confusable_with(to_str) || to_str.is_confusable_with(from_str))
{
write!(
f,
"{} → {}\n{}",
deleted(&printer.format_peek(*from)),
inserted(&printer.format_peek(*to)),
explain_confusable_differences(from_str, to_str)
)?;
return Ok(());
}
write!(
f,
"{} → {}",
deleted(&printer.format_peek(*from)),
inserted(&printer.format_peek(*to))
)
}
Diff::User {
from: _,
to: _,
variant,
value,
} => {
let printer = PrettyPrinter::default()
.with_colors(facet_pretty::ColorMode::Never)
.with_minimal_option_names(true);
if let Some(variant) = variant {
write!(f, "{}", variant.bold())?;
}
let has_prefix = variant.is_some();
match value {
Value::Struct {
updates,
deletions,
insertions,
unchanged,
} => {
if updates.is_empty() && deletions.is_empty() && insertions.is_empty() {
return write!(f, "{}", muted("(structurally equal)"));
}
if has_prefix {
writeln!(f, " {}", punct("{"))?;
} else {
writeln!(f, "{}", punct("{"))?;
}
let mut indent = PadAdapter::new_indented(f);
let unchanged_count = unchanged.len();
if unchanged_count > 0 {
let label = if unchanged_count == 1 {
"field"
} else {
"fields"
};
writeln!(
indent,
"{}",
muted(&format!(".. {unchanged_count} unchanged {label}"))
)?;
}
let mut updates: Vec<_> = updates.iter().collect();
updates.sort_by(|(a, _), (b, _)| a.cmp(b));
for (fld, update) in updates {
writeln!(indent, "{}{} {update}", field(fld), punct(":"))?;
}
let mut deletions: Vec<_> = deletions.iter().collect();
deletions.sort_by(|(a, _), (b, _)| a.cmp(b));
for (fld, value) in deletions {
writeln!(
indent,
"{} {}{} {}",
deleted("-"),
field(fld),
punct(":"),
deleted(&printer.format_peek(*value))
)?;
}
let mut insertions: Vec<_> = insertions.iter().collect();
insertions.sort_by(|(a, _), (b, _)| a.cmp(b));
for (fld, value) in insertions {
writeln!(
indent,
"{} {}{} {}",
inserted("+"),
field(fld),
punct(":"),
inserted(&printer.format_peek(*value))
)?;
}
write!(f, "{}", punct("}"))
}
Value::Tuple { updates } => {
if updates.is_empty() {
return write!(f, "{}", muted("(structurally equal)"));
}
if updates.is_single_replace() {
if has_prefix {
f.write_str(" ")?;
}
write!(f, "{updates}")
} else {
f.write_str(if has_prefix { " (\n" } else { "(\n" })?;
let mut indent = PadAdapter::new_indented(f);
write!(indent, "{updates}")?;
f.write_str(")")
}
}
}
}
Diff::Sequence {
from: _,
to: _,
updates,
} => {
if updates.is_empty() {
write!(f, "{}", muted("(structurally equal)"))
} else {
writeln!(f, "{}", punct("["))?;
let mut indent = PadAdapter::new_indented(f);
write!(indent, "{updates}")?;
write!(f, "{}", punct("]"))
}
}
}
}
}
impl<'mem, 'facet> Updates<'mem, 'facet> {
pub const fn is_single_replace(&self) -> bool {
self.0.first.is_some() && self.0.values.is_empty() && self.0.last.is_none()
}
pub const fn is_empty(&self) -> bool {
self.0.first.is_none() && self.0.values.is_empty()
}
}
impl<'mem, 'facet> Display for Updates<'mem, 'facet> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(update) = &self.0.first {
update.fmt(f)?;
}
for (values, update) in &self.0.values {
let count = values.len();
if count > 0 {
let label = if count == 1 { "item" } else { "items" };
writeln!(f, "{}", muted(&format!(".. {count} unchanged {label}")))?;
}
update.fmt(f)?;
}
if let Some(values) = &self.0.last {
let count = values.len();
if count > 0 {
let label = if count == 1 { "item" } else { "items" };
writeln!(f, "{}", muted(&format!(".. {count} unchanged {label}")))?;
}
}
Ok(())
}
}
impl<'mem, 'facet> Display for ReplaceGroup<'mem, 'facet> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let printer = PrettyPrinter::default()
.with_colors(facet_pretty::ColorMode::Never)
.with_minimal_option_names(true);
if self.removals.len() == 1 && self.additions.len() == 1 {
let from = self.removals[0];
let to = self.additions[0];
if peek_eq(from, to) {
return writeln!(f, "{}", muted(&printer.format_peek(from)));
}
return writeln!(
f,
"{} → {}",
deleted(&printer.format_peek(from)),
inserted(&printer.format_peek(to))
);
}
for remove in &self.removals {
writeln!(
f,
"{}",
deleted(&format!("- {}", printer.format_peek(*remove)))
)?;
}
for add in &self.additions {
writeln!(
f,
"{}",
inserted(&format!("+ {}", printer.format_peek(*add)))
)?;
}
Ok(())
}
}
fn write_diff_sequence(
f: &mut std::fmt::Formatter<'_>,
diffs: &[Diff<'_, '_>],
) -> std::fmt::Result {
let mut i = 0;
while i < diffs.len() {
let mut equal_count = 0;
while i + equal_count < diffs.len() {
if matches!(diffs[i + equal_count], Diff::Equal { .. }) {
equal_count += 1;
} else {
break;
}
}
if equal_count > 0 {
let label = if equal_count == 1 { "item" } else { "items" };
writeln!(
f,
"{}",
muted(&format!(".. {equal_count} unchanged {label}"))
)?;
i += equal_count;
} else {
writeln!(f, "{}", diffs[i])?;
i += 1;
}
}
Ok(())
}
impl<'mem, 'facet> Display for UpdatesGroup<'mem, 'facet> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(update) = &self.0.first {
update.fmt(f)?;
}
for (values, update) in &self.0.values {
write_diff_sequence(f, values)?;
update.fmt(f)?;
}
if let Some(values) = &self.0.last {
write_diff_sequence(f, values)?;
}
Ok(())
}
}
fn format_char_with_codepoint(c: char) -> String {
if c.is_ascii_graphic() {
format!("'{}' (U+{:04X})", c, c as u32)
} else {
format!("'\\u{{{:04X}}}'", c as u32)
}
}
fn explain_confusable_differences(left: &str, right: &str) -> String {
use std::fmt::Write;
let left_chars: Vec<char> = left.chars().collect();
let right_chars: Vec<char> = right.chars().collect();
let mut out = String::new();
let mut diffs: Vec<(usize, char, char)> = Vec::new();
let max_len = left_chars.len().max(right_chars.len());
for i in 0..max_len {
let lc = left_chars.get(i);
let rc = right_chars.get(i);
match (lc, rc) {
(Some(&l), Some(&r)) if l != r => {
diffs.push((i, l, r));
}
(Some(&l), None) => {
diffs.push((i, l, '\0'));
}
(None, Some(&r)) => {
diffs.push((i, '\0', r));
}
_ => {}
}
}
if diffs.is_empty() {
return muted("(strings are identical)");
}
writeln!(
out,
"{}",
muted(&format!(
"(strings are visually confusable but differ in {} position{}):",
diffs.len(),
if diffs.len() == 1 { "" } else { "s" }
))
)
.unwrap();
for (pos, lc, rc) in &diffs {
if *lc == '\0' {
writeln!(
out,
" [{}]: (missing) vs {}",
pos,
inserted(&format_char_with_codepoint(*rc))
)
.unwrap();
} else if *rc == '\0' {
writeln!(
out,
" [{}]: {} vs (missing)",
pos,
deleted(&format_char_with_codepoint(*lc))
)
.unwrap();
} else {
writeln!(
out,
" [{}]: {} vs {}",
pos,
deleted(&format_char_with_codepoint(*lc)),
inserted(&format_char_with_codepoint(*rc))
)
.unwrap();
}
}
out.trim_end().to_string()
}