• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

facet-rs / facet / 20107877096

10 Dec 2025 05:40PM UTC coverage: 57.698% (-0.9%) from 58.588%
20107877096

Pull #1220

github

web-flow
Merge 20b27caa2 into fe1531898
Pull Request #1220: feat(cinereus): improve tree matching for leaf nodes and filter no-op moves

1748 of 3613 new or added lines in 18 files covered. (48.38%)

462 existing lines in 6 files now uncovered.

28548 of 49478 relevant lines covered (57.7%)

808.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.05
/facet-diff-core/src/display.rs
1
//! Display implementations for diff types.
2

3
use std::fmt::{Display, Write};
4

5
use confusables::Confusable;
6
use facet_pretty::{PrettyPrinter, tokyo_night};
7
use facet_reflect::Peek;
8
use owo_colors::OwoColorize;
9

10
use crate::{Diff, ReplaceGroup, Updates, UpdatesGroup, Value};
11

12
/// Format text for deletions
13
fn deleted(s: &str) -> String {
182✔
14
    format!("{}", s.color(tokyo_night::DELETION))
182✔
15
}
182✔
16

17
/// Format text for insertions
18
fn inserted(s: &str) -> String {
190✔
19
    format!("{}", s.color(tokyo_night::INSERTION))
190✔
20
}
190✔
21

22
/// Format muted text (unchanged indicators, structural equality)
23
fn muted(s: &str) -> String {
162✔
24
    format!("{}", s.color(tokyo_night::MUTED))
162✔
25
}
162✔
26

27
/// Format field name
28
fn field(s: &str) -> String {
86✔
29
    format!("{}", s.color(tokyo_night::FIELD_NAME))
86✔
30
}
86✔
31

32
/// Format punctuation as dimmed
33
fn punct(s: &str) -> String {
296✔
34
    format!("{}", s.color(tokyo_night::COMMENT))
296✔
35
}
296✔
36

37
struct PadAdapter<'a, 'b: 'a> {
38
    fmt: &'a mut std::fmt::Formatter<'b>,
39
    on_newline: bool,
40
    indent: &'static str,
41
}
42

43
impl<'a, 'b> PadAdapter<'a, 'b> {
44
    fn new_indented(fmt: &'a mut std::fmt::Formatter<'b>) -> Self {
109✔
45
        Self {
109✔
46
            fmt,
109✔
47
            on_newline: true,
109✔
48
            indent: "    ",
109✔
49
        }
109✔
50
    }
109✔
51
}
52

