1use std::fmt::{Display, Write};
4
5use confusables::Confusable;
6use facet_pretty::{PrettyPrinter, tokyo_night};
7use facet_reflect::Peek;
8use owo_colors::OwoColorize;
9
10use crate::{Diff, ReplaceGroup, Updates, UpdatesGroup, Value};
11
12fn deleted(s: &str) -> String {
14 format!("{}", s.color(tokyo_night::DELETION))
15}
16
17fn inserted(s: &str) -> String {
19 format!("{}", s.color(tokyo_night::INSERTION))
20}
21
22fn muted(s: &str) -> String {
24 format!("{}", s.color(tokyo_night::MUTED))
25}
26
27fn field(s: &str) -> String {
29 format!("{}", s.color(tokyo_night::FIELD_NAME))
30}
31
32fn punct(s: &str) -> String {
34 format!("{}", s.color(tokyo_night::COMMENT))
35}
36
37struct PadAdapter<'a, 'b: 'a> {
38 fmt: &'a mut std::fmt::Formatter<'b>,
39 on_newline: bool,
40 indent: &'static str,
41}
42
43impl<'a, 'b> PadAdapter<'a, 'b> {
44 fn new_indented(fmt: &'a mut std::fmt::Formatter<'b>) -> Self {
45 Self {
46 fmt,
47 on_newline: true,
48 indent: " ",
49 }
50 }
51}
52
53impl<'a, 'b> Write for PadAdapter<'a, 'b> {
54 fn write_str(&mut self, s: &str) -> std::fmt::Result {
55 for line in s.split_inclusive('\n') {
56 if self.on_newline {
57 self.fmt.write_str(self.indent)?;
58 }
59
60 self.on_newline = line.ends_with('\n');
61
62 self.fmt.write_str(line)?;
63 }
64
65 Ok(())
66 }
67
68 fn write_char(&mut self, c: char) -> std::fmt::Result {
69 if self.on_newline {
70 self.fmt.write_str(self.indent)?;
71 }
72
73 self.on_newline = c == '\n';
74 self.fmt.write_char(c)
75 }
76}
77
78fn peek_eq<'mem, 'facet>(a: Peek<'mem, 'facet>, b: Peek<'mem, 'facet>) -> bool {
81 a.shape().id == b.shape().id && a.shape().is_partial_eq() && a == b
82}
83
84impl<'mem, 'facet> Display for Diff<'mem, 'facet> {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 match self {
87 Diff::Equal { value: _ } => {
88 write!(f, "{}", muted("(structurally equal)"))
89 }
90 Diff::Replace { from, to } => {
91 let printer = PrettyPrinter::default()
92 .with_colors(false)
93 .with_minimal_option_names(true);
94
95 if let (Some(from_str), Some(to_str)) = (from.as_str(), to.as_str())
98 && (from_str.is_confusable_with(to_str) || to_str.is_confusable_with(from_str))
99 {
100 write!(
103 f,
104 "{} → {}\n{}",
105 deleted(&printer.format_peek(*from)),
106 inserted(&printer.format_peek(*to)),
107 explain_confusable_differences(from_str, to_str)
108 )?;
109 return Ok(());
110 }
111
112 write!(
114 f,
115 "{} → {}",
116 deleted(&printer.format_peek(*from)),
117 inserted(&printer.format_peek(*to))
118 )
119 }
120 Diff::User {
121 from: _,
122 to: _,
123 variant,
124 value,
125 } => {
126 let printer = PrettyPrinter::default()
127 .with_colors(false)
128 .with_minimal_option_names(true);
129
130 if let Some(variant) = variant {
132 write!(f, "{}", variant.bold())?;
133 }
134
135 let has_prefix = variant.is_some();
136
137 match value {
138 Value::Struct {
139 updates,
140 deletions,
141 insertions,
142 unchanged,
143 } => {
144 if updates.is_empty() && deletions.is_empty() && insertions.is_empty() {
145 return write!(f, "{}", muted("(structurally equal)"));
146 }
147
148 if has_prefix {
149 writeln!(f, " {}", punct("{"))?;
150 } else {
151 writeln!(f, "{}", punct("{"))?;
152 }
153 let mut indent = PadAdapter::new_indented(f);
154
155 let unchanged_count = unchanged.len();
157 if unchanged_count > 0 {
158 let label = if unchanged_count == 1 {
159 "field"
160 } else {
161 "fields"
162 };
163 writeln!(
164 indent,
165 "{}",
166 muted(&format!(".. {unchanged_count} unchanged {label}"))
167 )?;
168 }
169
170 let mut updates: Vec<_> = updates.iter().collect();
172 updates.sort_by(|(a, _), (b, _)| a.cmp(b));
173 for (fld, update) in updates {
174 writeln!(indent, "{}{} {update}", field(fld), punct(":"))?;
175 }
176
177 let mut deletions: Vec<_> = deletions.iter().collect();
178 deletions.sort_by(|(a, _), (b, _)| a.cmp(b));
179 for (fld, value) in deletions {
180 writeln!(
181 indent,
182 "{} {}{} {}",
183 deleted("-"),
184 field(fld),
185 punct(":"),
186 deleted(&printer.format_peek(*value))
187 )?;
188 }
189
190 let mut insertions: Vec<_> = insertions.iter().collect();
191 insertions.sort_by(|(a, _), (b, _)| a.cmp(b));
192 for (fld, value) in insertions {
193 writeln!(
194 indent,
195 "{} {}{} {}",
196 inserted("+"),
197 field(fld),
198 punct(":"),
199 inserted(&printer.format_peek(*value))
200 )?;
201 }
202
203 write!(f, "{}", punct("}"))
204 }
205 Value::Tuple { updates } => {
206 if updates.is_empty() {
208 return write!(f, "{}", muted("(structurally equal)"));
209 }
210 if updates.is_single_replace() {
212 if has_prefix {
213 f.write_str(" ")?;
214 }
215 write!(f, "{updates}")
216 } else {
217 f.write_str(if has_prefix { " (\n" } else { "(\n" })?;
218 let mut indent = PadAdapter::new_indented(f);
219 write!(indent, "{updates}")?;
220 f.write_str(")")
221 }
222 }
223 }
224 }
225 Diff::Sequence {
226 from: _,
227 to: _,
228 updates,
229 } => {
230 if updates.is_empty() {
231 write!(f, "{}", muted("(structurally equal)"))
232 } else {
233 writeln!(f, "{}", punct("["))?;
234 let mut indent = PadAdapter::new_indented(f);
235 write!(indent, "{updates}")?;
236 write!(f, "{}", punct("]"))
237 }
238 }
239 }
240 }
241}
242
243impl<'mem, 'facet> Updates<'mem, 'facet> {
244 pub fn is_single_replace(&self) -> bool {
246 self.0.first.is_some() && self.0.values.is_empty() && self.0.last.is_none()
247 }
248
249 pub fn is_empty(&self) -> bool {
251 self.0.first.is_none() && self.0.values.is_empty()
252 }
253}
254
255impl<'mem, 'facet> Display for Updates<'mem, 'facet> {
256 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257 if let Some(update) = &self.0.first {
258 update.fmt(f)?;
259 }
260
261 for (values, update) in &self.0.values {
262 let count = values.len();
264 if count > 0 {
265 let label = if count == 1 { "item" } else { "items" };
266 writeln!(f, "{}", muted(&format!(".. {count} unchanged {label}")))?;
267 }
268 update.fmt(f)?;
269 }
270
271 if let Some(values) = &self.0.last {
272 let count = values.len();
274 if count > 0 {
275 let label = if count == 1 { "item" } else { "items" };
276 writeln!(f, "{}", muted(&format!(".. {count} unchanged {label}")))?;
277 }
278 }
279
280 Ok(())
281 }
282}
283
284impl<'mem, 'facet> Display for ReplaceGroup<'mem, 'facet> {
285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286 let printer = PrettyPrinter::default()
287 .with_colors(false)
288 .with_minimal_option_names(true);
289
290 if self.removals.len() == 1 && self.additions.len() == 1 {
292 let from = self.removals[0];
293 let to = self.additions[0];
294
295 if peek_eq(from, to) {
296 return writeln!(f, "{}", muted(&printer.format_peek(from)));
298 }
299
300 return writeln!(
302 f,
303 "{} → {}",
304 deleted(&printer.format_peek(from)),
305 inserted(&printer.format_peek(to))
306 );
307 }
308
309 for remove in &self.removals {
311 writeln!(
312 f,
313 "{}",
314 deleted(&format!("- {}", printer.format_peek(*remove)))
315 )?;
316 }
317
318 for add in &self.additions {
319 writeln!(
320 f,
321 "{}",
322 inserted(&format!("+ {}", printer.format_peek(*add)))
323 )?;
324 }
325
326 Ok(())
327 }
328}
329
330fn write_diff_sequence(
332 f: &mut std::fmt::Formatter<'_>,
333 diffs: &[Diff<'_, '_>],
334) -> std::fmt::Result {
335 let mut i = 0;
336 while i < diffs.len() {
337 let mut equal_count = 0;
339 while i + equal_count < diffs.len() {
340 if matches!(diffs[i + equal_count], Diff::Equal { .. }) {
341 equal_count += 1;
342 } else {
343 break;
344 }
345 }
346
347 if equal_count > 0 {
348 let label = if equal_count == 1 { "item" } else { "items" };
350 writeln!(
351 f,
352 "{}",
353 muted(&format!(".. {equal_count} unchanged {label}"))
354 )?;
355 i += equal_count;
356 } else {
357 writeln!(f, "{}", diffs[i])?;
359 i += 1;
360 }
361 }
362 Ok(())
363}
364
365impl<'mem, 'facet> Display for UpdatesGroup<'mem, 'facet> {
366 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
367 if let Some(update) = &self.0.first {
368 update.fmt(f)?;
369 }
370
371 for (values, update) in &self.0.values {
372 write_diff_sequence(f, values)?;
373 update.fmt(f)?;
374 }
375
376 if let Some(values) = &self.0.last {
377 write_diff_sequence(f, values)?;
378 }
379
380 Ok(())
381 }
382}
383
384fn format_char_with_codepoint(c: char) -> String {
386 if c.is_ascii_graphic() {
388 format!("'{}' (U+{:04X})", c, c as u32)
389 } else {
390 format!("'\\u{{{:04X}}}'", c as u32)
392 }
393}
394
395fn explain_confusable_differences(left: &str, right: &str) -> String {
398 use std::fmt::Write;
399
400 let left_chars: Vec<char> = left.chars().collect();
402 let right_chars: Vec<char> = right.chars().collect();
403
404 let mut out = String::new();
405
406 let mut diffs: Vec<(usize, char, char)> = Vec::new();
408
409 let max_len = left_chars.len().max(right_chars.len());
410 for i in 0..max_len {
411 let lc = left_chars.get(i);
412 let rc = right_chars.get(i);
413
414 match (lc, rc) {
415 (Some(&l), Some(&r)) if l != r => {
416 diffs.push((i, l, r));
417 }
418 (Some(&l), None) => {
419 diffs.push((i, l, '\0'));
421 }
422 (None, Some(&r)) => {
423 diffs.push((i, '\0', r));
425 }
426 _ => {}
427 }
428 }
429
430 if diffs.is_empty() {
431 return muted("(strings are identical)");
432 }
433
434 writeln!(
435 out,
436 "{}",
437 muted(&format!(
438 "(strings are visually confusable but differ in {} position{}):",
439 diffs.len(),
440 if diffs.len() == 1 { "" } else { "s" }
441 ))
442 )
443 .unwrap();
444
445 for (pos, lc, rc) in &diffs {
446 if *lc == '\0' {
447 writeln!(
448 out,
449 " [{}]: (missing) vs {}",
450 pos,
451 inserted(&format_char_with_codepoint(*rc))
452 )
453 .unwrap();
454 } else if *rc == '\0' {
455 writeln!(
456 out,
457 " [{}]: {} vs (missing)",
458 pos,
459 deleted(&format_char_with_codepoint(*lc))
460 )
461 .unwrap();
462 } else {
463 writeln!(
464 out,
465 " [{}]: {} vs {}",
466 pos,
467 deleted(&format_char_with_codepoint(*lc)),
468 inserted(&format_char_with_codepoint(*rc))
469 )
470 .unwrap();
471 }
472 }
473
474 out.trim_end().to_string()
475}