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