Skip to main content

facet_pretty/
printer.rs

1//! Pretty printer implementation for Facet types
2
3use alloc::borrow::Cow;
4use alloc::collections::BTreeMap;
5use core::{
6    fmt::{self, Write},
7    hash::{Hash, Hasher},
8    str,
9};
10use std::{hash::DefaultHasher, sync::LazyLock};
11
12use facet_core::{
13    Def, DynDateTimeKind, DynValueKind, Facet, Field, PointerType, PrimitiveType, PtrUninit,
14    SequenceType, Shape, StructKind, StructType, TextualType, Type, TypeNameOpts, UserType,
15};
16use facet_reflect::{Peek, ValueId};
17
18use owo_colors::{OwoColorize, Rgb};
19
20use crate::color::ColorGenerator;
21use crate::shape::{FieldSpan, Path, PathSegment, Span};
22
23/// Tokyo Night color palette (RGB values from official theme)
24///
25/// See: <https://github.com/tokyo-night/tokyo-night-vscode-theme>
26pub mod tokyo_night {
27    use owo_colors::Rgb;
28
29    // ========================================================================
30    // Core colors
31    // ========================================================================
32
33    /// Foreground - main text (#a9b1d6)
34    pub const FOREGROUND: Rgb = Rgb(169, 177, 214);
35    /// Background (#1a1b26)
36    pub const BACKGROUND: Rgb = Rgb(26, 27, 38);
37    /// Comment - muted text (#565f89)
38    pub const COMMENT: Rgb = Rgb(86, 95, 137);
39
40    // ========================================================================
41    // Terminal ANSI colors
42    // ========================================================================
43
44    /// Black (#414868)
45    pub const BLACK: Rgb = Rgb(65, 72, 104);
46    /// Red (#f7768e)
47    pub const RED: Rgb = Rgb(247, 118, 142);
48    /// Green - teal/cyan green (#73daca)
49    pub const GREEN: Rgb = Rgb(115, 218, 202);
50    /// Yellow - warm orange-yellow (#e0af68)
51    pub const YELLOW: Rgb = Rgb(224, 175, 104);
52    /// Blue (#7aa2f7)
53    pub const BLUE: Rgb = Rgb(122, 162, 247);
54    /// Magenta - purple (#bb9af7)
55    pub const MAGENTA: Rgb = Rgb(187, 154, 247);
56    /// Cyan - bright cyan (#7dcfff)
57    pub const CYAN: Rgb = Rgb(125, 207, 255);
58    /// White - muted white (#787c99)
59    pub const WHITE: Rgb = Rgb(120, 124, 153);
60
61    /// Bright white (#acb0d0)
62    pub const BRIGHT_WHITE: Rgb = Rgb(172, 176, 208);
63
64    // ========================================================================
65    // Extended syntax colors
66    // ========================================================================
67
68    /// Orange - numbers, constants (#ff9e64)
69    pub const ORANGE: Rgb = Rgb(255, 158, 100);
70    /// Dark green - strings (#9ece6a)
71    pub const DARK_GREEN: Rgb = Rgb(158, 206, 106);
72
73    // ========================================================================
74    // Semantic/status colors
75    // ========================================================================
76
77    /// Error - bright red for errors (#db4b4b)
78    pub const ERROR: Rgb = Rgb(219, 75, 75);
79    /// Warning - same as yellow (#e0af68)
80    pub const WARNING: Rgb = YELLOW;
81    /// Info - teal-blue (#0db9d7)
82    pub const INFO: Rgb = Rgb(13, 185, 215);
83    /// Hint - same as comment, muted
84    pub const HINT: Rgb = COMMENT;
85
86    // ========================================================================
87    // Semantic aliases for specific uses
88    // ========================================================================
89
90    /// Type names - blue, bold
91    pub const TYPE_NAME: Rgb = BLUE;
92    /// Field names - green/teal
93    pub const FIELD_NAME: Rgb = GREEN;
94    /// String literals - dark green
95    pub const STRING: Rgb = DARK_GREEN;
96    /// Number literals - orange
97    pub const NUMBER: Rgb = ORANGE;
98    /// Keywords (null, true, false) - magenta
99    pub const KEYWORD: Rgb = MAGENTA;
100    /// Deletions in diffs - red
101    pub const DELETION: Rgb = RED;
102    /// Insertions in diffs - green
103    pub const INSERTION: Rgb = GREEN;
104    /// Muted/unchanged - comment color
105    pub const MUTED: Rgb = COMMENT;
106    /// Borders - very muted, comment color
107    pub const BORDER: Rgb = COMMENT;
108}
109
110/// A formatter for pretty-printing Facet types
111#[derive(Clone, PartialEq)]
112pub struct PrettyPrinter {
113    /// usize::MAX is a special value that means indenting with tabs instead of spaces
114    indent_size: usize,
115    max_depth: Option<usize>,
116    color_generator: ColorGenerator,
117    colors: ColorMode,
118    list_u8_as_bytes: bool,
119    /// Skip type names for Options (show `Some(x)` instead of `Option<T>::Some(x)`)
120    minimal_option_names: bool,
121    /// Whether to show doc comments in output
122    show_doc_comments: bool,
123    /// Maximum length for strings/bytes before truncating the middle (None = no limit)
124    max_content_len: Option<usize>,
125}
126
127impl Default for PrettyPrinter {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133impl PrettyPrinter {
134    /// Create a new PrettyPrinter with default settings
135    pub const fn new() -> Self {
136        Self {
137            indent_size: 2,
138            max_depth: None,
139            color_generator: ColorGenerator::new(),
140            colors: ColorMode::Auto,
141            list_u8_as_bytes: true,
142            minimal_option_names: false,
143            show_doc_comments: false,
144            max_content_len: None,
145        }
146    }
147
148    /// Set the indentation size
149    pub const fn with_indent_size(mut self, size: usize) -> Self {
150        self.indent_size = size;
151        self
152    }
153
154    /// Set the maximum depth for recursive printing
155    pub const fn with_max_depth(mut self, depth: usize) -> Self {
156        self.max_depth = Some(depth);
157        self
158    }
159
160    /// Set the color generator
161    pub const fn with_color_generator(mut self, generator: ColorGenerator) -> Self {
162        self.color_generator = generator;
163        self
164    }
165
166    /// Enable or disable colors. Use `None` to automatically detect color support based on the `NO_COLOR` environment variable.
167    pub const fn with_colors(mut self, enable_colors: ColorMode) -> Self {
168        self.colors = enable_colors;
169        self
170    }
171
172    /// Use minimal names for Options (show `Some(x)` instead of `Option<T>::Some(x)`)
173    pub const fn with_minimal_option_names(mut self, minimal: bool) -> Self {
174        self.minimal_option_names = minimal;
175        self
176    }
177
178    /// Enable or disable doc comments in output
179    pub const fn with_doc_comments(mut self, show: bool) -> Self {
180        self.show_doc_comments = show;
181        self
182    }
183
184    /// Set the maximum length for strings and byte arrays before truncating
185    ///
186    /// When set, strings and byte arrays longer than this limit will be
187    /// truncated in the middle, showing the beginning and end with `...` between.
188    pub const fn with_max_content_len(mut self, max_len: usize) -> Self {
189        self.max_content_len = Some(max_len);
190        self
191    }
192
193    /// Format a value to a string
194    pub fn format<'a, T: ?Sized + Facet<'a>>(&self, value: &T) -> String {
195        let value = Peek::new(value);
196
197        let mut output = String::new();
198        self.format_peek_internal(value, &mut output, &mut BTreeMap::new())
199            .expect("Formatting failed");
200
201        output
202    }
203
204    /// Format a value to a formatter
205    pub fn format_to<'a, T: ?Sized + Facet<'a>>(
206        &self,
207        value: &T,
208        f: &mut fmt::Formatter<'_>,
209    ) -> fmt::Result {
210        let value = Peek::new(value);
211        self.format_peek_internal(value, f, &mut BTreeMap::new())
212    }
213
214    /// Format a value to a string
215    pub fn format_peek(&self, value: Peek<'_, '_>) -> String {
216        let mut output = String::new();
217        self.format_peek_internal(value, &mut output, &mut BTreeMap::new())
218            .expect("Formatting failed");
219        output
220    }
221
222    pub(crate) fn shape_chunkiness(shape: &Shape) -> usize {
223        let mut shape = shape;
224        while let Type::Pointer(PointerType::Reference(inner)) = shape.ty {
225            shape = inner.target;
226        }
227
228        match shape.ty {
229            Type::Pointer(_) | Type::Primitive(_) => 1,
230            Type::Sequence(SequenceType::Array(ty)) => {
231                Self::shape_chunkiness(ty.t).saturating_mul(ty.n)
232            }
233            Type::Sequence(SequenceType::Slice(_)) => usize::MAX,
234            Type::User(ty) => match ty {
235                UserType::Struct(ty) => {
236                    let mut sum = 0usize;
237                    for field in ty.fields {
238                        sum = sum.saturating_add(Self::shape_chunkiness(field.shape()));
239                    }
240                    sum
241                }
242                UserType::Enum(ty) => {
243                    let mut max = 0usize;
244                    for variant in ty.variants {
245                        max = Ord::max(max, {
246                            let mut sum = 0usize;
247                            for field in variant.data.fields {
248                                sum = sum.saturating_add(Self::shape_chunkiness(field.shape()));
249                            }
250                            sum
251                        })
252                    }
253                    max
254                }
255                UserType::Opaque | UserType::Union(_) => 1,
256            },
257            Type::Undefined => 1,
258        }
259    }
260
261    #[inline]
262    fn use_colors(&self) -> bool {
263        self.colors.enabled()
264    }
265
266    #[allow(clippy::too_many_arguments)]
267    pub(crate) fn format_peek_internal_(
268        &self,
269        value: Peek<'_, '_>,
270        f: &mut dyn Write,
271        visited: &mut BTreeMap<ValueId, usize>,
272        format_depth: usize,
273        type_depth: usize,
274        short: bool,
275    ) -> fmt::Result {
276        let mut value = value;
277        while let Ok(ptr) = value.into_pointer()
278            && let Some(pointee) = ptr.borrow_inner()
279        {
280            value = pointee;
281        }
282
283        // Unwrap transparent wrappers (e.g., newtype wrappers like IntAsString(String))
284        // This matches serialization behavior where we serialize the inner value directly
285        let value = value.innermost_peek();
286        let shape = value.shape();
287
288        if let Some(prev_type_depth) = visited.insert(value.id(), type_depth) {
289            self.write_type_name(f, &value)?;
290            self.write_punctuation(f, " { ")?;
291            self.write_comment(
292                f,
293                &format!(
294                    "/* cycle detected at {} (first seen at type_depth {}) */",
295                    value.id(),
296                    prev_type_depth,
297                ),
298            )?;
299            visited.remove(&value.id());
300            return Ok(());
301        }
302
303        // Handle proxy types by converting to the proxy representation and formatting that
304        if let Some(proxy_def) = shape.proxy {
305            return self.format_via_proxy(
306                value,
307                proxy_def,
308                f,
309                visited,
310                format_depth,
311                type_depth,
312                short,
313            );
314        }
315
316        match (shape.def, shape.ty) {
317            (_, Type::Primitive(PrimitiveType::Textual(TextualType::Str))) => {
318                let value = value.get::<str>().unwrap();
319                self.format_str_value(f, value)?;
320            }
321            // Handle String specially to add quotes (like &str)
322            (Def::Scalar, _) if value.shape().id == <alloc::string::String as Facet>::SHAPE.id => {
323                let s = value.get::<alloc::string::String>().unwrap();
324                self.format_str_value(f, s)?;
325            }
326            (Def::Scalar, _) => self.format_scalar(value, f)?,
327            (Def::Option(_), _) => {
328                let option = value.into_option().unwrap();
329
330                // Print the Option name (unless minimal mode)
331                if !self.minimal_option_names {
332                    self.write_type_name(f, &value)?;
333                }
334
335                if let Some(inner) = option.value() {
336                    let prefix = if self.minimal_option_names {
337                        "Some("
338                    } else {
339                        "::Some("
340                    };
341                    self.write_punctuation(f, prefix)?;
342                    self.format_peek_internal_(
343                        inner,
344                        f,
345                        visited,
346                        format_depth,
347                        type_depth + 1,
348                        short,
349                    )?;
350                    self.write_punctuation(f, ")")?;
351                } else {
352                    let suffix = if self.minimal_option_names {
353                        "None"
354                    } else {
355                        "::None"
356                    };
357                    self.write_punctuation(f, suffix)?;
358                }
359            }
360
361            (_, Type::Pointer(PointerType::Raw(_) | PointerType::Function(_))) => {
362                self.write_type_name(f, &value)?;
363                let addr = unsafe { value.data().read::<*const ()>() };
364                let value = Peek::new(&addr);
365                self.format_scalar(value, f)?;
366            }
367
368            (_, Type::User(UserType::Union(_))) => {
369                if !short && self.show_doc_comments {
370                    for &line in shape.doc {
371                        self.write_comment(f, &format!("///{line}"))?;
372                        writeln!(f)?;
373                        self.indent(f, format_depth)?;
374                    }
375                }
376                self.write_type_name(f, &value)?;
377
378                self.write_punctuation(f, " { ")?;
379                self.write_comment(f, "/* contents of untagged union */")?;
380                self.write_punctuation(f, " }")?;
381            }
382
383            (
384                _,
385                Type::User(UserType::Struct(
386                    ty @ StructType {
387                        kind: StructKind::Tuple | StructKind::TupleStruct,
388                        ..
389                    },
390                )),
391            ) => {
392                if !short && self.show_doc_comments {
393                    for &line in shape.doc {
394                        self.write_comment(f, &format!("///{line}"))?;
395                        writeln!(f)?;
396                        self.indent(f, format_depth)?;
397                    }
398                }
399
400                self.write_type_name(f, &value)?;
401                if matches!(ty.kind, StructKind::Tuple) {
402                    write!(f, " ")?;
403                }
404                let value = value.into_struct().unwrap();
405
406                let fields = ty.fields;
407                self.format_tuple_fields(
408                    &|i| value.field(i).unwrap(),
409                    f,
410                    visited,
411                    format_depth,
412                    type_depth,
413                    fields,
414                    short,
415                    matches!(ty.kind, StructKind::Tuple),
416                )?;
417            }
418
419            (
420                _,
421                Type::User(UserType::Struct(
422                    ty @ StructType {
423                        kind: StructKind::Struct | StructKind::Unit,
424                        ..
425                    },
426                )),
427            ) => {
428                if !short && self.show_doc_comments {
429                    for &line in shape.doc {
430                        self.write_comment(f, &format!("///{line}"))?;
431                        writeln!(f)?;
432                        self.indent(f, format_depth)?;
433                    }
434                }
435
436                self.write_type_name(f, &value)?;
437
438                if matches!(ty.kind, StructKind::Struct) {
439                    let value = value.into_struct().unwrap();
440                    self.format_struct_fields(
441                        &|i| value.field(i).unwrap(),
442                        f,
443                        visited,
444                        format_depth,
445                        type_depth,
446                        ty.fields,
447                        short,
448                    )?;
449                }
450            }
451
452            (_, Type::User(UserType::Enum(_))) => {
453                let enum_peek = value.into_enum().unwrap();
454                match enum_peek.active_variant() {
455                    Err(_) => {
456                        // Print the enum name
457                        self.write_type_name(f, &value)?;
458                        self.write_punctuation(f, " {")?;
459                        self.write_comment(f, " /* cannot determine variant */ ")?;
460                        self.write_punctuation(f, "}")?;
461                    }
462                    Ok(variant) => {
463                        if !short && self.show_doc_comments {
464                            for &line in shape.doc {
465                                self.write_comment(f, &format!("///{line}"))?;
466                                writeln!(f)?;
467                                self.indent(f, format_depth)?;
468                            }
469                            for &line in variant.doc {
470                                self.write_comment(f, &format!("///{line}"))?;
471                                writeln!(f)?;
472                                self.indent(f, format_depth)?;
473                            }
474                        }
475                        self.write_type_name(f, &value)?;
476                        self.write_punctuation(f, "::")?;
477
478                        // Variant docs are already handled above
479
480                        // Get the active variant name - we've already checked above that we can get it
481                        // This is the same variant, but we're repeating the code here to ensure consistency
482
483                        // Apply color for variant name
484                        if self.use_colors() {
485                            write!(f, "{}", variant.name.bold())?;
486                        } else {
487                            write!(f, "{}", variant.name)?;
488                        }
489
490                        // Process the variant fields based on the variant kind
491                        match variant.data.kind {
492                            StructKind::Unit => {
493                                // Unit variant has no fields, nothing more to print
494                            }
495                            StructKind::Struct => self.format_struct_fields(
496                                &|i| enum_peek.field(i).unwrap().unwrap(),
497                                f,
498                                visited,
499                                format_depth,
500                                type_depth,
501                                variant.data.fields,
502                                short,
503                            )?,
504                            _ => self.format_tuple_fields(
505                                &|i| enum_peek.field(i).unwrap().unwrap(),
506                                f,
507                                visited,
508                                format_depth,
509                                type_depth,
510                                variant.data.fields,
511                                short,
512                                false,
513                            )?,
514                        }
515                    }
516                };
517            }
518
519            _ if value.into_list_like().is_ok() => {
520                let list = value.into_list_like().unwrap();
521
522                // When recursing into a list, always increment format_depth
523                // Only increment type_depth if we're moving to a different address
524
525                // Print the list name
526                self.write_type_name(f, &value)?;
527
528                if !list.is_empty() {
529                    if list.def().t().is_type::<u8>() && self.list_u8_as_bytes {
530                        let total_len = list.len();
531                        let truncate = self.max_content_len.is_some_and(|max| total_len > max);
532
533                        self.write_punctuation(f, " [")?;
534
535                        if truncate {
536                            let max = self.max_content_len.unwrap();
537                            let half = max / 2;
538                            let start_count = half;
539                            let end_count = half;
540
541                            // Show beginning
542                            for (idx, item) in list.iter().enumerate().take(start_count) {
543                                if !short && idx % 16 == 0 {
544                                    writeln!(f)?;
545                                    self.indent(f, format_depth + 1)?;
546                                }
547                                write!(f, " ")?;
548                                let byte = *item.get::<u8>().unwrap();
549                                if self.use_colors() {
550                                    let mut hasher = DefaultHasher::new();
551                                    byte.hash(&mut hasher);
552                                    let hash = hasher.finish();
553                                    let color = self.color_generator.generate_color(hash);
554                                    let rgb = Rgb(color.r, color.g, color.b);
555                                    write!(f, "{}", format!("{byte:02x}").color(rgb))?;
556                                } else {
557                                    write!(f, "{byte:02x}")?;
558                                }
559                            }
560
561                            // Show ellipsis
562                            let omitted = total_len - start_count - end_count;
563                            if !short {
564                                writeln!(f)?;
565                                self.indent(f, format_depth + 1)?;
566                            }
567                            write!(f, " ...({omitted} bytes)...")?;
568
569                            // Show end
570                            for (idx, item) in list.iter().enumerate().skip(total_len - end_count) {
571                                let display_idx = start_count + 1 + (idx - (total_len - end_count));
572                                if !short && display_idx.is_multiple_of(16) {
573                                    writeln!(f)?;
574                                    self.indent(f, format_depth + 1)?;
575                                }
576                                write!(f, " ")?;
577                                let byte = *item.get::<u8>().unwrap();
578                                if self.use_colors() {
579                                    let mut hasher = DefaultHasher::new();
580                                    byte.hash(&mut hasher);
581                                    let hash = hasher.finish();
582                                    let color = self.color_generator.generate_color(hash);
583                                    let rgb = Rgb(color.r, color.g, color.b);
584                                    write!(f, "{}", format!("{byte:02x}").color(rgb))?;
585                                } else {
586                                    write!(f, "{byte:02x}")?;
587                                }
588                            }
589                        } else {
590                            for (idx, item) in list.iter().enumerate() {
591                                if !short && idx % 16 == 0 {
592                                    writeln!(f)?;
593                                    self.indent(f, format_depth + 1)?;
594                                }
595                                write!(f, " ")?;
596
597                                let byte = *item.get::<u8>().unwrap();
598                                if self.use_colors() {
599                                    let mut hasher = DefaultHasher::new();
600                                    byte.hash(&mut hasher);
601                                    let hash = hasher.finish();
602                                    let color = self.color_generator.generate_color(hash);
603                                    let rgb = Rgb(color.r, color.g, color.b);
604                                    write!(f, "{}", format!("{byte:02x}").color(rgb))?;
605                                } else {
606                                    write!(f, "{byte:02x}")?;
607                                }
608                            }
609                        }
610
611                        if !short {
612                            writeln!(f)?;
613                            self.indent(f, format_depth)?;
614                        }
615                        self.write_punctuation(f, "]")?;
616                    } else {
617                        // Check if elements are simple scalars - render inline if so
618                        let elem_shape = list.def().t();
619                        let is_simple = Self::shape_chunkiness(elem_shape) <= 1;
620
621                        self.write_punctuation(f, " [")?;
622                        let len = list.len();
623                        for (idx, item) in list.iter().enumerate() {
624                            if !short && !is_simple {
625                                writeln!(f)?;
626                                self.indent(f, format_depth + 1)?;
627                            } else if idx > 0 {
628                                write!(f, " ")?;
629                            }
630                            self.format_peek_internal_(
631                                item,
632                                f,
633                                visited,
634                                format_depth + 1,
635                                type_depth + 1,
636                                short || is_simple,
637                            )?;
638
639                            if (!short && !is_simple) || idx + 1 < len {
640                                self.write_punctuation(f, ",")?;
641                            }
642                        }
643                        if !short && !is_simple {
644                            writeln!(f)?;
645                            self.indent(f, format_depth)?;
646                        }
647                        self.write_punctuation(f, "]")?;
648                    }
649                } else {
650                    self.write_punctuation(f, "[]")?;
651                }
652            }
653
654            _ if value.into_set().is_ok() => {
655                self.write_type_name(f, &value)?;
656
657                let value = value.into_set().unwrap();
658                self.write_punctuation(f, " [")?;
659                if !value.is_empty() {
660                    let len = value.len();
661                    for (idx, item) in value.iter().enumerate() {
662                        if !short {
663                            writeln!(f)?;
664                            self.indent(f, format_depth + 1)?;
665                        }
666                        self.format_peek_internal_(
667                            item,
668                            f,
669                            visited,
670                            format_depth + 1,
671                            type_depth + 1,
672                            short,
673                        )?;
674                        if !short || idx + 1 < len {
675                            self.write_punctuation(f, ",")?;
676                        } else {
677                            write!(f, " ")?;
678                        }
679                    }
680                    if !short {
681                        writeln!(f)?;
682                        self.indent(f, format_depth)?;
683                    }
684                }
685                self.write_punctuation(f, "]")?;
686            }
687
688            (Def::Map(def), _) => {
689                let key_is_short = Self::shape_chunkiness(def.k) <= 2;
690
691                self.write_type_name(f, &value)?;
692
693                let value = value.into_map().unwrap();
694                self.write_punctuation(f, " [")?;
695
696                if !value.is_empty() {
697                    let len = value.len();
698                    for (idx, (key, value)) in value.iter().enumerate() {
699                        if !short {
700                            writeln!(f)?;
701                            self.indent(f, format_depth + 1)?;
702                        }
703                        self.format_peek_internal_(
704                            key,
705                            f,
706                            visited,
707                            format_depth + 1,
708                            type_depth + 1,
709                            key_is_short,
710                        )?;
711                        self.write_punctuation(f, " => ")?;
712                        self.format_peek_internal_(
713                            value,
714                            f,
715                            visited,
716                            format_depth + 1,
717                            type_depth + 1,
718                            short,
719                        )?;
720                        if !short || idx + 1 < len {
721                            self.write_punctuation(f, ",")?;
722                        } else {
723                            write!(f, " ")?;
724                        }
725                    }
726                    if !short {
727                        writeln!(f)?;
728                        self.indent(f, format_depth)?;
729                    }
730                }
731
732                self.write_punctuation(f, "]")?;
733            }
734
735            (Def::DynamicValue(_), _) => {
736                let dyn_val = value.into_dynamic_value().unwrap();
737                match dyn_val.kind() {
738                    DynValueKind::Null => {
739                        self.write_keyword(f, "null")?;
740                    }
741                    DynValueKind::Bool => {
742                        if let Some(b) = dyn_val.as_bool() {
743                            self.write_keyword(f, if b { "true" } else { "false" })?;
744                        }
745                    }
746                    DynValueKind::Number => {
747                        if let Some(n) = dyn_val.as_i64() {
748                            self.format_number(f, &n.to_string())?;
749                        } else if let Some(n) = dyn_val.as_u64() {
750                            self.format_number(f, &n.to_string())?;
751                        } else if let Some(n) = dyn_val.as_f64() {
752                            self.format_number(f, &n.to_string())?;
753                        }
754                    }
755                    DynValueKind::String => {
756                        if let Some(s) = dyn_val.as_str() {
757                            self.format_string(f, s)?;
758                        }
759                    }
760                    DynValueKind::Bytes => {
761                        if let Some(bytes) = dyn_val.as_bytes() {
762                            self.format_bytes(f, bytes)?;
763                        }
764                    }
765                    DynValueKind::Array => {
766                        let len = dyn_val.array_len().unwrap_or(0);
767                        if len == 0 {
768                            self.write_punctuation(f, "[]")?;
769                        } else {
770                            self.write_punctuation(f, "[")?;
771                            for idx in 0..len {
772                                if !short {
773                                    writeln!(f)?;
774                                    self.indent(f, format_depth + 1)?;
775                                }
776                                if let Some(elem) = dyn_val.array_get(idx) {
777                                    self.format_peek_internal_(
778                                        elem,
779                                        f,
780                                        visited,
781                                        format_depth + 1,
782                                        type_depth + 1,
783                                        short,
784                                    )?;
785                                }
786                                if !short || idx + 1 < len {
787                                    self.write_punctuation(f, ",")?;
788                                } else {
789                                    write!(f, " ")?;
790                                }
791                            }
792                            if !short {
793                                writeln!(f)?;
794                                self.indent(f, format_depth)?;
795                            }
796                            self.write_punctuation(f, "]")?;
797                        }
798                    }
799                    DynValueKind::Object => {
800                        let len = dyn_val.object_len().unwrap_or(0);
801                        if len == 0 {
802                            self.write_punctuation(f, "{}")?;
803                        } else {
804                            self.write_punctuation(f, "{")?;
805                            for idx in 0..len {
806                                if !short {
807                                    writeln!(f)?;
808                                    self.indent(f, format_depth + 1)?;
809                                }
810                                if let Some((key, val)) = dyn_val.object_get_entry(idx) {
811                                    self.write_field_name(f, key)?;
812                                    self.write_punctuation(f, ": ")?;
813                                    self.format_peek_internal_(
814                                        val,
815                                        f,
816                                        visited,
817                                        format_depth + 1,
818                                        type_depth + 1,
819                                        short,
820                                    )?;
821                                }
822                                if !short || idx + 1 < len {
823                                    self.write_punctuation(f, ",")?;
824                                } else {
825                                    write!(f, " ")?;
826                                }
827                            }
828                            if !short {
829                                writeln!(f)?;
830                                self.indent(f, format_depth)?;
831                            }
832                            self.write_punctuation(f, "}")?;
833                        }
834                    }
835                    DynValueKind::DateTime => {
836                        // Format datetime using the vtable's get_datetime
837                        #[allow(clippy::uninlined_format_args)]
838                        if let Some((year, month, day, hour, minute, second, nanos, kind)) =
839                            dyn_val.as_datetime()
840                        {
841                            match kind {
842                                DynDateTimeKind::Offset { offset_minutes } => {
843                                    if nanos > 0 {
844                                        write!(
845                                            f,
846                                            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09}",
847                                            year, month, day, hour, minute, second, nanos
848                                        )?;
849                                    } else {
850                                        write!(
851                                            f,
852                                            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
853                                            year, month, day, hour, minute, second
854                                        )?;
855                                    }
856                                    if offset_minutes == 0 {
857                                        write!(f, "Z")?;
858                                    } else {
859                                        let sign = if offset_minutes >= 0 { '+' } else { '-' };
860                                        let abs = offset_minutes.abs();
861                                        write!(f, "{}{:02}:{:02}", sign, abs / 60, abs % 60)?;
862                                    }
863                                }
864                                DynDateTimeKind::LocalDateTime => {
865                                    if nanos > 0 {
866                                        write!(
867                                            f,
868                                            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09}",
869                                            year, month, day, hour, minute, second, nanos
870                                        )?;
871                                    } else {
872                                        write!(
873                                            f,
874                                            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
875                                            year, month, day, hour, minute, second
876                                        )?;
877                                    }
878                                }
879                                DynDateTimeKind::LocalDate => {
880                                    write!(f, "{:04}-{:02}-{:02}", year, month, day)?;
881                                }
882                                DynDateTimeKind::LocalTime => {
883                                    if nanos > 0 {
884                                        write!(
885                                            f,
886                                            "{:02}:{:02}:{:02}.{:09}",
887                                            hour, minute, second, nanos
888                                        )?;
889                                    } else {
890                                        write!(f, "{:02}:{:02}:{:02}", hour, minute, second)?;
891                                    }
892                                }
893                            }
894                        }
895                    }
896                    DynValueKind::QName => {
897                        // QName formatting is not yet supported via vtable
898                        write!(f, "<qname>")?;
899                    }
900                    DynValueKind::Uuid => {
901                        // UUID formatting is not yet supported via vtable
902                        write!(f, "<uuid>")?;
903                    }
904                }
905            }
906
907            (d, t) => write!(f, "unsupported peek variant: {value:?} ({d:?}, {t:?})")?,
908        }
909
910        visited.remove(&value.id());
911        Ok(())
912    }
913
914    /// Format a value through its proxy type representation.
915    ///
916    /// This allocates memory for the proxy type, converts the value to its proxy
917    /// representation, formats the proxy, then cleans up.
918    #[allow(clippy::too_many_arguments)]
919    fn format_via_proxy(
920        &self,
921        value: Peek<'_, '_>,
922        proxy_def: &'static facet_core::ProxyDef,
923        f: &mut dyn Write,
924        visited: &mut BTreeMap<ValueId, usize>,
925        format_depth: usize,
926        type_depth: usize,
927        short: bool,
928    ) -> fmt::Result {
929        let proxy_shape = proxy_def.shape;
930        let proxy_layout = match proxy_shape.layout.sized_layout() {
931            Ok(layout) => layout,
932            Err(_) => {
933                return write!(f, "/* proxy type must be sized for formatting */");
934            }
935        };
936
937        // Allocate memory for the proxy value
938        let proxy_mem = unsafe { alloc::alloc::alloc(proxy_layout) };
939        if proxy_mem.is_null() {
940            return write!(f, "/* failed to allocate proxy memory */");
941        }
942
943        // Convert target → proxy
944        let proxy_uninit = PtrUninit::new(proxy_mem);
945        let convert_result = unsafe { (proxy_def.convert_out)(value.data(), proxy_uninit) };
946
947        let proxy_ptr = match convert_result {
948            Ok(ptr) => ptr,
949            Err(msg) => {
950                unsafe { alloc::alloc::dealloc(proxy_mem, proxy_layout) };
951                return write!(f, "/* proxy conversion failed: {msg} */");
952            }
953        };
954
955        // Create a Peek to the proxy value and format it
956        let proxy_peek = unsafe { Peek::unchecked_new(proxy_ptr.as_const(), proxy_shape) };
957        let result =
958            self.format_peek_internal_(proxy_peek, f, visited, format_depth, type_depth, short);
959
960        // Clean up: drop the proxy value and deallocate
961        unsafe {
962            let _ = proxy_shape.call_drop_in_place(proxy_ptr);
963            alloc::alloc::dealloc(proxy_mem, proxy_layout);
964        }
965
966        result
967    }
968
969    /// Format a value through its proxy type representation (unified version for FormatOutput).
970    ///
971    /// This allocates memory for the proxy type, converts the value to its proxy
972    /// representation, formats the proxy, then cleans up.
973    #[allow(clippy::too_many_arguments)]
974    fn format_via_proxy_unified<O: FormatOutput>(
975        &self,
976        value: Peek<'_, '_>,
977        proxy_def: &'static facet_core::ProxyDef,
978        out: &mut O,
979        visited: &mut BTreeMap<ValueId, usize>,
980        format_depth: usize,
981        type_depth: usize,
982        short: bool,
983        current_path: Path,
984    ) -> fmt::Result {
985        let proxy_shape = proxy_def.shape;
986        let proxy_layout = match proxy_shape.layout.sized_layout() {
987            Ok(layout) => layout,
988            Err(_) => {
989                return write!(out, "/* proxy type must be sized for formatting */");
990            }
991        };
992
993        // Allocate memory for the proxy value
994        let proxy_mem = unsafe { alloc::alloc::alloc(proxy_layout) };
995        if proxy_mem.is_null() {
996            return write!(out, "/* failed to allocate proxy memory */");
997        }
998
999        // Convert target → proxy
1000        let proxy_uninit = PtrUninit::new(proxy_mem);
1001        let convert_result = unsafe { (proxy_def.convert_out)(value.data(), proxy_uninit) };
1002
1003        let proxy_ptr = match convert_result {
1004            Ok(ptr) => ptr,
1005            Err(msg) => {
1006                unsafe { alloc::alloc::dealloc(proxy_mem, proxy_layout) };
1007                return write!(out, "/* proxy conversion failed: {msg} */");
1008            }
1009        };
1010
1011        // Create a Peek to the proxy value and format it
1012        let proxy_peek = unsafe { Peek::unchecked_new(proxy_ptr.as_const(), proxy_shape) };
1013        let result = self.format_unified(
1014            proxy_peek,
1015            out,
1016            visited,
1017            format_depth,
1018            type_depth,
1019            short,
1020            current_path,
1021        );
1022
1023        // Clean up: drop the proxy value and deallocate
1024        unsafe {
1025            let _ = proxy_shape.call_drop_in_place(proxy_ptr);
1026            alloc::alloc::dealloc(proxy_mem, proxy_layout);
1027        }
1028
1029        result
1030    }
1031
1032    #[allow(clippy::too_many_arguments)]
1033    fn format_tuple_fields<'mem, 'facet>(
1034        &self,
1035        peek_field: &dyn Fn(usize) -> Peek<'mem, 'facet>,
1036        f: &mut dyn Write,
1037        visited: &mut BTreeMap<ValueId, usize>,
1038        format_depth: usize,
1039        type_depth: usize,
1040        fields: &[Field],
1041        short: bool,
1042        force_trailing_comma: bool,
1043    ) -> fmt::Result {
1044        self.write_punctuation(f, "(")?;
1045        if let [field] = fields
1046            && field.doc.is_empty()
1047        {
1048            let field_value = peek_field(0);
1049            if let Some(proxy_def) = field.proxy() {
1050                self.format_via_proxy(
1051                    field_value,
1052                    proxy_def,
1053                    f,
1054                    visited,
1055                    format_depth,
1056                    type_depth,
1057                    short,
1058                )?;
1059            } else {
1060                self.format_peek_internal_(
1061                    field_value,
1062                    f,
1063                    visited,
1064                    format_depth,
1065                    type_depth,
1066                    short,
1067                )?;
1068            }
1069
1070            if force_trailing_comma {
1071                self.write_punctuation(f, ",")?;
1072            }
1073        } else if !fields.is_empty() {
1074            for idx in 0..fields.len() {
1075                if !short {
1076                    writeln!(f)?;
1077                    self.indent(f, format_depth + 1)?;
1078
1079                    if self.show_doc_comments {
1080                        for &line in fields[idx].doc {
1081                            self.write_comment(f, &format!("///{line}"))?;
1082                            writeln!(f)?;
1083                            self.indent(f, format_depth + 1)?;
1084                        }
1085                    }
1086                }
1087
1088                if fields[idx].is_sensitive() {
1089                    self.write_redacted(f, "[REDACTED]")?;
1090                } else if let Some(proxy_def) = fields[idx].proxy() {
1091                    // Field-level proxy: format through the proxy type
1092                    self.format_via_proxy(
1093                        peek_field(idx),
1094                        proxy_def,
1095                        f,
1096                        visited,
1097                        format_depth + 1,
1098                        type_depth + 1,
1099                        short,
1100                    )?;
1101                } else {
1102                    self.format_peek_internal_(
1103                        peek_field(idx),
1104                        f,
1105                        visited,
1106                        format_depth + 1,
1107                        type_depth + 1,
1108                        short,
1109                    )?;
1110                }
1111
1112                if !short || idx + 1 < fields.len() {
1113                    self.write_punctuation(f, ",")?;
1114                } else {
1115                    write!(f, " ")?;
1116                }
1117            }
1118            if !short {
1119                writeln!(f)?;
1120                self.indent(f, format_depth)?;
1121            }
1122        }
1123        self.write_punctuation(f, ")")?;
1124        Ok(())
1125    }
1126
1127    #[allow(clippy::too_many_arguments)]
1128    fn format_struct_fields<'mem, 'facet>(
1129        &self,
1130        peek_field: &dyn Fn(usize) -> Peek<'mem, 'facet>,
1131        f: &mut dyn Write,
1132        visited: &mut BTreeMap<ValueId, usize>,
1133        format_depth: usize,
1134        type_depth: usize,
1135        fields: &[Field],
1136        short: bool,
1137    ) -> fmt::Result {
1138        // First, determine which fields will be printed (not skipped)
1139        let visible_indices: Vec<usize> = (0..fields.len())
1140            .filter(|&idx| {
1141                let field = &fields[idx];
1142                // SAFETY: peek_field returns a valid Peek with valid data pointer
1143                let field_ptr = peek_field(idx).data();
1144                !unsafe { field.should_skip_serializing(field_ptr) }
1145            })
1146            .collect();
1147
1148        self.write_punctuation(f, " {")?;
1149        if !visible_indices.is_empty() {
1150            for (i, &idx) in visible_indices.iter().enumerate() {
1151                let is_last = i + 1 == visible_indices.len();
1152
1153                if !short {
1154                    writeln!(f)?;
1155                    self.indent(f, format_depth + 1)?;
1156                }
1157
1158                if self.show_doc_comments {
1159                    for &line in fields[idx].doc {
1160                        self.write_comment(f, &format!("///{line}"))?;
1161                        writeln!(f)?;
1162                        self.indent(f, format_depth + 1)?;
1163                    }
1164                }
1165
1166                self.write_field_name(f, fields[idx].name)?;
1167                self.write_punctuation(f, ": ")?;
1168                if fields[idx].is_sensitive() {
1169                    self.write_redacted(f, "[REDACTED]")?;
1170                } else if let Some(proxy_def) = fields[idx].proxy() {
1171                    // Field-level proxy: format through the proxy type
1172                    self.format_via_proxy(
1173                        peek_field(idx),
1174                        proxy_def,
1175                        f,
1176                        visited,
1177                        format_depth + 1,
1178                        type_depth + 1,
1179                        short,
1180                    )?;
1181                } else {
1182                    self.format_peek_internal_(
1183                        peek_field(idx),
1184                        f,
1185                        visited,
1186                        format_depth + 1,
1187                        type_depth + 1,
1188                        short,
1189                    )?;
1190                }
1191
1192                if !short || !is_last {
1193                    self.write_punctuation(f, ",")?;
1194                } else {
1195                    write!(f, " ")?;
1196                }
1197            }
1198            if !short {
1199                writeln!(f)?;
1200                self.indent(f, format_depth)?;
1201            }
1202        }
1203        self.write_punctuation(f, "}")?;
1204        Ok(())
1205    }
1206
1207    fn indent(&self, f: &mut dyn Write, indent: usize) -> fmt::Result {
1208        if self.indent_size == usize::MAX {
1209            write!(f, "{:\t<width$}", "", width = indent)
1210        } else {
1211            write!(f, "{: <width$}", "", width = indent * self.indent_size)
1212        }
1213    }
1214
1215    /// Internal method to format a Peek value
1216    pub(crate) fn format_peek_internal(
1217        &self,
1218        value: Peek<'_, '_>,
1219        f: &mut dyn Write,
1220        visited: &mut BTreeMap<ValueId, usize>,
1221    ) -> fmt::Result {
1222        self.format_peek_internal_(value, f, visited, 0, 0, false)
1223    }
1224
1225    /// Format a scalar value
1226    fn format_scalar(&self, value: Peek, f: &mut dyn Write) -> fmt::Result {
1227        // Generate a color for this shape
1228        let mut hasher = DefaultHasher::new();
1229        value.shape().id.hash(&mut hasher);
1230        let hash = hasher.finish();
1231        let color = self.color_generator.generate_color(hash);
1232
1233        // Display the value
1234        struct DisplayWrapper<'mem, 'facet>(&'mem Peek<'mem, 'facet>);
1235
1236        impl fmt::Display for DisplayWrapper<'_, '_> {
1237            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1238                if self.0.shape().is_display() {
1239                    write!(f, "{}", self.0)?;
1240                } else if self.0.shape().is_debug() {
1241                    write!(f, "{:?}", self.0)?;
1242                } else {
1243                    write!(f, "{}", self.0.shape())?;
1244                    write!(f, "(…)")?;
1245                }
1246                Ok(())
1247            }
1248        }
1249
1250        // Apply color if needed and display
1251        if self.use_colors() {
1252            let rgb = Rgb(color.r, color.g, color.b);
1253            write!(f, "{}", DisplayWrapper(&value).color(rgb))?;
1254        } else {
1255            write!(f, "{}", DisplayWrapper(&value))?;
1256        }
1257
1258        Ok(())
1259    }
1260
1261    /// Write a keyword (null, true, false) with coloring
1262    fn write_keyword(&self, f: &mut dyn Write, keyword: &str) -> fmt::Result {
1263        if self.use_colors() {
1264            write!(f, "{}", keyword.color(tokyo_night::KEYWORD))
1265        } else {
1266            write!(f, "{keyword}")
1267        }
1268    }
1269
1270    /// Format a number for dynamic values
1271    fn format_number(&self, f: &mut dyn Write, s: &str) -> fmt::Result {
1272        if self.use_colors() {
1273            write!(f, "{}", s.color(tokyo_night::NUMBER))
1274        } else {
1275            write!(f, "{s}")
1276        }
1277    }
1278
1279    /// Format a &str or String value with optional truncation and raw string handling
1280    fn format_str_value(&self, f: &mut dyn Write, value: &str) -> fmt::Result {
1281        // Check if truncation is needed
1282        if let Some(max) = self.max_content_len
1283            && value.len() > max
1284        {
1285            return self.format_truncated_str(f, value, max);
1286        }
1287
1288        // Normal formatting with raw string handling for quotes
1289        let mut hashes = 0usize;
1290        let mut rest = value;
1291        while let Some(idx) = rest.find('"') {
1292            rest = &rest[idx + 1..];
1293            let before = rest.len();
1294            rest = rest.trim_start_matches('#');
1295            let after = rest.len();
1296            let count = before - after;
1297            hashes = Ord::max(hashes, 1 + count);
1298        }
1299
1300        let pad = "";
1301        let width = hashes.saturating_sub(1);
1302        if hashes > 0 {
1303            write!(f, "r{pad:#<width$}")?;
1304        }
1305        write!(f, "\"")?;
1306        if self.use_colors() {
1307            write!(f, "{}", value.color(tokyo_night::STRING))?;
1308        } else {
1309            write!(f, "{value}")?;
1310        }
1311        write!(f, "\"")?;
1312        if hashes > 0 {
1313            write!(f, "{pad:#<width$}")?;
1314        }
1315        Ok(())
1316    }
1317
1318    /// Format a truncated string showing beginning...end
1319    fn format_truncated_str(&self, f: &mut dyn Write, s: &str, max: usize) -> fmt::Result {
1320        let half = max / 2;
1321
1322        // Find char boundary for start portion
1323        let start_end = s
1324            .char_indices()
1325            .take_while(|(i, _)| *i < half)
1326            .last()
1327            .map(|(i, c)| i + c.len_utf8())
1328            .unwrap_or(0);
1329
1330        // Find char boundary for end portion
1331        let end_start = s
1332            .char_indices()
1333            .rev()
1334            .take_while(|(i, _)| s.len() - *i <= half)
1335            .last()
1336            .map(|(i, _)| i)
1337            .unwrap_or(s.len());
1338
1339        let omitted = s[start_end..end_start].chars().count();
1340        let start_part = &s[..start_end];
1341        let end_part = &s[end_start..];
1342
1343        if self.use_colors() {
1344            write!(
1345                f,
1346                "\"{}\"...({omitted} chars)...\"{}\"",
1347                start_part.color(tokyo_night::STRING),
1348                end_part.color(tokyo_night::STRING)
1349            )
1350        } else {
1351            write!(f, "\"{start_part}\"...({omitted} chars)...\"{end_part}\"")
1352        }
1353    }
1354
1355    /// Format a string for dynamic values (uses debug escaping for special chars)
1356    fn format_string(&self, f: &mut dyn Write, s: &str) -> fmt::Result {
1357        if let Some(max) = self.max_content_len
1358            && s.len() > max
1359        {
1360            return self.format_truncated_str(f, s, max);
1361        }
1362
1363        if self.use_colors() {
1364            write!(f, "\"{}\"", s.color(tokyo_night::STRING))
1365        } else {
1366            write!(f, "{s:?}")
1367        }
1368    }
1369
1370    /// Format bytes for dynamic values
1371    fn format_bytes(&self, f: &mut dyn Write, bytes: &[u8]) -> fmt::Result {
1372        write!(f, "b\"")?;
1373
1374        match self.max_content_len {
1375            Some(max) if bytes.len() > max => {
1376                // Show beginning ... end
1377                let half = max / 2;
1378                let start = half;
1379                let end = half;
1380
1381                for byte in &bytes[..start] {
1382                    write!(f, "\\x{byte:02x}")?;
1383                }
1384                let omitted = bytes.len() - start - end;
1385                write!(f, "\"...({omitted} bytes)...b\"")?;
1386                for byte in &bytes[bytes.len() - end..] {
1387                    write!(f, "\\x{byte:02x}")?;
1388                }
1389            }
1390            _ => {
1391                for byte in bytes {
1392                    write!(f, "\\x{byte:02x}")?;
1393                }
1394            }
1395        }
1396
1397        write!(f, "\"")
1398    }
1399
1400    /// Write styled type name to formatter
1401    fn write_type_name(&self, f: &mut dyn Write, peek: &Peek) -> fmt::Result {
1402        struct TypeNameWriter<'mem, 'facet>(&'mem Peek<'mem, 'facet>);
1403
1404        impl core::fmt::Display for TypeNameWriter<'_, '_> {
1405            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1406                self.0.type_name(f, TypeNameOpts::infinite())
1407            }
1408        }
1409        let type_name = TypeNameWriter(peek);
1410
1411        if self.use_colors() {
1412            write!(f, "{}", type_name.color(tokyo_night::TYPE_NAME).bold())
1413        } else {
1414            write!(f, "{type_name}")
1415        }
1416    }
1417
1418    /// Style a type name and return it as a string
1419    #[allow(dead_code)]
1420    fn style_type_name(&self, peek: &Peek) -> String {
1421        let mut result = String::new();
1422        self.write_type_name(&mut result, peek).unwrap();
1423        result
1424    }
1425
1426    /// Write styled field name to formatter
1427    fn write_field_name(&self, f: &mut dyn Write, name: &str) -> fmt::Result {
1428        if self.use_colors() {
1429            write!(f, "{}", name.color(tokyo_night::FIELD_NAME))
1430        } else {
1431            write!(f, "{name}")
1432        }
1433    }
1434
1435    /// Write styled punctuation to formatter
1436    fn write_punctuation(&self, f: &mut dyn Write, text: &str) -> fmt::Result {
1437        if self.use_colors() {
1438            write!(f, "{}", text.dimmed())
1439        } else {
1440            write!(f, "{text}")
1441        }
1442    }
1443
1444    /// Write styled comment to formatter
1445    fn write_comment(&self, f: &mut dyn Write, text: &str) -> fmt::Result {
1446        if self.use_colors() {
1447            write!(f, "{}", text.color(tokyo_night::MUTED))
1448        } else {
1449            write!(f, "{text}")
1450        }
1451    }
1452
1453    /// Write styled redacted value to formatter
1454    fn write_redacted(&self, f: &mut dyn Write, text: &str) -> fmt::Result {
1455        if self.use_colors() {
1456            write!(f, "{}", text.color(tokyo_night::ERROR).bold())
1457        } else {
1458            write!(f, "{text}")
1459        }
1460    }
1461
1462    /// Style a redacted value and return it as a string
1463    #[allow(dead_code)]
1464    fn style_redacted(&self, text: &str) -> String {
1465        let mut result = String::new();
1466        self.write_redacted(&mut result, text).unwrap();
1467        result
1468    }
1469
1470    /// Format a value with span tracking for each path.
1471    ///
1472    /// Returns a `FormattedValue` containing the plain text output and a map
1473    /// from paths to their byte spans in the output.
1474    ///
1475    /// This is useful for creating rich diagnostics that can highlight specific
1476    /// parts of a pretty-printed value.
1477    pub fn format_peek_with_spans(&self, value: Peek<'_, '_>) -> FormattedValue {
1478        let mut output = SpanTrackingOutput::new();
1479        let printer = Self {
1480            colors: ColorMode::Never, // Always disable colors for span tracking
1481            indent_size: self.indent_size,
1482            max_depth: self.max_depth,
1483            color_generator: self.color_generator.clone(),
1484            list_u8_as_bytes: self.list_u8_as_bytes,
1485            minimal_option_names: self.minimal_option_names,
1486            show_doc_comments: self.show_doc_comments,
1487            max_content_len: self.max_content_len,
1488        };
1489        printer
1490            .format_unified(
1491                value,
1492                &mut output,
1493                &mut BTreeMap::new(),
1494                0,
1495                0,
1496                false,
1497                vec![],
1498            )
1499            .expect("Formatting failed");
1500
1501        output.into_formatted_value()
1502    }
1503
1504    /// Unified formatting implementation that works with any FormatOutput.
1505    ///
1506    /// This is the core implementation - both `format_peek` and `format_peek_with_spans`
1507    /// use this internally with different output types.
1508    #[allow(clippy::too_many_arguments)]
1509    fn format_unified<O: FormatOutput>(
1510        &self,
1511        value: Peek<'_, '_>,
1512        out: &mut O,
1513        visited: &mut BTreeMap<ValueId, usize>,
1514        format_depth: usize,
1515        type_depth: usize,
1516        short: bool,
1517        current_path: Path,
1518    ) -> fmt::Result {
1519        let mut value = value;
1520        while let Ok(ptr) = value.into_pointer()
1521            && let Some(pointee) = ptr.borrow_inner()
1522        {
1523            value = pointee;
1524        }
1525
1526        // Unwrap transparent wrappers (e.g., newtype wrappers like IntAsString(String))
1527        // This matches serialization behavior where we serialize the inner value directly
1528        let value = value.innermost_peek();
1529        let shape = value.shape();
1530
1531        // Record the start of this value
1532        let value_start = out.position();
1533
1534        if let Some(prev_type_depth) = visited.insert(value.id(), type_depth) {
1535            write!(out, "{} {{ ", shape.type_identifier)?;
1536            write!(
1537                out,
1538                "/* cycle detected at {} (first seen at type_depth {}) */",
1539                value.id(),
1540                prev_type_depth,
1541            )?;
1542            visited.remove(&value.id());
1543            let value_end = out.position();
1544            out.record_span(current_path, (value_start, value_end));
1545            return Ok(());
1546        }
1547
1548        // Handle proxy types by converting to the proxy representation and formatting that
1549        if let Some(proxy_def) = shape.proxy {
1550            let result = self.format_via_proxy_unified(
1551                value,
1552                proxy_def,
1553                out,
1554                visited,
1555                format_depth,
1556                type_depth,
1557                short,
1558                current_path.clone(),
1559            );
1560
1561            visited.remove(&value.id());
1562
1563            // Record span for this value
1564            let value_end = out.position();
1565            out.record_span(current_path, (value_start, value_end));
1566
1567            return result;
1568        }
1569
1570        match (shape.def, shape.ty) {
1571            (_, Type::Primitive(PrimitiveType::Textual(TextualType::Str))) => {
1572                let s = value.get::<str>().unwrap();
1573                write!(out, "\"{}\"", s)?;
1574            }
1575            (Def::Scalar, _) if value.shape().id == <alloc::string::String as Facet>::SHAPE.id => {
1576                let s = value.get::<alloc::string::String>().unwrap();
1577                write!(out, "\"{}\"", s)?;
1578            }
1579            (Def::Scalar, _) => {
1580                self.format_scalar_to_output(value, out)?;
1581            }
1582            (Def::Option(_), _) => {
1583                let option = value.into_option().unwrap();
1584                if let Some(inner) = option.value() {
1585                    write!(out, "Some(")?;
1586                    self.format_unified(
1587                        inner,
1588                        out,
1589                        visited,
1590                        format_depth,
1591                        type_depth + 1,
1592                        short,
1593                        current_path.clone(),
1594                    )?;
1595                    write!(out, ")")?;
1596                } else {
1597                    write!(out, "None")?;
1598                }
1599            }
1600            (
1601                _,
1602                Type::User(UserType::Struct(
1603                    ty @ StructType {
1604                        kind: StructKind::Struct | StructKind::Unit,
1605                        ..
1606                    },
1607                )),
1608            ) => {
1609                write!(out, "{}", shape.type_identifier)?;
1610                if matches!(ty.kind, StructKind::Struct) {
1611                    let struct_peek = value.into_struct().unwrap();
1612                    write!(out, " {{")?;
1613                    for (i, field) in ty.fields.iter().enumerate() {
1614                        if !short {
1615                            writeln!(out)?;
1616                            self.indent_to_output(out, format_depth + 1)?;
1617                        }
1618                        // Record field name span
1619                        let field_name_start = out.position();
1620                        write!(out, "{}", field.name)?;
1621                        let field_name_end = out.position();
1622                        write!(out, ": ")?;
1623
1624                        // Build path for this field
1625                        let mut field_path = current_path.clone();
1626                        field_path.push(PathSegment::Field(Cow::Borrowed(field.name)));
1627
1628                        // Record field value span
1629                        let field_value_start = out.position();
1630                        if let Ok(field_value) = struct_peek.field(i) {
1631                            // Check for field-level proxy
1632                            if let Some(proxy_def) = field.proxy() {
1633                                self.format_via_proxy_unified(
1634                                    field_value,
1635                                    proxy_def,
1636                                    out,
1637                                    visited,
1638                                    format_depth + 1,
1639                                    type_depth + 1,
1640                                    short,
1641                                    field_path.clone(),
1642                                )?;
1643                            } else {
1644                                self.format_unified(
1645                                    field_value,
1646                                    out,
1647                                    visited,
1648                                    format_depth + 1,
1649                                    type_depth + 1,
1650                                    short,
1651                                    field_path.clone(),
1652                                )?;
1653                            }
1654                        }
1655                        let field_value_end = out.position();
1656
1657                        // Record span for this field
1658                        out.record_field_span(
1659                            field_path,
1660                            (field_name_start, field_name_end),
1661                            (field_value_start, field_value_end),
1662                        );
1663
1664                        if !short || i + 1 < ty.fields.len() {
1665                            write!(out, ",")?;
1666                        }
1667                    }
1668                    if !short {
1669                        writeln!(out)?;
1670                        self.indent_to_output(out, format_depth)?;
1671                    }
1672                    write!(out, "}}")?;
1673                }
1674            }
1675            (
1676                _,
1677                Type::User(UserType::Struct(
1678                    ty @ StructType {
1679                        kind: StructKind::Tuple | StructKind::TupleStruct,
1680                        ..
1681                    },
1682                )),
1683            ) => {
1684                write!(out, "{}", shape.type_identifier)?;
1685                if matches!(ty.kind, StructKind::Tuple) {
1686                    write!(out, " ")?;
1687                }
1688                let struct_peek = value.into_struct().unwrap();
1689                write!(out, "(")?;
1690                for (i, field) in ty.fields.iter().enumerate() {
1691                    if i > 0 {
1692                        write!(out, ", ")?;
1693                    }
1694                    let mut elem_path = current_path.clone();
1695                    elem_path.push(PathSegment::Index(i));
1696
1697                    let elem_start = out.position();
1698                    if let Ok(field_value) = struct_peek.field(i) {
1699                        // Check for field-level proxy
1700                        if let Some(proxy_def) = field.proxy() {
1701                            self.format_via_proxy_unified(
1702                                field_value,
1703                                proxy_def,
1704                                out,
1705                                visited,
1706                                format_depth + 1,
1707                                type_depth + 1,
1708                                short,
1709                                elem_path.clone(),
1710                            )?;
1711                        } else {
1712                            self.format_unified(
1713                                field_value,
1714                                out,
1715                                visited,
1716                                format_depth + 1,
1717                                type_depth + 1,
1718                                short,
1719                                elem_path.clone(),
1720                            )?;
1721                        }
1722                    }
1723                    let elem_end = out.position();
1724                    out.record_span(elem_path, (elem_start, elem_end));
1725                }
1726                write!(out, ")")?;
1727            }
1728            (_, Type::User(UserType::Enum(_))) => {
1729                let enum_peek = value.into_enum().unwrap();
1730                match enum_peek.active_variant() {
1731                    Err(_) => {
1732                        write!(
1733                            out,
1734                            "{} {{ /* cannot determine variant */ }}",
1735                            shape.type_identifier
1736                        )?;
1737                    }
1738                    Ok(variant) => {
1739                        write!(out, "{}::{}", shape.type_identifier, variant.name)?;
1740
1741                        match variant.data.kind {
1742                            StructKind::Unit => {}
1743                            StructKind::Struct => {
1744                                write!(out, " {{")?;
1745                                for (i, field) in variant.data.fields.iter().enumerate() {
1746                                    if !short {
1747                                        writeln!(out)?;
1748                                        self.indent_to_output(out, format_depth + 1)?;
1749                                    }
1750                                    let field_name_start = out.position();
1751                                    write!(out, "{}", field.name)?;
1752                                    let field_name_end = out.position();
1753                                    write!(out, ": ")?;
1754
1755                                    let mut field_path = current_path.clone();
1756                                    field_path
1757                                        .push(PathSegment::Variant(Cow::Borrowed(variant.name)));
1758                                    field_path.push(PathSegment::Field(Cow::Borrowed(field.name)));
1759
1760                                    let field_value_start = out.position();
1761                                    if let Ok(Some(field_value)) = enum_peek.field(i) {
1762                                        // Check for field-level proxy
1763                                        if let Some(proxy_def) = field.proxy() {
1764                                            self.format_via_proxy_unified(
1765                                                field_value,
1766                                                proxy_def,
1767                                                out,
1768                                                visited,
1769                                                format_depth + 1,
1770                                                type_depth + 1,
1771                                                short,
1772                                                field_path.clone(),
1773                                            )?;
1774                                        } else {
1775                                            self.format_unified(
1776                                                field_value,
1777                                                out,
1778                                                visited,
1779                                                format_depth + 1,
1780                                                type_depth + 1,
1781                                                short,
1782                                                field_path.clone(),
1783                                            )?;
1784                                        }
1785                                    }
1786                                    let field_value_end = out.position();
1787
1788                                    out.record_field_span(
1789                                        field_path,
1790                                        (field_name_start, field_name_end),
1791                                        (field_value_start, field_value_end),
1792                                    );
1793
1794                                    if !short || i + 1 < variant.data.fields.len() {
1795                                        write!(out, ",")?;
1796                                    }
1797                                }
1798                                if !short {
1799                                    writeln!(out)?;
1800                                    self.indent_to_output(out, format_depth)?;
1801                                }
1802                                write!(out, "}}")?;
1803                            }
1804                            _ => {
1805                                write!(out, "(")?;
1806                                for (i, field) in variant.data.fields.iter().enumerate() {
1807                                    if i > 0 {
1808                                        write!(out, ", ")?;
1809                                    }
1810                                    let mut elem_path = current_path.clone();
1811                                    elem_path
1812                                        .push(PathSegment::Variant(Cow::Borrowed(variant.name)));
1813                                    elem_path.push(PathSegment::Index(i));
1814
1815                                    let elem_start = out.position();
1816                                    if let Ok(Some(field_value)) = enum_peek.field(i) {
1817                                        // Check for field-level proxy
1818                                        if let Some(proxy_def) = field.proxy() {
1819                                            self.format_via_proxy_unified(
1820                                                field_value,
1821                                                proxy_def,
1822                                                out,
1823                                                visited,
1824                                                format_depth + 1,
1825                                                type_depth + 1,
1826                                                short,
1827                                                elem_path.clone(),
1828                                            )?;
1829                                        } else {
1830                                            self.format_unified(
1831                                                field_value,
1832                                                out,
1833                                                visited,
1834                                                format_depth + 1,
1835                                                type_depth + 1,
1836                                                short,
1837                                                elem_path.clone(),
1838                                            )?;
1839                                        }
1840                                    }
1841                                    let elem_end = out.position();
1842                                    out.record_span(elem_path, (elem_start, elem_end));
1843                                }
1844                                write!(out, ")")?;
1845                            }
1846                        }
1847                    }
1848                }
1849            }
1850            _ if value.into_list_like().is_ok() => {
1851                let list = value.into_list_like().unwrap();
1852
1853                // Check if elements are simple scalars - render inline if so
1854                let elem_shape = list.def().t();
1855                let is_simple = Self::shape_chunkiness(elem_shape) <= 1;
1856
1857                write!(out, "[")?;
1858                let len = list.len();
1859                for (i, item) in list.iter().enumerate() {
1860                    if !short && !is_simple {
1861                        writeln!(out)?;
1862                        self.indent_to_output(out, format_depth + 1)?;
1863                    } else if i > 0 {
1864                        write!(out, " ")?;
1865                    }
1866                    let mut elem_path = current_path.clone();
1867                    elem_path.push(PathSegment::Index(i));
1868
1869                    let elem_start = out.position();
1870                    self.format_unified(
1871                        item,
1872                        out,
1873                        visited,
1874                        format_depth + 1,
1875                        type_depth + 1,
1876                        short || is_simple,
1877                        elem_path.clone(),
1878                    )?;
1879                    let elem_end = out.position();
1880                    out.record_span(elem_path, (elem_start, elem_end));
1881
1882                    if (!short && !is_simple) || i + 1 < len {
1883                        write!(out, ",")?;
1884                    }
1885                }
1886                if !short && !is_simple {
1887                    writeln!(out)?;
1888                    self.indent_to_output(out, format_depth)?;
1889                }
1890                write!(out, "]")?;
1891            }
1892            _ if value.into_map().is_ok() => {
1893                let map = value.into_map().unwrap();
1894                write!(out, "{{")?;
1895                for (i, (key, val)) in map.iter().enumerate() {
1896                    if !short {
1897                        writeln!(out)?;
1898                        self.indent_to_output(out, format_depth + 1)?;
1899                    }
1900                    // Format key
1901                    let key_start = out.position();
1902                    self.format_unified(
1903                        key,
1904                        out,
1905                        visited,
1906                        format_depth + 1,
1907                        type_depth + 1,
1908                        true, // short for keys
1909                        vec![],
1910                    )?;
1911                    let key_end = out.position();
1912
1913                    write!(out, ": ")?;
1914
1915                    // Build path for this entry (use key's string representation)
1916                    let key_str = self.format_peek(key);
1917                    let mut entry_path = current_path.clone();
1918                    entry_path.push(PathSegment::Key(Cow::Owned(key_str)));
1919
1920                    let val_start = out.position();
1921                    self.format_unified(
1922                        val,
1923                        out,
1924                        visited,
1925                        format_depth + 1,
1926                        type_depth + 1,
1927                        short,
1928                        entry_path.clone(),
1929                    )?;
1930                    let val_end = out.position();
1931
1932                    out.record_field_span(entry_path, (key_start, key_end), (val_start, val_end));
1933
1934                    if !short || i + 1 < map.len() {
1935                        write!(out, ",")?;
1936                    }
1937                }
1938                if !short && !map.is_empty() {
1939                    writeln!(out)?;
1940                    self.indent_to_output(out, format_depth)?;
1941                }
1942                write!(out, "}}")?;
1943            }
1944            _ => {
1945                // Fallback: just write the type name
1946                write!(out, "{} {{ ... }}", shape.type_identifier)?;
1947            }
1948        }
1949
1950        visited.remove(&value.id());
1951
1952        // Record span for this value
1953        let value_end = out.position();
1954        out.record_span(current_path, (value_start, value_end));
1955
1956        Ok(())
1957    }
1958
1959    fn format_scalar_to_output(&self, value: Peek<'_, '_>, out: &mut impl Write) -> fmt::Result {
1960        // Use Display or Debug trait to format scalar values
1961        if value.shape().is_display() {
1962            write!(out, "{}", value)
1963        } else if value.shape().is_debug() {
1964            write!(out, "{:?}", value)
1965        } else {
1966            write!(out, "{}(…)", value.shape())
1967        }
1968    }
1969
1970    fn indent_to_output(&self, out: &mut impl Write, depth: usize) -> fmt::Result {
1971        for _ in 0..depth {
1972            for _ in 0..self.indent_size {
1973                out.write_char(' ')?;
1974            }
1975        }
1976        Ok(())
1977    }
1978}
1979
1980/// Color mode for the pretty printer.
1981#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1982pub enum ColorMode {
1983    /// Automtically detect whether colors are desired through the `NO_COLOR` environment variable.
1984    Auto,
1985    /// Always enable colors.
1986    Always,
1987    /// Never enable colors.
1988    Never,
1989}
1990
1991impl ColorMode {
1992    /// Convert the color mode to an option of a boolean.
1993    pub fn enabled(&self) -> bool {
1994        static NO_COLOR: LazyLock<bool> = LazyLock::new(|| std::env::var_os("NO_COLOR").is_some());
1995        match self {
1996            ColorMode::Auto => !*NO_COLOR,
1997            ColorMode::Always => true,
1998            ColorMode::Never => false,
1999        }
2000    }
2001}
2002
2003impl From<bool> for ColorMode {
2004    fn from(value: bool) -> Self {
2005        if value {
2006            ColorMode::Always
2007        } else {
2008            ColorMode::Never
2009        }
2010    }
2011}
2012
2013impl From<ColorMode> for Option<bool> {
2014    fn from(value: ColorMode) -> Self {
2015        match value {
2016            ColorMode::Auto => None,
2017            ColorMode::Always => Some(true),
2018            ColorMode::Never => Some(false),
2019        }
2020    }
2021}
2022
2023/// Result of formatting a value with span tracking
2024#[derive(Debug)]
2025pub struct FormattedValue {
2026    /// The formatted text (plain text, no ANSI colors)
2027    pub text: String,
2028    /// Map from paths to their byte spans in `text`
2029    pub spans: BTreeMap<Path, FieldSpan>,
2030}
2031
2032/// Trait for output destinations that may optionally track spans.
2033///
2034/// This allows a single formatting implementation to work with both
2035/// simple string output and span-tracking output.
2036trait FormatOutput: Write {
2037    /// Get the current byte position in the output (for span tracking)
2038    fn position(&self) -> usize;
2039
2040    /// Record a span for a path (value only, key=value)
2041    fn record_span(&mut self, _path: Path, _span: Span) {}
2042
2043    /// Record a span with separate key and value spans
2044    fn record_field_span(&mut self, _path: Path, _key_span: Span, _value_span: Span) {}
2045}
2046
2047/// A wrapper around any Write that implements FormatOutput but doesn't track spans.
2048/// Position tracking is approximated by counting bytes written.
2049#[allow(dead_code)]
2050struct NonTrackingOutput<W> {
2051    inner: W,
2052    position: usize,
2053}
2054
2055#[allow(dead_code)]
2056impl<W> NonTrackingOutput<W> {
2057    const fn new(inner: W) -> Self {
2058        Self { inner, position: 0 }
2059    }
2060}
2061
2062impl<W: Write> Write for NonTrackingOutput<W> {
2063    fn write_str(&mut self, s: &str) -> fmt::Result {
2064        self.position += s.len();
2065        self.inner.write_str(s)
2066    }
2067}
2068
2069impl<W: Write> FormatOutput for NonTrackingOutput<W> {
2070    fn position(&self) -> usize {
2071        self.position
2072    }
2073    // Uses default no-op implementations for span recording
2074}
2075
2076/// Context for tracking spans during value formatting
2077struct SpanTrackingOutput {
2078    output: String,
2079    spans: BTreeMap<Path, FieldSpan>,
2080}
2081
2082impl SpanTrackingOutput {
2083    const fn new() -> Self {
2084        Self {
2085            output: String::new(),
2086            spans: BTreeMap::new(),
2087        }
2088    }
2089
2090    fn into_formatted_value(self) -> FormattedValue {
2091        FormattedValue {
2092            text: self.output,
2093            spans: self.spans,
2094        }
2095    }
2096}
2097
2098impl Write for SpanTrackingOutput {
2099    fn write_str(&mut self, s: &str) -> fmt::Result {
2100        self.output.push_str(s);
2101        Ok(())
2102    }
2103}
2104
2105impl FormatOutput for SpanTrackingOutput {
2106    fn position(&self) -> usize {
2107        self.output.len()
2108    }
2109
2110    fn record_span(&mut self, path: Path, span: Span) {
2111        self.spans.insert(
2112            path,
2113            FieldSpan {
2114                key: span,
2115                value: span,
2116            },
2117        );
2118    }
2119
2120    fn record_field_span(&mut self, path: Path, key_span: Span, value_span: Span) {
2121        self.spans.insert(
2122            path,
2123            FieldSpan {
2124                key: key_span,
2125                value: value_span,
2126            },
2127        );
2128    }
2129}
2130
2131#[cfg(test)]
2132mod tests {
2133    use super::*;
2134
2135    // Basic tests for the PrettyPrinter
2136    #[test]
2137    fn test_pretty_printer_default() {
2138        let printer = PrettyPrinter::default();
2139        assert_eq!(printer.indent_size, 2);
2140        assert_eq!(printer.max_depth, None);
2141        // use_colors defaults to true unless NO_COLOR is set
2142        // In tests, NO_COLOR=1 is set via nextest config for consistent snapshots
2143        assert_eq!(printer.use_colors(), std::env::var_os("NO_COLOR").is_none());
2144    }
2145
2146    #[test]
2147    fn test_pretty_printer_with_methods() {
2148        let printer = PrettyPrinter::new()
2149            .with_indent_size(4)
2150            .with_max_depth(3)
2151            .with_colors(ColorMode::Never);
2152
2153        assert_eq!(printer.indent_size, 4);
2154        assert_eq!(printer.max_depth, Some(3));
2155        assert!(!printer.use_colors());
2156    }
2157
2158    #[test]
2159    fn test_format_peek_with_spans() {
2160        use crate::PathSegment;
2161        use facet_reflect::Peek;
2162
2163        // Test with a simple tuple - no need for custom struct
2164        let value = ("Alice", 30u32);
2165
2166        let printer = PrettyPrinter::new();
2167        let formatted = printer.format_peek_with_spans(Peek::new(&value));
2168
2169        // Check that we got output
2170        assert!(!formatted.text.is_empty());
2171        assert!(formatted.text.contains("Alice"));
2172        assert!(formatted.text.contains("30"));
2173
2174        // Check that spans were recorded
2175        assert!(!formatted.spans.is_empty());
2176
2177        // Check that the root span exists (empty path)
2178        assert!(formatted.spans.contains_key(&vec![]));
2179
2180        // Check that index spans exist
2181        let idx0_path = vec![PathSegment::Index(0)];
2182        let idx1_path = vec![PathSegment::Index(1)];
2183        assert!(
2184            formatted.spans.contains_key(&idx0_path),
2185            "index 0 span not found"
2186        );
2187        assert!(
2188            formatted.spans.contains_key(&idx1_path),
2189            "index 1 span not found"
2190        );
2191    }
2192
2193    #[test]
2194    fn test_max_content_len_string() {
2195        let printer = PrettyPrinter::new()
2196            .with_colors(ColorMode::Never)
2197            .with_max_content_len(20);
2198
2199        // Short string - no truncation
2200        let short = "hello";
2201        let output = printer.format(&short);
2202        assert_eq!(output, "\"hello\"");
2203
2204        // Long string - should truncate middle
2205        let long = "abcdefghijklmnopqrstuvwxyz0123456789";
2206        let output = printer.format(&long);
2207        assert!(output.contains("..."), "should contain ellipsis: {}", output);
2208        assert!(output.contains("chars"), "should mention chars: {}", output);
2209        assert!(output.starts_with("\"abc"), "should start with beginning: {}", output);
2210        assert!(output.ends_with("89\""), "should end with ending: {}", output);
2211    }
2212
2213    #[test]
2214    fn test_max_content_len_bytes() {
2215        let printer = PrettyPrinter::new()
2216            .with_colors(ColorMode::Never)
2217            .with_max_content_len(10);
2218
2219        // Short bytes - no truncation
2220        let short: Vec<u8> = vec![1, 2, 3];
2221        let output = printer.format(&short);
2222        assert!(output.contains("01 02 03"), "should show all bytes: {}", output);
2223
2224        // Long bytes - should truncate middle
2225        let long: Vec<u8> = (0..50).collect();
2226        let output = printer.format(&long);
2227        assert!(output.contains("..."), "should contain ellipsis: {}", output);
2228        assert!(output.contains("bytes"), "should mention bytes: {}", output);
2229    }
2230}