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