53
impl<'a, 'b> Write for PadAdapter<'a, 'b> {
54
    fn write_str(&mut self, s: &str) -> std::fmt::Result {
2,234✔
55
        for line in s.split_inclusive('\n') {
2,300✔
56
            if self.on_newline {
2,300✔
57
                self.fmt.write_str(self.indent)?;
625✔
58
            }
1,675✔
59

60
            self.on_newline = line.ends_with('\n');
2,300✔
61

62
            self.fmt.write_str(line)?;
2,300✔
63
        }
64

65
        Ok(())
2,234✔
66
    }
2,234✔
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

78
/// Simple equality check for display purposes.
79
/// This is used when rendering ReplaceGroup to check if values are equal.
80
fn peek_eq<'mem, 'facet>(a: Peek<'mem, 'facet>, b: Peek<'mem, 'facet>) -> bool {
15✔
81
    a.shape().id == b.shape().id && a.shape().is_partial_eq() && a == b
15✔
82
}
15✔
83

84
impl<'mem, 'facet> Display for Diff<'mem, 'facet> {
85
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339✔
86
        match self {
339✔
87
            Diff::Equal { value: _ } => {
88
                write!(f, "{}", muted("(structurally equal)"))
52✔
89
            }
90
            Diff::Replace { from, to } => {
141✔
91
                let printer = PrettyPrinter::default()
141✔
92
                    .with_colors(false)
141✔
93
                    .with_minimal_option_names(true);
141✔
94

95
                // Check if both values are strings and visually confusable
96
                // Note: is_confusable_with is directional - check both directions
97
                if let (Some(from_str), Some(to_str)) = (from.as_str(), to.as_str())
141✔
98
                    && (from_str.is_confusable_with(to_str) || to_str.is_confusable_with(from_str))
25✔
99
                {
100
                    // Show the strings with character-level diff
101
                    // Don't wrap in muted() since the explanation has its own colors
102
                    write!(
2✔
103
                        f,
2✔
104
                        "{} → {}\n{}",
105
                        deleted(&printer.format_peek(*from)),
2✔
106
                        inserted(&printer.format_peek(*to)),
2✔
107
                        explain_confusable_differences(from_str, to_str)
2✔
108
                    )?;
×
109
                    return Ok(());
2✔
110
                }
139✔
111

112
                // Show value change inline: old → new
113
                write!(
139✔
114
                    f,
139✔
115
                    "{} → {}",
116
                    deleted(&printer.format_peek(*from)),
139✔
117
                    inserted(&printer.format_peek(*to))
139✔
118
                )
119
            }
120
            Diff::User {
121
                from: _,
122
                to: _,
123
                variant,
99✔
124
                value,
99✔
125
            } => {
126
                let printer = PrettyPrinter::default()
99✔
127
                    .with_colors(false)
99✔
128
                    .with_minimal_option_names(true);
99✔
129

130
                // Show variant if present (e.g., "Some" for Option::Some)
131
                if let Some(variant) = variant {
99✔
132
                    write!(f, "{}", variant.bold())?;
19✔
133
                }
80✔
134

135
                let has_prefix = variant.is_some();
99✔
136

137
                match value {
99✔
138
                    Value::Struct {
139
                        updates,
76✔
140
                        deletions,
76✔
141
                        insertions,
76✔
142
                        unchanged,
76✔
143
                    } => {
144
                        if updates.is_empty() && deletions.is_empty() && insertions.is_empty() {
76✔
145
                            return write!(f, "{}", muted("(structurally equal)"));
12✔
146
                        }
64✔
147

148
                        if has_prefix {
64✔
149
                            writeln!(f, " {}", punct("{"))?;
1✔
150
                        } else {
151
                            writeln!(f, "{}", punct("{"))?;
63✔
152
                        }
153
                        let mut indent = PadAdapter::new_indented(f);
64✔
154

155
                        // Show unchanged fields indicator first
156
                        let unchanged_count = unchanged.len();
64✔
157
                        if unchanged_count > 0 {
64✔
158
                            let label = if unchanged_count == 1 {
42✔
159
                                "field"
29✔
160
                            } else {
161
                                "fields"
13✔
162
                            };
163
                            writeln!(
42✔
164
                                indent,
42✔
165
                                "{}",
166
                                muted(&format!(".. {unchanged_count} unchanged {label}"))
42✔
167
                            )?;
×
168
                        }
22✔
169

170
                        // Sort fields for deterministic output
171
                        let mut updates: Vec<_> = updates.iter().collect();
64✔
172
                        updates.sort_by(|(a, _), (b, _)| a.cmp(b));
64✔
173
                        for (fld, update) in updates {
82✔
174
                            writeln!(indent, "{}{} {update}", field(fld), punct(":"))?;
82✔
175
                        }
176

177
                        let mut deletions: Vec<_> = deletions.iter().collect();
64✔
178
                        deletions.sort_by(|(a, _), (b, _)| a.cmp(b));
64✔
179
                        for (fld, value) in deletions {
64✔
180
                            writeln!(
2✔
181
                                indent,
2✔
182
                                "{} {}{} {}",
183
                                deleted("-"),
2✔
184
                                field(fld),
2✔
185
                                punct(":"),
2✔
186
                                deleted(&printer.format_peek(*value))
2✔
187
                            )?;
×
188
                        }
189

190
                        let mut insertions: Vec<_> = insertions.iter().collect();
64✔
191
                        insertions.sort_by(|(a, _), (b, _)| a.cmp(b));
64✔
192
                        for (fld, value) in insertions {
64✔
193
                            writeln!(
2✔
194
                                indent,
2✔
195
                                "{} {}{} {}",
196
                                inserted("+"),
2✔
197
                                field(fld),
2✔
198
                                punct(":"),
2✔
199
                                inserted(&printer.format_peek(*value))
2✔
200
                            )?;
×
201
                        }
202

203
                        write!(f, "{}", punct("}"))
64✔
204
                    }
205
                    Value::Tuple { updates } => {
23✔
206
                        // No changes in tuple
207
                        if updates.is_empty() {
23✔
208
                            return write!(f, "{}", muted("(structurally equal)"));
2✔
209
                        }
21✔
210
                        // For single-element tuples (like Option::Some), try to be concise
211
                        if updates.is_single_replace() {
21✔
212
                            if has_prefix {
17✔
213
                                f.write_str(" ")?;
14✔
214
                            }
3✔
215
                            write!(f, "{updates}")
17✔
216
                        } else {
217
                            f.write_str(if has_prefix { " (\n" } else { "(\n" })?;
4✔
218
                            let mut indent = PadAdapter::new_indented(f);
4✔
219
                            write!(indent, "{updates}")?;
4✔
220
                            f.write_str(")")
4✔
221
                        }
222
                    }
223
                }
224
            }
225
            Diff::Sequence {
226
                from: _,
227
                to: _,
228
                updates,
47✔
229
            } => {
230
                if updates.is_empty() {
47✔
231
                    write!(f, "{}", muted("(structurally equal)"))
6✔
232
                } else {
233
                    writeln!(f, "{}", punct("["))?;
41✔
234
                    let mut indent = PadAdapter::new_indented(f);
41✔
235
                    write!(indent, "{updates}")?;
41✔
236
                    write!(f, "{}", punct("]"))
41✔
237
                }
238
            }
239
        }
240
    }
339✔
241
}
242

