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                                writeln!(f, " ...({omitted} bytes)...")?;
615                                self.indent(f, format_depth + 1)?;
616                            } else {
617                                write!(f, " ...({omitted} bytes)...")?;
618                            }
619
620                            // Show end
621                            for (idx, item) in list.iter().enumerate().skip(total_len - end_count) {
622                                let display_idx = start_count + 1 + (idx - (total_len - end_count));
623                                if !short && display_idx.is_multiple_of(16) {
624                                    writeln!(f)?;
625                                    self.indent(f, format_depth + 1)?;
626                                }
627                                write!(f, " ")?;
628                                let byte = *item.get::<u8>().unwrap();
629                                if self.use_colors() {
630                                    let mut hasher = DefaultHasher::new();
631                                    byte.hash(&mut hasher);
632                                    let hash = hasher.finish();
633                                    let color = self.color_generator.generate_color(hash);
634                                    let rgb = Rgb(color.r, color.g, color.b);
635                                    write!(f, "{}", format!("{byte:02x}").color(rgb))?;
636                                } else {
637                                    write!(f, "{byte:02x}")?;
638                                }
639                            }
640                        } else {
641                            for (idx, item) in list.iter().enumerate() {
642                                if !short && idx % 16 == 0 {
643                                    writeln!(f)?;
644                                    self.indent(f, format_depth + 1)?;
645                                }
646                                write!(f, " ")?;
647
648                                let byte = *item.get::<u8>().unwrap();
649                                if self.use_colors() {
650                                    let mut hasher = DefaultHasher::new();
651                                    byte.hash(&mut hasher);
652                                    let hash = hasher.finish();
653                                    let color = self.color_generator.generate_color(hash);
654                                    let rgb = Rgb(color.r, color.g, color.b);
655                                    write!(f, "{}", format!("{byte:02x}").color(rgb))?;
656                                } else {
657                                    write!(f, "{byte:02x}")?;
658                                }
659                            }
660                        }
661
662                        if !short {
663                            writeln!(f)?;
664                            self.indent(f, format_depth)?;
665                        }
666                        self.write_punctuation(f, "]")?;
667                    } else {
668                        // Check if elements are simple scalars - render inline if so
669                        let elem_shape = list.def().t();
670                        let is_simple = Self::shape_chunkiness(elem_shape) <= 1;
671
672                        self.write_punctuation(f, " [")?;
673                        let len = list.len();
674                        let visible_len = self.visible_collection_len(len);
675                        for (idx, item) in list.iter().take(visible_len).enumerate() {
676                            if !short && !is_simple {
677                                writeln!(f)?;
678                                self.indent(f, format_depth + 1)?;
679                            } else if idx > 0 {
680                                write!(f, " ")?;
681                            }
682                            self.format_peek_internal_(
683                                item,
684                                f,
685                                visited,
686                                format_depth + 1,
687                                type_depth + 1,
688                                short || is_simple,
689                            )?;
690
691                            if (!short && !is_simple) || idx + 1 < visible_len || visible_len < len
692                            {
693                                self.write_punctuation(f, ",")?;
694                            }
695                        }
696                        if visible_len < len {
697                            if !short && !is_simple {
698                                writeln!(f)?;
699                                self.indent(f, format_depth + 1)?;
700                            } else if visible_len > 0 {
701                                write!(f, " ")?;
702                            }
703                            self.write_collection_ellipsis(f, len - visible_len, "items")?;
704                        }
705                        if !short && !is_simple {
706                            writeln!(f)?;
707                            self.indent(f, format_depth)?;
708                        }
709                        self.write_punctuation(f, "]")?;
710                    }
711                } else {
712                    self.write_punctuation(f, "[]")?;
713                }
714            }
715
716            _ if value.into_set().is_ok() => {
717                self.write_type_name(f, &value)?;
718
719                let value = value.into_set().unwrap();
720                self.write_punctuation(f, " [")?;
721                if !value.is_empty() {
722                    let len = value.len();
723                    let visible_len = self.visible_collection_len(len);
724                    for (idx, item) in value.iter().take(visible_len).enumerate() {
725                        if !short {
726                            writeln!(f)?;
727                            self.indent(f, format_depth + 1)?;
728                        }
729                        self.format_peek_internal_(
730                            item,
731                            f,
732                            visited,
733                            format_depth + 1,
734                            type_depth + 1,
735                            short,
736                        )?;
737                        if !short || idx + 1 < visible_len || visible_len < len {
738                            self.write_punctuation(f, ",")?;
739                        } else {
740                            write!(f, " ")?;
741                        }
742                    }
743                    if visible_len < len {
744                        if !short {
745                            writeln!(f)?;
746                            self.indent(f, format_depth + 1)?;
747                        } else if visible_len > 0 {
748                            write!(f, " ")?;
749                        }
750                        self.write_collection_ellipsis(f, len - visible_len, "items")?;
751                    }
752                    if !short {
753                        writeln!(f)?;
754                        self.indent(f, format_depth)?;
755                    }
756                }
757                self.write_punctuation(f, "]")?;
758            }
759
760            (Def::Map(def), _) => {
761                let key_is_short = Self::shape_chunkiness(def.k) <= 2;
762
763                self.write_type_name(f, &value)?;
764
765                let value = value.into_map().unwrap();
766                self.write_punctuation(f, " [")?;
767
768                if !value.is_empty() {
769                    let len = value.len();
770                    let visible_len = self.visible_collection_len(len);
771                    for (idx, (key, value)) in value.iter().take(visible_len).enumerate() {
772                        if !short {
773                            writeln!(f)?;
774                            self.indent(f, format_depth + 1)?;
775                        }
776                        self.format_peek_internal_(
777                            key,
778                            f,
779                            visited,
780                            format_depth + 1,
781                            type_depth + 1,
782                            key_is_short,
783                        )?;
784                        self.write_punctuation(f, " => ")?;
785                        self.format_peek_internal_(
786                            value,
787                            f,
788                            visited,
789                            format_depth + 1,
790                            type_depth + 1,
791                            short,
792                        )?;
793                        if !short || idx + 1 < visible_len || visible_len < len {
794                            self.write_punctuation(f, ",")?;
795                        } else {
796                            write!(f, " ")?;
797                        }
798                    }
799                    if visible_len < len {
800                        if !short {
801                            writeln!(f)?;
802                            self.indent(f, format_depth + 1)?;
803                        } else if visible_len > 0 {
804                            write!(f, " ")?;
805                        }
806                        self.write_collection_ellipsis(f, len - visible_len, "entries")?;
807                    }
808                    if !short {
809                        writeln!(f)?;
810                        self.indent(f, format_depth)?;
811                    }
812                }
813
814                self.write_punctuation(f, "]")?;
815            }
816
817            (Def::DynamicValue(_), _) => {
818                let dyn_val = value.into_dynamic_value().unwrap();
819                match dyn_val.kind() {
820                    DynValueKind::Null => {
821                        self.write_keyword(f, "null")?;
822                    }
823                    DynValueKind::Bool => {
824                        if let Some(b) = dyn_val.as_bool() {
825                            self.write_keyword(f, if b { "true" } else { "false" })?;
826                        }
827                    }
828                    DynValueKind::Number => {
829                        if let Some(n) = dyn_val.as_i64() {
830                            self.format_number(f, &n.to_string())?;
831                        } else if let Some(n) = dyn_val.as_u64() {
832                            self.format_number(f, &n.to_string())?;
833                        } else if let Some(n) = dyn_val.as_f64() {
834                            self.format_number(f, &n.to_string())?;
835                        }
836                    }
837                    DynValueKind::String => {
838                        if let Some(s) = dyn_val.as_str() {
839                            self.format_string(f, s)?;
840                        }
841                    }
842                    DynValueKind::Bytes => {
843                        if let Some(bytes) = dyn_val.as_bytes() {
844                            self.format_bytes(f, bytes)?;
845                        }
846                    }
847                    DynValueKind::Array => {
848                        let len = dyn_val.array_len().unwrap_or(0);
849                        if len == 0 {
850                            self.write_punctuation(f, "[]")?;
851                        } else {
852                            self.write_punctuation(f, "[")?;
853                            let visible_len = self.visible_collection_len(len);
854                            for idx in 0..visible_len {
855                                if !short {
856                                    writeln!(f)?;
857                                    self.indent(f, format_depth + 1)?;
858                                }
859                                if let Some(elem) = dyn_val.array_get(idx) {
860                                    self.format_peek_internal_(
861                                        elem,
862                                        f,
863                                        visited,
864                                        format_depth + 1,
865                                        type_depth + 1,
866                                        short,
867                                    )?;
868                                }
869                                if !short || idx + 1 < visible_len || visible_len < len {
870                                    self.write_punctuation(f, ",")?;
871                                } else {
872                                    write!(f, " ")?;
873                                }
874                            }
875                            if visible_len < len {
876                                if !short {
877                                    writeln!(f)?;
878                                    self.indent(f, format_depth + 1)?;
879                                } else if visible_len > 0 {
880                                    write!(f, " ")?;
881                                }
882                                self.write_collection_ellipsis(f, len - visible_len, "items")?;
883                            }
884                            if !short {
885                                writeln!(f)?;
886                                self.indent(f, format_depth)?;
887                            }
888                            self.write_punctuation(f, "]")?;
889                        }
890                    }
891                    DynValueKind::Object => {
892                        let len = dyn_val.object_len().unwrap_or(0);
893                        if len == 0 {
894                            self.write_punctuation(f, "{}")?;
895                        } else {
896                            self.write_punctuation(f, "{")?;
897                            let visible_len = self.visible_collection_len(len);
898                            for idx in 0..visible_len {
899                                if !short {
900                                    writeln!(f)?;
901                                    self.indent(f, format_depth + 1)?;
902                                }
903                                if let Some((key, val)) = dyn_val.object_get_entry(idx) {
904                                    self.write_field_name(f, key)?;
905                                    self.write_punctuation(f, ": ")?;
906                                    self.format_peek_internal_(
907                                        val,
908                                        f,
909                                        visited,
910                                        format_depth + 1,
911                                        type_depth + 1,
912                                        short,
913                                    )?;
914                                }
915                                if !short || idx + 1 < visible_len || visible_len < len {
916                                    self.write_punctuation(f, ",")?;
917                                } else {
918                                    write!(f, " ")?;
919                                }
920                            }
921                            if visible_len < len {
922                                if !short {
923                                    writeln!(f)?;
924                                    self.indent(f, format_depth + 1)?;
925                                } else if visible_len > 0 {
926                                    write!(f, " ")?;
927                                }
928                                self.write_collection_ellipsis(f, len - visible_len, "entries")?;
929                            }
930                            if !short {
931                                writeln!(f)?;
932                                self.indent(f, format_depth)?;
933                            }
934                            self.write_punctuation(f, "}")?;
935                        }
936                    }
937                    DynValueKind::DateTime => {
938                        // Format datetime using the vtable's get_datetime
939                        #[allow(clippy::uninlined_format_args)]
940                        if let Some((year, month, day, hour, minute, second, nanos, kind)) =
941                            dyn_val.as_datetime()
942                        {
943                            match kind {
944                                DynDateTimeKind::Offset { offset_minutes } => {
945                                    if nanos > 0 {
946                                        write!(
947                                            f,
948                                            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09}",
949                                            year, month, day, hour, minute, second, nanos
950                                        )?;
951                                    } else {
952                                        write!(
953                                            f,
954                                            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
955                                            year, month, day, hour, minute, second
956                                        )?;
957                                    }
958                                    if offset_minutes == 0 {
959                                        write!(f, "Z")?;
960                                    } else {
961                                        let sign = if offset_minutes >= 0 { '+' } else { '-' };
962                                        let abs = offset_minutes.abs();
963                                        write!(f, "{}{:02}:{:02}", sign, abs / 60, abs % 60)?;
964                                    }
965                                }
966                                DynDateTimeKind::LocalDateTime => {
967                                    if nanos > 0 {
968                                        write!(
969                                            f,
970                                            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09}",
971                                            year, month, day, hour, minute, second, nanos
972                                        )?;
973                                    } else {
974                                        write!(
975                                            f,
976                                            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
977                                            year, month, day, hour, minute, second
978                                        )?;
979                                    }
980                                }
981                                DynDateTimeKind::LocalDate => {
982                                    write!(f, "{:04}-{:02}-{:02}", year, month, day)?;
983                                }
984                                DynDateTimeKind::LocalTime => {
985                                    if nanos > 0 {
986                                        write!(
987                                            f,
988                                            "{:02}:{:02}:{:02}.{:09}",
989                                            hour, minute, second, nanos
990                                        )?;
991                                    } else {
992                                        write!(f, "{:02}:{:02}:{:02}", hour, minute, second)?;
993                                    }
994                                }
995                            }
996                        }
997                    }
998                    DynValueKind::QName => {
999                        // QName formatting is not yet supported via vtable
1000                        write!(f, "<qname>")?;
1001                    }
1002                    DynValueKind::Uuid => {
1003                        // UUID formatting is not yet supported via vtable
1004                        write!(f, "<uuid>")?;
1005                    }
1006                }
1007            }
1008
1009            (d, t) => write!(f, "unsupported peek variant: {value:?} ({d:?}, {t:?})")?,
1010        }
1011
1012        visited.remove(&value.id());
1013        Ok(())
1014    }
1015
1016    /// Format a value through its proxy type representation.
1017    ///
1018    /// This allocates memory for the proxy type, converts the value to its proxy
1019    /// representation, formats the proxy, then cleans up.
1020    #[allow(clippy::too_many_arguments)]
1021    fn format_via_proxy(
1022        &self,
1023        value: Peek<'_, '_>,
1024        proxy_def: &'static facet_core::ProxyDef,
1025        f: &mut dyn Write,
1026        visited: &mut BTreeMap<ValueId, usize>,
1027        format_depth: usize,
1028        type_depth: usize,
1029        short: bool,
1030    ) -> fmt::Result {
1031        let proxy_shape = proxy_def.shape;
1032        let proxy_layout = match proxy_shape.layout.sized_layout() {
1033            Ok(layout) => layout,
1034            Err(_) => {
1035                return write!(f, "/* proxy type must be sized for formatting */");
1036            }
1037        };
1038
1039        // Allocate memory for the proxy value
1040        let proxy_uninit = facet_core::alloc_for_layout(proxy_layout);
1041
1042        // Convert target → proxy
1043        let convert_result = unsafe { (proxy_def.convert_out)(value.data(), proxy_uninit) };
1044
1045        let proxy_ptr = match convert_result {
1046            Ok(ptr) => ptr,
1047            Err(msg) => {
1048                unsafe { facet_core::dealloc_for_layout(proxy_uninit.assume_init(), proxy_layout) };
1049                return write!(f, "/* proxy conversion failed: {msg} */");
1050            }
1051        };
1052
1053        // Create a Peek to the proxy value and format it
1054        let proxy_peek = unsafe { Peek::unchecked_new(proxy_ptr.as_const(), proxy_shape) };
1055        let result =
1056            self.format_peek_internal_(proxy_peek, f, visited, format_depth, type_depth, short);
1057
1058        // Clean up: drop the proxy value and deallocate
1059        unsafe {
1060            let _ = proxy_shape.call_drop_in_place(proxy_ptr);
1061            facet_core::dealloc_for_layout(proxy_ptr, proxy_layout);
1062        }
1063
1064        result
1065    }
1066
1067    /// Format a value through its proxy type representation (unified version for FormatOutput).
1068    ///
1069    /// This allocates memory for the proxy type, converts the value to its proxy
1070    /// representation, formats the proxy, then cleans up.
1071    #[allow(clippy::too_many_arguments)]
1072    fn format_via_proxy_unified<O: FormatOutput>(
1073        &self,
1074        value: Peek<'_, '_>,
1075        proxy_def: &'static facet_core::ProxyDef,
1076        out: &mut O,
1077        visited: &mut BTreeMap<ValueId, usize>,
1078        format_depth: usize,
1079        type_depth: usize,
1080        short: bool,
1081        current_path: Path,
1082    ) -> fmt::Result {
1083        let proxy_shape = proxy_def.shape;
1084        let proxy_layout = match proxy_shape.layout.sized_layout() {
1085            Ok(layout) => layout,
1086            Err(_) => {
1087                return write!(out, "/* proxy type must be sized for formatting */");
1088            }
1089        };
1090
1091        // Allocate memory for the proxy value
1092        let proxy_uninit = facet_core::alloc_for_layout(proxy_layout);
1093
1094        // Convert target → proxy
1095        let convert_result = unsafe { (proxy_def.convert_out)(value.data(), proxy_uninit) };
1096
1097        let proxy_ptr = match convert_result {
1098            Ok(ptr) => ptr,
1099            Err(msg) => {
1100                unsafe { facet_core::dealloc_for_layout(proxy_uninit.assume_init(), proxy_layout) };
1101                return write!(out, "/* proxy conversion failed: {msg} */");
1102            }
1103        };
1104
1105        // Create a Peek to the proxy value and format it
1106        let proxy_peek = unsafe { Peek::unchecked_new(proxy_ptr.as_const(), proxy_shape) };
1107        let result = self.format_unified(
1108            proxy_peek,
1109            out,
1110            visited,
1111            format_depth,
1112            type_depth,
1113            short,
1114            current_path,
1115        );
1116
1117        // Clean up: drop the proxy value and deallocate
1118        unsafe {
1119            let _ = proxy_shape.call_drop_in_place(proxy_ptr);
1120            facet_core::dealloc_for_layout(proxy_ptr, proxy_layout);
1121        }
1122
1123        result
1124    }
1125
1126    #[allow(clippy::too_many_arguments)]
1127    fn format_tuple_fields<'mem, 'facet>(
1128        &self,
1129        peek_field: &dyn Fn(usize) -> Peek<'mem, 'facet>,
1130        f: &mut dyn Write,
1131        visited: &mut BTreeMap<ValueId, usize>,
1132        format_depth: usize,
1133        type_depth: usize,
1134        fields: &[Field],
1135        short: bool,
1136        force_trailing_comma: bool,
1137    ) -> fmt::Result {
1138        self.write_punctuation(f, "(")?;
1139        if let [field] = fields
1140            && field.doc.is_empty()
1141        {
1142            let field_value = peek_field(0);
1143            if let Some(proxy_def) = field.proxy() {
1144                self.format_via_proxy(
1145                    field_value,
1146                    proxy_def,
1147                    f,
1148                    visited,
1149                    format_depth,
1150                    type_depth,
1151                    short,
1152                )?;
1153            } else {
1154                self.format_peek_internal_(
1155                    field_value,
1156                    f,
1157                    visited,
1158                    format_depth,
1159                    type_depth,
1160                    short,
1161                )?;
1162            }
1163
1164            if force_trailing_comma {
1165                self.write_punctuation(f, ",")?;
1166            }
1167        } else if !fields.is_empty() {
1168            let visible_len = self.visible_collection_len(fields.len());
1169            for idx in 0..visible_len {
1170                if !short {
1171                    writeln!(f)?;
1172                    self.indent(f, format_depth + 1)?;
1173
1174                    if self.show_doc_comments {
1175                        for &line in fields[idx].doc {
1176                            self.write_comment(f, &format!("///{line}"))?;
1177                            writeln!(f)?;
1178                            self.indent(f, format_depth + 1)?;
1179                        }
1180                    }
1181                }
1182
1183                if fields[idx].is_sensitive() {
1184                    self.write_redacted(f, "[REDACTED]")?;
1185                } else if let Some(proxy_def) = fields[idx].proxy() {
1186                    // Field-level proxy: format through the proxy type
1187                    self.format_via_proxy(
1188                        peek_field(idx),
1189                        proxy_def,
1190                        f,
1191                        visited,
1192                        format_depth + 1,
1193                        type_depth + 1,
1194                        short,
1195                    )?;
1196                } else {
1197                    self.format_peek_internal_(
1198                        peek_field(idx),
1199                        f,
1200                        visited,
1201                        format_depth + 1,
1202                        type_depth + 1,
1203                        short,
1204                    )?;
1205                }
1206
1207                if !short || idx + 1 < visible_len || visible_len < fields.len() {
1208                    self.write_punctuation(f, ",")?;
1209                } else {
1210                    write!(f, " ")?;
1211                }
1212            }
1213            if visible_len < fields.len() {
1214                if !short {
1215                    writeln!(f)?;
1216                    self.indent(f, format_depth + 1)?;
1217                } else if visible_len > 0 {
1218                    write!(f, " ")?;
1219                }
1220                self.write_collection_ellipsis(f, fields.len() - visible_len, "fields")?;
1221            }
1222            if !short {
1223                writeln!(f)?;
1224                self.indent(f, format_depth)?;
1225            }
1226        }
1227        self.write_punctuation(f, ")")?;
1228        Ok(())
1229    }
1230
1231    #[allow(clippy::too_many_arguments)]
1232    fn format_struct_fields<'mem, 'facet>(
1233        &self,
1234        peek_field: &dyn Fn(usize) -> Peek<'mem, 'facet>,
1235        f: &mut dyn Write,
1236        visited: &mut BTreeMap<ValueId, usize>,
1237        format_depth: usize,
1238        type_depth: usize,
1239        fields: &[Field],
1240        short: bool,
1241    ) -> fmt::Result {
1242        // First, determine which fields will be printed (not skipped)
1243        let mut visible_indices: Vec<usize> = (0..fields.len())
1244            .filter(|&idx| {
1245                let field = &fields[idx];
1246                // SAFETY: peek_field returns a valid Peek with valid data pointer
1247                let field_ptr = peek_field(idx).data();
1248                !unsafe { field.should_skip_serializing(field_ptr) }
1249            })
1250            .collect();
1251        let total_visible = visible_indices.len();
1252        if let Some(max) = self.max_collection_len {
1253            visible_indices.truncate(max);
1254        }
1255
1256        self.write_punctuation(f, " {")?;
1257        if !visible_indices.is_empty() {
1258            for (i, &idx) in visible_indices.iter().enumerate() {
1259                let is_last = i + 1 == visible_indices.len();
1260
1261                if !short {
1262                    writeln!(f)?;
1263                    self.indent(f, format_depth + 1)?;
1264                }
1265
1266                if self.show_doc_comments {
1267                    for &line in fields[idx].doc {
1268                        self.write_comment(f, &format!("///{line}"))?;
1269                        writeln!(f)?;
1270                        self.indent(f, format_depth + 1)?;
1271                    }
1272                }
1273
1274                self.write_field_name(f, fields[idx].name)?;
1275                self.write_punctuation(f, ": ")?;
1276                if fields[idx].is_sensitive() {
1277                    self.write_redacted(f, "[REDACTED]")?;
1278                } else if let Some(proxy_def) = fields[idx].proxy() {
1279                    // Field-level proxy: format through the proxy type
1280                    self.format_via_proxy(
1281                        peek_field(idx),
1282                        proxy_def,
1283                        f,
1284                        visited,
1285                        format_depth + 1,
1286                        type_depth + 1,
1287                        short,
1288                    )?;
1289                } else {
1290                    self.format_peek_internal_(
1291                        peek_field(idx),
1292                        f,
1293                        visited,
1294                        format_depth + 1,
1295                        type_depth + 1,
1296                        short,
1297                    )?;
1298                }
1299
1300                if !short || !is_last {
1301                    self.write_punctuation(f, ",")?;
1302                } else {
1303                    write!(f, " ")?;
1304                }
1305            }
1306            if total_visible > visible_indices.len() {
1307                if !short {
1308                    writeln!(f)?;
1309                    self.indent(f, format_depth + 1)?;
1310                } else {
1311                    write!(f, " ")?;
1312                }
1313                self.write_collection_ellipsis(f, total_visible - visible_indices.len(), "fields")?;
1314            }
1315            if !short {
1316                writeln!(f)?;
1317                self.indent(f, format_depth)?;
1318            }
1319        }
1320        self.write_punctuation(f, "}")?;
1321        Ok(())
1322    }
1323
1324    fn indent(&self, f: &mut dyn Write, indent: usize) -> fmt::Result {
1325        if self.indent_size == usize::MAX {
1326            write!(f, "{:\t<width$}", "", width = indent)
1327        } else {
1328            write!(f, "{: <width$}", "", width = indent * self.indent_size)
1329        }
1330    }
1331
1332    fn visible_collection_len(&self, len: usize) -> usize {
1333        self.max_collection_len
1334            .map_or(len, |max| Ord::min(len, max))
1335    }
1336
1337    fn write_collection_ellipsis(
1338        &self,
1339        f: &mut dyn Write,
1340        omitted: usize,
1341        noun: &str,
1342    ) -> fmt::Result {
1343        self.write_comment(f, &format!("...({omitted} more {noun})..."))
1344    }
1345
1346    /// Internal method to format a Peek value
1347    pub(crate) fn format_peek_internal(
1348        &self,
1349        value: Peek<'_, '_>,
1350        f: &mut dyn Write,
1351        visited: &mut BTreeMap<ValueId, usize>,
1352    ) -> fmt::Result {
1353        self.format_peek_internal_(value, f, visited, 0, 0, false)
1354    }
1355
1356    /// Format a scalar value
1357    fn format_scalar(&self, value: Peek, f: &mut dyn Write) -> fmt::Result {
1358        // Generate a color for this shape
1359        let mut hasher = DefaultHasher::new();
1360        value.shape().id.hash(&mut hasher);
1361        let hash = hasher.finish();
1362        let color = self.color_generator.generate_color(hash);
1363
1364        // Display the value
1365        struct DisplayWrapper<'mem, 'facet>(&'mem Peek<'mem, 'facet>);
1366
1367        impl fmt::Display for DisplayWrapper<'_, '_> {
1368            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1369                if self.0.shape().is_display() {
1370                    write!(f, "{}", self.0)?;
1371                } else if self.0.shape().is_debug() {
1372                    write!(f, "{:?}", self.0)?;
1373                } else {
1374                    write!(f, "{}", self.0.shape())?;
1375                    write!(f, "(…)")?;
1376                }
1377                Ok(())
1378            }
1379        }
1380
1381        // Apply color if needed and display
1382        if self.use_colors() {
1383            let rgb = Rgb(color.r, color.g, color.b);
1384            write!(f, "{}", DisplayWrapper(&value).color(rgb))?;
1385        } else {
1386            write!(f, "{}", DisplayWrapper(&value))?;
1387        }
1388
1389        Ok(())
1390    }
1391
1392    /// Write a keyword (null, true, false) with coloring
1393    fn write_keyword(&self, f: &mut dyn Write, keyword: &str) -> fmt::Result {
1394        if self.use_colors() {
1395            write!(f, "{}", keyword.color(tokyo_night::KEYWORD))
1396        } else {
1397            write!(f, "{keyword}")
1398        }
1399    }
1400
1401    /// Format a number for dynamic values
1402    fn format_number(&self, f: &mut dyn Write, s: &str) -> fmt::Result {
1403        if self.use_colors() {
1404            write!(f, "{}", s.color(tokyo_night::NUMBER))
1405        } else {
1406            write!(f, "{s}")
1407        }
1408    }
1409
1410    /// Format a &str or String value with optional truncation and raw string handling
1411    fn format_str_value(&self, f: &mut dyn Write, value: &str) -> fmt::Result {
1412        // Check if truncation is needed
1413        if let Some(max) = self.max_content_len
1414            && value.len() > max
1415        {
1416            return self.format_truncated_str(f, value, max);
1417        }
1418
1419        // Normal formatting with raw string handling for quotes
1420        let mut hashes = 0usize;
1421        let mut rest = value;
1422        while let Some(idx) = rest.find('"') {
1423            rest = &rest[idx + 1..];
1424            let before = rest.len();
1425            rest = rest.trim_start_matches('#');
1426            let after = rest.len();
1427            let count = before - after;
1428            hashes = Ord::max(hashes, 1 + count);
1429        }
1430
1431        let pad = "";
1432        let width = hashes.saturating_sub(1);
1433        if hashes > 0 {
1434            write!(f, "r{pad:#<width$}")?;
1435        }
1436        write!(f, "\"")?;
1437        if self.use_colors() {
1438            write!(f, "{}", value.color(tokyo_night::STRING))?;
1439        } else {
1440            write!(f, "{value}")?;
1441        }
1442        write!(f, "\"")?;
1443        if hashes > 0 {
1444            write!(f, "{pad:#<width$}")?;
1445        }
1446        Ok(())
1447    }
1448
1449    /// Format a truncated string showing beginning...end
1450    fn format_truncated_str(&self, f: &mut dyn Write, s: &str, max: usize) -> fmt::Result {
1451        let half = max / 2;
1452
1453        // Find char boundary for start portion
1454        let start_end = s
1455            .char_indices()
1456            .take_while(|(i, _)| *i < half)
1457            .last()
1458            .map(|(i, c)| i + c.len_utf8())
1459            .unwrap_or(0);
1460
1461        // Find char boundary for end portion
1462        let end_start = s
1463            .char_indices()
1464            .rev()
1465            .take_while(|(i, _)| s.len() - *i <= half)
1466            .last()
1467            .map(|(i, _)| i)
1468            .unwrap_or(s.len());
1469
1470        let omitted = s[start_end..end_start].chars().count();
1471        let start_part = &s[..start_end];
1472        let end_part = &s[end_start..];
1473
1474        if self.use_colors() {
1475            write!(
1476                f,
1477                "\"{}\"...({omitted} chars)...\"{}\"",
1478                start_part.color(tokyo_night::STRING),
1479                end_part.color(tokyo_night::STRING)
1480            )
1481        } else {
1482            write!(f, "\"{start_part}\"...({omitted} chars)...\"{end_part}\"")
1483        }
1484    }
1485
1486    /// Format a string for dynamic values (uses debug escaping for special chars)
1487    fn format_string(&self, f: &mut dyn Write, s: &str) -> fmt::Result {
1488        if let Some(max) = self.max_content_len
1489            && s.len() > max
1490        {
1491            return self.format_truncated_str(f, s, max);
1492        }
1493
1494        if self.use_colors() {
1495            write!(f, "\"{}\"", s.color(tokyo_night::STRING))
1496        } else {
1497            write!(f, "{s:?}")
1498        }
1499    }
1500
1501    /// Format bytes for dynamic values
1502    fn format_bytes(&self, f: &mut dyn Write, bytes: &[u8]) -> fmt::Result {
1503        write!(f, "b\"")?;
1504
1505        match self.max_content_len {
1506            Some(max) if bytes.len() > max => {
1507                // Show beginning ... end
1508                let half = max / 2;
1509                let start = half;
1510                let end = half;
1511
1512                for byte in &bytes[..start] {
1513                    write!(f, "\\x{byte:02x}")?;
1514                }
1515                let omitted = bytes.len() - start - end;
1516                write!(f, "\"...({omitted} bytes)...b\"")?;
1517                for byte in &bytes[bytes.len() - end..] {
1518                    write!(f, "\\x{byte:02x}")?;
1519                }
1520            }
1521            _ => {
1522                for byte in bytes {
1523                    write!(f, "\\x{byte:02x}")?;
1524                }
1525            }
1526        }
1527
1528        write!(f, "\"")
1529    }
1530
1531    /// Write styled type name to formatter
1532    fn write_type_name(&self, f: &mut dyn Write, peek: &Peek) -> fmt::Result {
1533        struct TypeNameWriter<'mem, 'facet>(&'mem Peek<'mem, 'facet>);
1534
1535        impl core::fmt::Display for TypeNameWriter<'_, '_> {
1536            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1537                self.0.type_name(f, TypeNameOpts::infinite())
1538            }
1539        }
1540        let type_name = TypeNameWriter(peek);
1541
1542        if self.use_colors() {
1543            write!(f, "{}", type_name.color(tokyo_night::TYPE_NAME).bold())
1544        } else {
1545            write!(f, "{type_name}")
1546        }
1547    }
1548
1549    /// Style a type name and return it as a string
1550    #[allow(dead_code)]
1551    fn style_type_name(&self, peek: &Peek) -> String {
1552        let mut result = String::new();
1553        self.write_type_name(&mut result, peek).unwrap();
1554        result
1555    }
1556
1557    /// Write styled field name to formatter
1558    fn write_field_name(&self, f: &mut dyn Write, name: &str) -> fmt::Result {
1559        if self.use_colors() {
1560            write!(f, "{}", name.color(tokyo_night::FIELD_NAME))
1561        } else {
1562            write!(f, "{name}")
1563        }
1564    }
1565
1566    /// Write styled punctuation to formatter
1567    fn write_punctuation(&self, f: &mut dyn Write, text: &str) -> fmt::Result {
1568        if self.use_colors() {
1569            write!(f, "{}", text.dimmed())
1570        } else {
1571            write!(f, "{text}")
1572        }
1573    }
1574
1575    /// Write styled comment to formatter
1576    fn write_comment(&self, f: &mut dyn Write, text: &str) -> fmt::Result {
1577        if self.use_colors() {
1578            write!(f, "{}", text.color(tokyo_night::MUTED))
1579        } else {
1580            write!(f, "{text}")
1581        }
1582    }
1583
1584    /// Write styled redacted value to formatter
1585    fn write_redacted(&self, f: &mut dyn Write, text: &str) -> fmt::Result {
1586        if self.use_colors() {
1587            write!(f, "{}", text.color(tokyo_night::ERROR).bold())
1588        } else {
1589            write!(f, "{text}")
1590        }
1591    }
1592
1593    /// Style a redacted value and return it as a string
1594    #[allow(dead_code)]
1595    fn style_redacted(&self, text: &str) -> String {
1596        let mut result = String::new();
1597        self.write_redacted(&mut result, text).unwrap();
1598        result
1599    }
1600
1601    /// Format a value with span tracking for each path.
1602    ///
1603    /// Returns a `FormattedValue` containing the plain text output and a map
1604    /// from paths to their byte spans in the output.
1605    ///
1606    /// This is useful for creating rich diagnostics that can highlight specific
1607    /// parts of a pretty-printed value.
1608    pub fn format_peek_with_spans(&self, value: Peek<'_, '_>) -> FormattedValue {
1609        let mut output = SpanTrackingOutput::new();
1610        let printer = Self {
1611            colors: ColorMode::Never, // Always disable colors for span tracking
1612            indent_size: self.indent_size,
1613            max_depth: self.max_depth,
1614            color_generator: self.color_generator.clone(),
1615            list_u8_as_bytes: self.list_u8_as_bytes,
1616            minimal_option_names: self.minimal_option_names,
1617            show_doc_comments: self.show_doc_comments,
1618            max_content_len: self.max_content_len,
1619            max_collection_len: self.max_collection_len,
1620        };
1621        printer
1622            .format_unified(
1623                value,
1624                &mut output,
1625                &mut BTreeMap::new(),
1626                0,
1627                0,
1628                false,
1629                vec![],
1630            )
1631            .expect("Formatting failed");
1632
1633        output.into_formatted_value()
1634    }
1635
1636    /// Unified formatting implementation that works with any FormatOutput.
1637    ///
1638    /// This is the core implementation - both `format_peek` and `format_peek_with_spans`
1639    /// use this internally with different output types.
1640    #[allow(clippy::too_many_arguments)]
1641    fn format_unified<O: FormatOutput>(
1642        &self,
1643        value: Peek<'_, '_>,
1644        out: &mut O,
1645        visited: &mut BTreeMap<ValueId, usize>,
1646        format_depth: usize,
1647        type_depth: usize,
1648        short: bool,
1649        current_path: Path,
1650    ) -> fmt::Result {
1651        let mut value = value;
1652        while let Ok(ptr) = value.into_pointer()
1653            && let Some(pointee) = ptr.borrow_inner()
1654        {
1655            value = pointee;
1656        }
1657
1658        // Unwrap transparent wrappers (e.g., newtype wrappers like IntAsString(String))
1659        // This matches serialization behavior where we serialize the inner value directly
1660        let value = value.innermost_peek();
1661        let shape = value.shape();
1662
1663        // Record the start of this value
1664        let value_start = out.position();
1665
1666        if let Some(prev_type_depth) = visited.insert(value.id(), type_depth) {
1667            write!(out, "{} {{ ", shape)?;
1668            write!(
1669                out,
1670                "/* cycle detected at {} (first seen at type_depth {}) */",
1671                value.id(),
1672                prev_type_depth,
1673            )?;
1674            visited.remove(&value.id());
1675            let value_end = out.position();
1676            out.record_span(current_path, (value_start, value_end));
1677            return Ok(());
1678        }
1679
1680        // Handle proxy types by converting to the proxy representation and formatting that
1681        if let Some(proxy_def) = shape.proxy {
1682            let result = self.format_via_proxy_unified(
1683                value,
1684                proxy_def,
1685                out,
1686                visited,
1687                format_depth,
1688                type_depth,
1689                short,
1690                current_path.clone(),
1691            );
1692
1693            visited.remove(&value.id());
1694
1695            // Record span for this value
1696            let value_end = out.position();
1697            out.record_span(current_path, (value_start, value_end));
1698
1699            return result;
1700        }
1701
1702        match (shape.def, shape.ty) {
1703            (_, Type::Primitive(PrimitiveType::Textual(TextualType::Str))) => {
1704                let s = value.get::<str>().unwrap();
1705                write!(out, "\"{}\"", s)?;
1706            }
1707            (Def::Scalar, _) if value.shape().id == <alloc::string::String as Facet>::SHAPE.id => {
1708                let s = value.get::<alloc::string::String>().unwrap();
1709                write!(out, "\"{}\"", s)?;
1710            }
1711            (Def::Scalar, _) => {
1712                self.format_scalar_to_output(value, out)?;
1713            }
1714            (Def::Option(_), _) => {
1715                let option = value.into_option().unwrap();
1716                if let Some(inner) = option.value() {
1717                    write!(out, "Some(")?;
1718                    self.format_unified(
1719                        inner,
1720                        out,
1721                        visited,
1722                        format_depth,
1723                        type_depth + 1,
1724                        short,
1725                        current_path.clone(),
1726                    )?;
1727                    write!(out, ")")?;
1728                } else {
1729                    write!(out, "None")?;
1730                }
1731            }
1732            (Def::Result(_), _) => {
1733                let result = value.into_result().unwrap();
1734                write!(out, "{}", shape)?;
1735                if result.is_ok() {
1736                    write!(out, " Ok(")?;
1737                    if let Some(ok_val) = result.ok() {
1738                        self.format_unified(
1739                            ok_val,
1740                            out,
1741                            visited,
1742                            format_depth,
1743                            type_depth + 1,
1744                            short,
1745                            current_path.clone(),
1746                        )?;
1747                    }
1748                    write!(out, ")")?;
1749                } else {
1750                    write!(out, " Err(")?;
1751                    if let Some(err_val) = result.err() {
1752                        self.format_unified(
1753                            err_val,
1754                            out,
1755                            visited,
1756                            format_depth,
1757                            type_depth + 1,
1758                            short,
1759                            current_path.clone(),
1760                        )?;
1761                    }
1762                    write!(out, ")")?;
1763                }
1764            }
1765            (
1766                _,
1767                Type::User(UserType::Struct(
1768                    ty @ StructType {
1769                        kind: StructKind::Struct | StructKind::Unit,
1770                        ..
1771                    },
1772                )),
1773            ) => {
1774                write!(out, "{}", shape)?;
1775                if matches!(ty.kind, StructKind::Struct) {
1776                    let struct_peek = value.into_struct().unwrap();
1777                    write!(out, " {{")?;
1778                    for (i, field) in ty.fields.iter().enumerate() {
1779                        if !short {
1780                            writeln!(out)?;
1781                            self.indent_to_output(out, format_depth + 1)?;
1782                        }
1783                        // Record field name span
1784                        let field_name_start = out.position();
1785                        write!(out, "{}", field.name)?;
1786                        let field_name_end = out.position();
1787                        write!(out, ": ")?;
1788
1789                        // Build path for this field
1790                        let mut field_path = current_path.clone();
1791                        field_path.push(PathSegment::Field(Cow::Borrowed(field.name)));
1792
1793                        // Record field value span
1794                        let field_value_start = out.position();
1795                        if let Ok(field_value) = struct_peek.field(i) {
1796                            // Check for field-level proxy
1797                            if let Some(proxy_def) = field.proxy() {
1798                                self.format_via_proxy_unified(
1799                                    field_value,
1800                                    proxy_def,
1801                                    out,
1802                                    visited,
1803                                    format_depth + 1,
1804                                    type_depth + 1,
1805                                    short,
1806                                    field_path.clone(),
1807                                )?;
1808                            } else {
1809                                self.format_unified(
1810                                    field_value,
1811                                    out,
1812                                    visited,
1813                                    format_depth + 1,
1814                                    type_depth + 1,
1815                                    short,
1816                                    field_path.clone(),
1817                                )?;
1818                            }
1819                        }
1820                        let field_value_end = out.position();
1821
1822                        // Record span for this field
1823                        out.record_field_span(
1824                            field_path,
1825                            (field_name_start, field_name_end),
1826                            (field_value_start, field_value_end),
1827                        );
1828
1829                        if !short || i + 1 < ty.fields.len() {
1830                            write!(out, ",")?;
1831                        }
1832                    }
1833                    if !short {
1834                        writeln!(out)?;
1835                        self.indent_to_output(out, format_depth)?;
1836                    }
1837                    write!(out, "}}")?;
1838                }
1839            }
1840            (
1841                _,
1842                Type::User(UserType::Struct(
1843                    ty @ StructType {
1844                        kind: StructKind::Tuple | StructKind::TupleStruct,
1845                        ..
1846                    },
1847                )),
1848            ) => {
1849                write!(out, "{}", shape)?;
1850                if matches!(ty.kind, StructKind::Tuple) {
1851                    write!(out, " ")?;
1852                }
1853                let struct_peek = value.into_struct().unwrap();
1854                write!(out, "(")?;
1855                for (i, field) in ty.fields.iter().enumerate() {
1856                    if i > 0 {
1857                        write!(out, ", ")?;
1858                    }
1859                    let mut elem_path = current_path.clone();
1860                    elem_path.push(PathSegment::Index(i));
1861
1862                    let elem_start = out.position();
1863                    if let Ok(field_value) = struct_peek.field(i) {
1864                        // Check for field-level proxy
1865                        if let Some(proxy_def) = field.proxy() {
1866                            self.format_via_proxy_unified(
1867                                field_value,
1868                                proxy_def,
1869                                out,
1870                                visited,
1871                                format_depth + 1,
1872                                type_depth + 1,
1873                                short,
1874                                elem_path.clone(),
1875                            )?;
1876                        } else {
1877                            self.format_unified(
1878                                field_value,
1879                                out,
1880                                visited,
1881                                format_depth + 1,
1882                                type_depth + 1,
1883                                short,
1884                                elem_path.clone(),
1885                            )?;
1886                        }
1887                    }
1888                    let elem_end = out.position();
1889                    out.record_span(elem_path, (elem_start, elem_end));
1890                }
1891                write!(out, ")")?;
1892            }
1893            (_, Type::User(UserType::Enum(_))) => {
1894                let enum_peek = value.into_enum().unwrap();
1895                match enum_peek.active_variant() {
1896                    Err(_) => {
1897                        write!(out, "{} {{ /* cannot determine variant */ }}", shape)?;
1898                    }
1899                    Ok(variant) => {
1900                        write!(out, "{}::{}", shape, variant.name)?;
1901
1902                        match variant.data.kind {
1903                            StructKind::Unit => {}
1904                            StructKind::Struct => {
1905                                write!(out, " {{")?;
1906                                for (i, field) in variant.data.fields.iter().enumerate() {
1907                                    if !short {
1908                                        writeln!(out)?;
1909                                        self.indent_to_output(out, format_depth + 1)?;
1910                                    }
1911                                    let field_name_start = out.position();
1912                                    write!(out, "{}", field.name)?;
1913                                    let field_name_end = out.position();
1914                                    write!(out, ": ")?;
1915
1916                                    let mut field_path = current_path.clone();
1917                                    field_path
1918                                        .push(PathSegment::Variant(Cow::Borrowed(variant.name)));
1919                                    field_path.push(PathSegment::Field(Cow::Borrowed(field.name)));
1920
1921                                    let field_value_start = out.position();
1922                                    if let Ok(Some(field_value)) = enum_peek.field(i) {
1923                                        // Check for field-level proxy
1924                                        if let Some(proxy_def) = field.proxy() {
1925                                            self.format_via_proxy_unified(
1926                                                field_value,
1927                                                proxy_def,
1928                                                out,
1929                                                visited,
1930                                                format_depth + 1,
1931                                                type_depth + 1,
1932                                                short,
1933                                                field_path.clone(),
1934                                            )?;
1935                                        } else {
1936                                            self.format_unified(
1937                                                field_value,
1938                                                out,
1939                                                visited,
1940                                                format_depth + 1,
1941                                                type_depth + 1,
1942                                                short,
1943                                                field_path.clone(),
1944                                            )?;
1945                                        }
1946                                    }
1947                                    let field_value_end = out.position();
1948
1949                                    out.record_field_span(
1950                                        field_path,
1951                                        (field_name_start, field_name_end),
1952                                        (field_value_start, field_value_end),
1953                                    );
1954
1955                                    if !short || i + 1 < variant.data.fields.len() {
1956                                        write!(out, ",")?;
1957                                    }
1958                                }
1959                                if !short {
1960                                    writeln!(out)?;
1961                                    self.indent_to_output(out, format_depth)?;
1962                                }
1963                                write!(out, "}}")?;
1964                            }
1965                            _ => {
1966                                write!(out, "(")?;
1967                                for (i, field) in variant.data.fields.iter().enumerate() {
1968                                    if i > 0 {
1969                                        write!(out, ", ")?;
1970                                    }
1971                                    let mut elem_path = current_path.clone();
1972                                    elem_path
1973                                        .push(PathSegment::Variant(Cow::Borrowed(variant.name)));
1974                                    elem_path.push(PathSegment::Index(i));
1975
1976                                    let elem_start = out.position();
1977                                    if let Ok(Some(field_value)) = enum_peek.field(i) {
1978                                        // Check for field-level proxy
1979                                        if let Some(proxy_def) = field.proxy() {
1980                                            self.format_via_proxy_unified(
1981                                                field_value,
1982                                                proxy_def,
1983                                                out,
1984                                                visited,
1985                                                format_depth + 1,
1986                                                type_depth + 1,
1987                                                short,
1988                                                elem_path.clone(),
1989                                            )?;
1990                                        } else {
1991                                            self.format_unified(
1992                                                field_value,
1993                                                out,
1994                                                visited,
1995                                                format_depth + 1,
1996                                                type_depth + 1,
1997                                                short,
1998                                                elem_path.clone(),
1999                                            )?;
2000                                        }
2001                                    }
2002                                    let elem_end = out.position();
2003                                    out.record_span(elem_path, (elem_start, elem_end));
2004                                }
2005                                write!(out, ")")?;
2006                            }
2007                        }
2008                    }
2009                }
2010            }
2011            _ if value.into_list_like().is_ok() => {
2012                let list = value.into_list_like().unwrap();
2013
2014                // Check if elements are simple scalars - render inline if so
2015                let elem_shape = list.def().t();
2016                let is_simple = Self::shape_chunkiness(elem_shape) <= 1;
2017
2018                write!(out, "[")?;
2019                let len = list.len();
2020                for (i, item) in list.iter().enumerate() {
2021                    if !short && !is_simple {
2022                        writeln!(out)?;
2023                        self.indent_to_output(out, format_depth + 1)?;
2024                    } else if i > 0 {
2025                        write!(out, " ")?;
2026                    }
2027                    let mut elem_path = current_path.clone();
2028                    elem_path.push(PathSegment::Index(i));
2029
2030                    let elem_start = out.position();
2031                    self.format_unified(
2032                        item,
2033                        out,
2034                        visited,
2035                        format_depth + 1,
2036                        type_depth + 1,
2037                        short || is_simple,
2038                        elem_path.clone(),
2039                    )?;
2040                    let elem_end = out.position();
2041                    out.record_span(elem_path, (elem_start, elem_end));
2042
2043                    if (!short && !is_simple) || i + 1 < len {
2044                        write!(out, ",")?;
2045                    }
2046                }
2047                if !short && !is_simple {
2048                    writeln!(out)?;
2049                    self.indent_to_output(out, format_depth)?;
2050                }
2051                write!(out, "]")?;
2052            }
2053            _ if value.into_map().is_ok() => {
2054                let map = value.into_map().unwrap();
2055                write!(out, "{{")?;
2056                for (i, (key, val)) in map.iter().enumerate() {
2057                    if !short {
2058                        writeln!(out)?;
2059                        self.indent_to_output(out, format_depth + 1)?;
2060                    }
2061                    // Format key
2062                    let key_start = out.position();
2063                    self.format_unified(
2064                        key,
2065                        out,
2066                        visited,
2067                        format_depth + 1,
2068                        type_depth + 1,
2069                        true, // short for keys
2070                        vec![],
2071                    )?;
2072                    let key_end = out.position();
2073
2074                    write!(out, ": ")?;
2075
2076                    // Build path for this entry (use key's string representation)
2077                    let key_str = self.format_peek(key);
2078                    let mut entry_path = current_path.clone();
2079                    entry_path.push(PathSegment::Key(Cow::Owned(key_str)));
2080
2081                    let val_start = out.position();
2082                    self.format_unified(
2083                        val,
2084                        out,
2085                        visited,
2086                        format_depth + 1,
2087                        type_depth + 1,
2088                        short,
2089                        entry_path.clone(),
2090                    )?;
2091                    let val_end = out.position();
2092
2093                    out.record_field_span(entry_path, (key_start, key_end), (val_start, val_end));
2094
2095                    if !short || i + 1 < map.len() {
2096                        write!(out, ",")?;
2097                    }
2098                }
2099                if !short && !map.is_empty() {
2100                    writeln!(out)?;
2101                    self.indent_to_output(out, format_depth)?;
2102                }
2103                write!(out, "}}")?;
2104            }
2105            _ => {
2106                // Fallback: just write the type name
2107                write!(out, "{} {{ ... }}", shape)?;
2108            }
2109        }
2110
2111        visited.remove(&value.id());
2112
2113        // Record span for this value
2114        let value_end = out.position();
2115        out.record_span(current_path, (value_start, value_end));
2116
2117        Ok(())
2118    }
2119
2120    fn format_scalar_to_output(&self, value: Peek<'_, '_>, out: &mut impl Write) -> fmt::Result {
2121        // Use Display or Debug trait to format scalar values
2122        if value.shape().is_display() {
2123            write!(out, "{}", value)
2124        } else if value.shape().is_debug() {
2125            write!(out, "{:?}", value)
2126        } else {
2127            write!(out, "{}(…)", value.shape())
2128        }
2129    }
2130
2131    fn indent_to_output(&self, out: &mut impl Write, depth: usize) -> fmt::Result {
2132        for _ in 0..depth {
2133            for _ in 0..self.indent_size {
2134                out.write_char(' ')?;
2135            }
2136        }
2137        Ok(())
2138    }
2139}
2140
2141/// Color mode for the pretty printer.
2142#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2143pub enum ColorMode {
2144    /// Automtically detect whether colors are desired through the `NO_COLOR` environment variable.
2145    Auto,
2146    /// Always enable colors.
2147    Always,
2148    /// Never enable colors.
2149    Never,
2150}
2151
2152impl ColorMode {
2153    /// Convert the color mode to an option of a boolean.
2154    pub fn enabled(&self) -> bool {
2155        static NO_COLOR: LazyLock<bool> = LazyLock::new(|| std::env::var_os("NO_COLOR").is_some());
2156        match self {
2157            ColorMode::Auto => !*NO_COLOR,
2158            ColorMode::Always => true,
2159            ColorMode::Never => false,
2160        }
2161    }
2162}
2163
2164impl From<bool> for ColorMode {
2165    fn from(value: bool) -> Self {
2166        if value {
2167            ColorMode::Always
2168        } else {
2169            ColorMode::Never
2170        }
2171    }
2172}
2173
2174impl From<ColorMode> for Option<bool> {
2175    fn from(value: ColorMode) -> Self {
2176        match value {
2177            ColorMode::Auto => None,
2178            ColorMode::Always => Some(true),
2179            ColorMode::Never => Some(false),
2180        }
2181    }
2182}
2183
2184/// Result of formatting a value with span tracking
2185#[derive(Debug)]
2186pub struct FormattedValue {
2187    /// The formatted text (plain text, no ANSI colors)
2188    pub text: String,
2189    /// Map from paths to their byte spans in `text`
2190    pub spans: BTreeMap<Path, FieldSpan>,
2191}
2192
2193/// Trait for output destinations that may optionally track spans.
2194///
2195/// This allows a single formatting implementation to work with both
2196/// simple string output and span-tracking output.
2197trait FormatOutput: Write {
2198    /// Get the current byte position in the output (for span tracking)
2199    fn position(&self) -> usize;
2200
2201    /// Record a span for a path (value only, key=value)
2202    fn record_span(&mut self, _path: Path, _span: Span) {}
2203
2204    /// Record a span with separate key and value spans
2205    fn record_field_span(&mut self, _path: Path, _key_span: Span, _value_span: Span) {}
2206}
2207
2208/// A wrapper around any Write that implements FormatOutput but doesn't track spans.
2209/// Position tracking is approximated by counting bytes written.
2210#[allow(dead_code)]
2211struct NonTrackingOutput<W> {
2212    inner: W,
2213    position: usize,
2214}
2215
2216#[allow(dead_code)]
2217impl<W> NonTrackingOutput<W> {
2218    const fn new(inner: W) -> Self {
2219        Self { inner, position: 0 }
2220    }
2221}
2222
2223impl<W: Write> Write for NonTrackingOutput<W> {
2224    fn write_str(&mut self, s: &str) -> fmt::Result {
2225        self.position += s.len();
2226        self.inner.write_str(s)
2227    }
2228}
2229
2230impl<W: Write> FormatOutput for NonTrackingOutput<W> {
2231    fn position(&self) -> usize {
2232        self.position
2233    }
2234    // Uses default no-op implementations for span recording
2235}
2236
2237/// Context for tracking spans during value formatting
2238struct SpanTrackingOutput {
2239    output: String,
2240    spans: BTreeMap<Path, FieldSpan>,
2241}
2242
2243impl SpanTrackingOutput {
2244    const fn new() -> Self {
2245        Self {
2246            output: String::new(),
2247            spans: BTreeMap::new(),
2248        }
2249    }
2250
2251    fn into_formatted_value(self) -> FormattedValue {
2252        FormattedValue {
2253            text: self.output,
2254            spans: self.spans,
2255        }
2256    }
2257}
2258
2259impl Write for SpanTrackingOutput {
2260    fn write_str(&mut self, s: &str) -> fmt::Result {
2261        self.output.push_str(s);
2262        Ok(())
2263    }
2264}
2265
2266impl FormatOutput for SpanTrackingOutput {
2267    fn position(&self) -> usize {
2268        self.output.len()
2269    }
2270
2271    fn record_span(&mut self, path: Path, span: Span) {
2272        self.spans.insert(
2273            path,
2274            FieldSpan {
2275                key: span,
2276                value: span,
2277            },
2278        );
2279    }
2280
2281    fn record_field_span(&mut self, path: Path, key_span: Span, value_span: Span) {
2282        self.spans.insert(
2283            path,
2284            FieldSpan {
2285                key: key_span,
2286                value: value_span,
2287            },
2288        );
2289    }
2290}
2291
2292#[cfg(test)]
2293mod tests {
2294    use super::*;
2295
2296    // Basic tests for the PrettyPrinter
2297    #[test]
2298    fn test_pretty_printer_default() {
2299        let printer = PrettyPrinter::default();
2300        assert_eq!(printer.indent_size, 2);
2301        assert_eq!(printer.max_depth, None);
2302        // use_colors defaults to true unless NO_COLOR is set
2303        // In tests, NO_COLOR=1 is set via nextest config for consistent snapshots
2304        assert_eq!(printer.use_colors(), std::env::var_os("NO_COLOR").is_none());
2305    }
2306
2307    #[test]
2308    fn test_pretty_printer_with_methods() {
2309        let printer = PrettyPrinter::new()
2310            .with_indent_size(4)
2311            .with_max_depth(3)
2312            .with_colors(ColorMode::Never);
2313
2314        assert_eq!(printer.indent_size, 4);
2315        assert_eq!(printer.max_depth, Some(3));
2316        assert!(!printer.use_colors());
2317    }
2318
2319    #[test]
2320    fn test_format_peek_with_spans() {
2321        use crate::PathSegment;
2322        use facet_reflect::Peek;
2323
2324        // Test with a simple tuple - no need for custom struct
2325        let value = ("Alice", 30u32);
2326
2327        let printer = PrettyPrinter::new();
2328        let formatted = printer.format_peek_with_spans(Peek::new(&value));
2329
2330        // Check that we got output
2331        assert!(!formatted.text.is_empty());
2332        assert!(formatted.text.contains("Alice"));
2333        assert!(formatted.text.contains("30"));
2334
2335        // Check that spans were recorded
2336        assert!(!formatted.spans.is_empty());
2337
2338        // Check that the root span exists (empty path)
2339        assert!(formatted.spans.contains_key(&vec![]));
2340
2341        // Check that index spans exist
2342        let idx0_path = vec![PathSegment::Index(0)];
2343        let idx1_path = vec![PathSegment::Index(1)];
2344        assert!(
2345            formatted.spans.contains_key(&idx0_path),
2346            "index 0 span not found"
2347        );
2348        assert!(
2349            formatted.spans.contains_key(&idx1_path),
2350            "index 1 span not found"
2351        );
2352    }
2353
2354    #[test]
2355    fn test_max_content_len_string() {
2356        let printer = PrettyPrinter::new()
2357            .with_colors(ColorMode::Never)
2358            .with_max_content_len(20);
2359
2360        // Short string - no truncation
2361        let short = "hello";
2362        let output = printer.format(&short);
2363        assert_eq!(output, "\"hello\"");
2364
2365        // Long string - should truncate middle
2366        let long = "abcdefghijklmnopqrstuvwxyz0123456789";
2367        let output = printer.format(&long);
2368        assert!(
2369            output.contains("..."),
2370            "should contain ellipsis: {}",
2371            output
2372        );
2373        assert!(output.contains("chars"), "should mention chars: {}", output);
2374        assert!(
2375            output.starts_with("\"abc"),
2376            "should start with beginning: {}",
2377            output
2378        );
2379        assert!(
2380            output.ends_with("89\""),
2381            "should end with ending: {}",
2382            output
2383        );
2384    }
2385
2386    #[test]
2387    fn test_max_content_len_bytes() {
2388        let printer = PrettyPrinter::new()
2389            .with_colors(ColorMode::Never)
2390            .with_max_content_len(10);
2391
2392        // Short bytes - no truncation
2393        let short: Vec<u8> = vec![1, 2, 3];
2394        let output = printer.format(&short);
2395        assert!(
2396            output.contains("01 02 03"),
2397            "should show all bytes: {}",
2398            output
2399        );
2400
2401        // Long bytes - should truncate middle
2402        let long: Vec<u8> = (0..50).collect();
2403        let output = printer.format(&long);
2404        assert!(
2405            output.contains("..."),
2406            "should contain ellipsis: {}",
2407            output
2408        );
2409        assert!(output.contains("bytes"), "should mention bytes: {}", output);
2410    }
2411
2412    #[test]
2413    fn test_max_content_formatting() {
2414        let printer = PrettyPrinter::new()
2415            .with_colors(ColorMode::Never)
2416            .with_max_content_len(12);
2417        let data: Vec<u8> = (0..50).collect();
2418        let output = printer.format(&data);
2419        insta::assert_snapshot!(output, @"
2420        Vec<u8> [
2421           00 01 02 03 04 05
2422           ...(38 bytes)...
2423           2c 2d 2e 2f 30 31
2424        ]
2425        ");
2426    }
2427
2428    #[test]
2429    fn test_max_collection_len_sequence() {
2430        let printer = PrettyPrinter::new()
2431            .with_colors(ColorMode::Never)
2432            .with_max_collection_len(3);
2433
2434        let value = vec![1u32, 2, 3, 4, 5];
2435        let output = printer.format(&value);
2436
2437        assert!(output.contains("1"));
2438        assert!(output.contains("2"));
2439        assert!(output.contains("3"));
2440        assert!(!output.contains("5"));
2441        assert!(output.contains("more items"), "output: {output}");
2442    }
2443
2444    #[test]
2445    fn test_max_collection_len_struct_fields() {
2446        #[derive(facet::Facet)]
2447        struct Record {
2448            alpha: u32,
2449            beta: u32,
2450            gamma: u32,
2451            delta: u32,
2452        }
2453
2454        let printer = PrettyPrinter::new()
2455            .with_colors(ColorMode::Never)
2456            .with_max_collection_len(2);
2457
2458        let value = Record {
2459            alpha: 1,
2460            beta: 2,
2461            gamma: 3,
2462            delta: 4,
2463        };
2464        let output = printer.format(&value);
2465
2466        assert!(output.contains("alpha"));
2467        assert!(output.contains("beta"));
2468        assert!(!output.contains("gamma"));
2469        assert!(output.contains("more fields"), "output: {output}");
2470    }
2471}