243
impl<'mem, 'facet> Updates<'mem, 'facet> {
244
    /// Check if this is a single replace operation (useful for Option::Some)
245
    pub fn is_single_replace(&self) -> bool {
21✔
246
        self.0.first.is_some() && self.0.values.is_empty() && self.0.last.is_none()
21✔
247
    }
21✔
248

249
    /// Check if there are no changes (everything is unchanged)
250
    pub fn is_empty(&self) -> bool {
70✔
251
        self.0.first.is_none() && self.0.values.is_empty()
70✔
252
    }
70✔
253
}
254

255
impl<'mem, 'facet> Display for Updates<'mem, 'facet> {
256
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62✔
257
        if let Some(update) = &self.0.first {
62✔
258
            update.fmt(f)?;
44✔
259
        }
18✔
260

261
        for (values, update) in &self.0.values {
62✔
262
            // Collapse kept values into ".. N unchanged items"
263
            let count = values.len();
25✔
264
            if count > 0 {
25✔
265
                let label = if count == 1 { "item" } else { "items" };
25✔
266
                writeln!(f, "{}", muted(&format!(".. {count} unchanged {label}")))?;
25✔
267
            }
×
268
            update.fmt(f)?;
25✔
269
        }
270

271
        if let Some(values) = &self.0.last {
62✔
272
            // Collapse trailing kept values
273
            let count = values.len();
9✔
274
            if count > 0 {
9✔
275
                let label = if count == 1 { "item" } else { "items" };
9✔
276
                writeln!(f, "{}", muted(&format!(".. {count} unchanged {label}")))?;
9✔
277
            }
×
278
        }
53✔
279

280
        Ok(())
62✔
281
    }
62✔
282
}
283

284
impl<'mem, 'facet> Display for ReplaceGroup<'mem, 'facet> {
285
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43✔
286
        let printer = PrettyPrinter::default()
43✔
287
            .with_colors(false)
43✔
288
            .with_minimal_option_names(true);
43✔
289

290
        // If it's a 1-to-1 replacement, check for equality
291
        if self.removals.len() == 1 && self.additions.len() == 1 {
43✔
292
            let from = self.removals[0];
15✔
293
            let to = self.additions[0];
15✔
294

295
            if peek_eq(from, to) {
15✔
296
                // Values are equal, show muted
NEW
297
                return writeln!(f, "{}", muted(&printer.format_peek(from)));
×
298
            }
15✔
299

300
            // Show value change inline: old → new
301
            return writeln!(
15✔
302
                f,
15✔
303
                "{} → {}",
304
                deleted(&printer.format_peek(from)),
15✔
305
                inserted(&printer.format_peek(to))
15✔
306
            );
307
        }
28✔
308

309
        // Otherwise show as - / + lines with consistent indentation
310
        for remove in &self.removals {
28✔
311
            writeln!(
22✔
312
                f,
22✔
313
                "{}",
314
                deleted(&format!("- {}", printer.format_peek(*remove)))
22✔
315
            )?;
×
316
        }
317

318
        for add in &self.additions {
30✔
319
            writeln!(
30✔
320
                f,
30✔
321
                "{}",
322
                inserted(&format!("+ {}", printer.format_peek(*add)))
30✔
323
            )?;
×
324
        }
325

326
        Ok(())
28✔
327
    }
43✔
328
}
329

330
/// Write a sequence of diffs, collapsing Equal diffs into ".. N unchanged items"
331
fn write_diff_sequence(
32✔
332
    f: &mut std::fmt::Formatter<'_>,
32✔
333
    diffs: &[Diff<'_, '_>],
32✔
334
) -> std::fmt::Result {
32✔
335
    let mut i = 0;
32✔
336
    while i < diffs.len() {
89✔
337
        // Count consecutive Equal diffs
338
        let mut equal_count = 0;
57✔
339
        while i + equal_count < diffs.len() {
70✔
340
            if matches!(diffs[i + equal_count], Diff::Equal { .. }) {
70✔
341
                equal_count += 1;
13✔
342
            } else {
13✔
343
                break;
57✔
344
            }
345
        }
346

347
        if equal_count > 0 {
57✔
348
            // Collapse Equal diffs
349
            let label = if equal_count == 1 { "item" } else { "items" };
12✔
350
            writeln!(
12✔
351
                f,
12✔
352
                "{}",
353
                muted(&format!(".. {equal_count} unchanged {label}"))
12✔
354
            )?;
×
355
            i += equal_count;
12✔
356
        } else {
357
            // Show the non-Equal diff
358
            writeln!(f, "{}", diffs[i])?;
45✔
359
            i += 1;
45✔
360
        }
361
    }
362
    Ok(())
32✔
363
}
32✔
364

365
impl<'mem, 'facet> Display for UpdatesGroup<'mem, 'facet> {
366
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69✔
367
        if let Some(update) = &self.0.first {
69✔
368
            update.fmt(f)?;
39✔
369
        }
30✔
370

371
        for (values, update) in &self.0.values {
69✔
372
            write_diff_sequence(f, values)?;
4✔
373
            update.fmt(f)?;
4✔
374
        }
375

376
        if let Some(values) = &self.0.last {
69✔
377
            write_diff_sequence(f, values)?;
28✔
378
        }
41✔
379

380
        Ok(())
69✔
381
    }
69✔
382
}
383

384
/// Format a character for display with its Unicode codepoint.
385
fn format_char_with_codepoint(c: char) -> String {
×
386
    // For printable ASCII characters (except space), show the character directly
387
    if c.is_ascii_graphic() {
×
388
        format!("'{}' (U+{:04X})", c, c as u32)
×
389
    } else {
390
        // For non-printable chars, show the escaped form (codepoint is visible in the escape)
NEW
391
        format!("'\\u{{{:04X}}}'", c as u32)
×
392
    }
393
}
×
394

395
/// Explain the confusable differences between two strings that look identical.
396
/// Shows character-level differences with full Unicode codepoints.
397
fn explain_confusable_differences(left: &str, right: &str) -> String {
2✔
398
    use std::fmt::Write;
399

400
    // Find character-level differences
401
    let left_chars: Vec<char> = left.chars().collect();
2✔
402
    let right_chars: Vec<char> = right.chars().collect();
2✔
403

404
    let mut out = String::new();
2✔
405

406
    // Find all positions where characters differ
407
    let mut diffs: Vec<(usize, char, char)> = Vec::new();
2✔
408

409
    let max_len = left_chars.len().max(right_chars.len());
2✔
410
    for i in 0..max_len {
10✔
411
        let lc = left_chars.get(i);
10✔
412
        let rc = right_chars.get(i);
10✔
413

414
        match (lc, rc) {
10✔
415
            (Some(&l), Some(&r)) if l != r => {
10✔
416
                diffs.push((i, l, r));
×
417
            }
×
418
            (Some(&l), None) => {
×
419
                // Character only in left (will show as deletion)
×
420
                diffs.push((i, l, '\0'));
×
421
            }
×
422
            (None, Some(&r)) => {
×
423
                // Character only in right (will show as insertion)
×
424
                diffs.push((i, '\0', r));
×
425
            }
×
426
            _ => {}
10✔
427
        }
428
    }
429

430
    if diffs.is_empty() {
2✔
431
        return muted("(strings are identical)");
2✔
432
    }
×
433

434
    writeln!(
×
435
        out,
×
436
        "{}",
NEW
437
        muted(&format!(
×
438
            "(strings are visually confusable but differ in {} position{}):",
NEW
439
            diffs.len(),
×
NEW
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,
×
NEW
451
                inserted(&format_char_with_codepoint(*rc))
×
452
            )
×
453
            .unwrap();
×
454
        } else if *rc == '\0' {
×
455
            writeln!(
×
456
                out,
×
457
                "  [{}]: {} vs (missing)",
×
458
                pos,
×
NEW
459
                deleted(&format_char_with_codepoint(*lc))
×
460
            )
×
461
            .unwrap();
×
462
        } else {
×
463
            writeln!(
×
464
                out,
×
465
                "  [{}]: {} vs {}",
×
466
                pos,
×
NEW
467
                deleted(&format_char_with_codepoint(*lc)),
×
NEW
468
                inserted(&format_char_with_codepoint(*rc))
×
469
            )
×
470
            .unwrap();
×
471
        }
×
472
    }
473

474
    out.trim_end().to_string()
×
475
}
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc