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;
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
111pub struct PrettyPrinter {
112    /// usize::MAX is a special value that means indenting with tabs instead of spaces
113    indent_size: usize,
114    max_depth: Option<usize>,
115    color_generator: ColorGenerator,
116    use_colors: bool,
117    list_u8_as_bytes: bool,
118    /// Skip type names for Options (show `Some(x)` instead of `Option<T>::Some(x)`)
119    minimal_option_names: bool,
120    /// Whether to show doc comments in output
121    show_doc_comments: bool,
122}
123
124impl Default for PrettyPrinter {
125    fn default() -> Self {
126        Self {
127            indent_size: 2,
128            max_depth: None,
129            color_generator: ColorGenerator::default(),
130            use_colors: std::env::var_os("NO_COLOR").is_none(),
131            list_u8_as_bytes: true,
132            minimal_option_names: false,
133            show_doc_comments: false,
134        }
135    }
136}
137
138impl PrettyPrinter {
139    /// Create a new PrettyPrinter with default settings
140    pub fn new() -> Self {
141        Self::default()
142    }
143
144    /// Set the indentation size
145    pub fn with_indent_size(mut self, size: usize) -> Self {
146        self.indent_size = size;
147        self
148    }
149
150    /// Set the maximum depth for recursive printing
151    pub fn with_max_depth(mut self, depth: usize) -> Self {
152        self.max_depth = Some(depth);
153        self
154    }
155
156    /// Set the color generator
157    pub fn with_color_generator(mut self, generator: ColorGenerator) -> Self {
158        self.color_generator = generator;
159        self
160    }
161
162    /// Enable or disable colors
163    pub fn with_colors(mut self, use_colors: bool) -> Self {
164        self.use_colors = use_colors;
165        self
166    }
167
168    /// Use minimal names for Options (show `Some(x)` instead of `Option<T>::Some(x)`)
169    pub fn with_minimal_option_names(mut self, minimal: bool) -> Self {
170        self.minimal_option_names = minimal;
171        self
172    }
173
174    /// Enable or disable doc comments in output
175    pub fn with_doc_comments(mut self, show: bool) -> Self {
176        self.show_doc_comments = show;
177        self
178    }
179
180    /// Format a value to a string
181    pub fn format<'a, T: ?Sized + Facet<'a>>(&self, value: &T) -> String {
182        let value = Peek::new(value);
183
184        let mut output = String::new();
185        self.format_peek_internal(value, &mut output, &mut BTreeMap::new())
186            .expect("Formatting failed");
187
188        output
189    }
190
191    /// Format a value to a formatter
192    pub fn format_to<'a, T: ?Sized + Facet<'a>>(
193        &self,
194        value: &T,
195        f: &mut fmt::Formatter<'_>,
196    ) -> fmt::Result {
197        let value = Peek::new(value);
198        self.format_peek_internal(value, f, &mut BTreeMap::new())
199    }
200
201    /// Format a value to a string
202    pub fn format_peek(&self, value: Peek<'_, '_>) -> String {
203        let mut output = String::new();
204        self.format_peek_internal(value, &mut output, &mut BTreeMap::new())
205            .expect("Formatting failed");
206        output
207    }
208
209    pub(crate) fn shape_chunkiness(shape: &Shape) -> usize {
210        let mut shape = shape;
211        while let Type::Pointer(PointerType::Reference(inner)) = shape.ty {
212            shape = inner.target;
213        }
214
215        match shape.ty {
216            Type::Pointer(_) | Type::Primitive(_) => 1,
217            Type::Sequence(SequenceType::Array(ty)) => {
218                Self::shape_chunkiness(ty.t).saturating_mul(ty.n)
219            }
220            Type::Sequence(SequenceType::Slice(_)) => usize::MAX,
221            Type::User(ty) => match ty {
222                UserType::Struct(ty) => {
223                    let mut sum = 0usize;
224                    for field in ty.fields {
225                        sum = sum.saturating_add(Self::shape_chunkiness(field.shape()));
226                    }
227                    sum
228                }
229                UserType::Enum(ty) => {
230                    let mut max = 0usize;
231                    for variant in ty.variants {
232                        max = Ord::max(max, {
233                            let mut sum = 0usize;
234                            for field in variant.data.fields {
235                                sum = sum.saturating_add(Self::shape_chunkiness(field.shape()));
236                            }
237                            sum
238                        })
239                    }
240                    max
241                }
242                UserType::Opaque | UserType::Union(_) => 1,
243            },
244            Type::Undefined => 1,
245        }
246    }
247
248    #[allow(clippy::too_many_arguments)]
249    pub(crate) fn format_peek_internal_(
250        &self,
251        value: Peek<'_, '_>,
252        f: &mut dyn Write,
253        visited: &mut BTreeMap<ValueId, usize>,
254        format_depth: usize,
255        type_depth: usize,
256        short: bool,
257    ) -> fmt::Result {
258        let mut value = value;
259        while let Ok(ptr) = value.into_pointer()
260            && let Some(pointee) = ptr.borrow_inner()
261        {
262            value = pointee;
263        }
264        let shape = value.shape();
265
266        if let Some(prev_type_depth) = visited.insert(value.id(), type_depth) {
267            self.write_type_name(f, &value)?;
268            self.write_punctuation(f, " { ")?;
269            self.write_comment(
270                f,
271                &format!(
272                    "/* cycle detected at {} (first seen at type_depth {}) */",
273                    value.id(),
274                    prev_type_depth,
275                ),
276            )?;
277            visited.remove(&value.id());
278            return Ok(());
279        }
280
281        match (shape.def, shape.ty) {
282            (_, Type::Primitive(PrimitiveType::Textual(TextualType::Str))) => {
283                let value = value.get::<str>().unwrap();
284                let mut hashes = 0usize;
285
286                let mut rest = value;
287                while let Some(idx) = rest.find('"') {
288                    rest = &rest[idx + 1..];
289                    let before = rest.len();
290                    rest = rest.trim_start_matches('#');
291                    let after = rest.len();
292                    let count = before - after;
293                    hashes = Ord::max(hashes, 1 + count);
294                }
295
296                let pad = "";
297                let width = hashes.saturating_sub(1);
298                if hashes > 0 {
299                    write!(f, "r{pad:#<width$}")?;
300                }
301                write!(f, "\"")?;
302                if self.use_colors {
303                    write!(f, "{}", value.color(tokyo_night::STRING))?;
304                } else {
305                    write!(f, "{value}")?;
306                }
307                write!(f, "\"")?;
308                if hashes > 0 {
309                    write!(f, "{pad:#<width$}")?;
310                }
311            }
312            // Handle String specially to add quotes (like &str)
313            (Def::Scalar, _) if value.shape().id == <alloc::string::String as Facet>::SHAPE.id => {
314                let s = value.get::<alloc::string::String>().unwrap();
315                write!(f, "\"")?;
316                if self.use_colors {
317                    write!(f, "{}", s.color(tokyo_night::STRING))?;
318                } else {
319                    write!(f, "{s}")?;
320                }
321                write!(f, "\"")?;
322            }
323            (Def::Scalar, _) => self.format_scalar(value, f)?,
324            (Def::Option(_), _) => {
325                let option = value.into_option().unwrap();
326
327                // Print the Option name (unless minimal mode)
328                if !self.minimal_option_names {
329                    self.write_type_name(f, &value)?;
330                }
331
332                if let Some(inner) = option.value() {
333                    let prefix = if self.minimal_option_names {
334                        "Some("
335                    } else {
336                        "::Some("
337                    };
338                    self.write_punctuation(f, prefix)?;
339                    self.format_peek_internal_(
340                        inner,
341                        f,
342                        visited,
343                        format_depth,
344                        type_depth + 1,
345                        short,
346                    )?;
347                    self.write_punctuation(f, ")")?;
348                } else {
349                    let suffix = if self.minimal_option_names {
350                        "None"
351                    } else {
352                        "::None"
353                    };
354                    self.write_punctuation(f, suffix)?;
355                }
356            }
357
358            (_, Type::Pointer(PointerType::Raw(_) | PointerType::Function(_))) => {
359                self.write_type_name(f, &value)?;
360                let addr = unsafe { value.data().read::<*const ()>() };
361                let value = Peek::new(&addr);
362                self.format_scalar(value, f)?;
363            }
364
365            (_, Type::User(UserType::Union(_))) => {
366                if !short && self.show_doc_comments {
367                    for &line in shape.doc {
368                        self.write_comment(f, &format!("///{line}"))?;
369                        writeln!(f)?;
370                        self.indent(f, format_depth)?;
371                    }
372                }
373                self.write_type_name(f, &value)?;
374
375                self.write_punctuation(f, " { ")?;
376                self.write_comment(f, "/* contents of untagged union */")?;
377                self.write_punctuation(f, " }")?;
378            }
379
380            (
381                _,
382                Type::User(UserType::Struct(
383                    ty @ StructType {
384                        kind: StructKind::Tuple | StructKind::TupleStruct,
385                        ..
386                    },
387                )),
388            ) => {
389                if !short && self.show_doc_comments {
390                    for &line in shape.doc {
391                        self.write_comment(f, &format!("///{line}"))?;
392                        writeln!(f)?;
393                        self.indent(f, format_depth)?;
394                    }
395                }
396
397                self.write_type_name(f, &value)?;
398                if matches!(ty.kind, StructKind::Tuple) {
399                    write!(f, " ")?;
400                }
401                let value = value.into_struct().unwrap();
402
403                let fields = ty.fields;
404                self.format_tuple_fields(
405                    &|i| value.field(i).unwrap(),
406                    f,
407                    visited,
408                    format_depth,
409                    type_depth,
410                    fields,
411                    short,
412                    matches!(ty.kind, StructKind::Tuple),
413                )?;
414            }
415
416            (
417                _,
418                Type::User(UserType::Struct(
419                    ty @ StructType {
420                        kind: StructKind::Struct | StructKind::Unit,
421                        ..
422                    },
423                )),
424            ) => {
425                if !short && self.show_doc_comments {
426                    for &line in shape.doc {
427                        self.write_comment(f, &format!("///{line}"))?;
428                        writeln!(f)?;
429                        self.indent(f, format_depth)?;
430                    }
431                }
432
433                self.write_type_name(f, &value)?;
434
435                if matches!(ty.kind, StructKind::Struct) {
436                    let value = value.into_struct().unwrap();
437                    self.format_struct_fields(
438                        &|i| value.field(i).unwrap(),
439                        f,
440                        visited,
441                        format_depth,
442                        type_depth,
443                        ty.fields,
444                        short,
445                    )?;
446                }
447            }
448
449            (_, Type::User(UserType::Enum(_))) => {
450                let enum_peek = value.into_enum().unwrap();
451                match enum_peek.active_variant() {
452                    Err(_) => {
453                        // Print the enum name
454                        self.write_type_name(f, &value)?;
455                        self.write_punctuation(f, " {")?;
456                        self.write_comment(f, " /* cannot determine variant */ ")?;
457                        self.write_punctuation(f, "}")?;
458                    }
459                    Ok(variant) => {
460                        if !short && self.show_doc_comments {
461                            for &line in shape.doc {
462                                self.write_comment(f, &format!("///{line}"))?;
463                                writeln!(f)?;
464                                self.indent(f, format_depth)?;
465                            }
466                            for &line in variant.doc {
467                                self.write_comment(f, &format!("///{line}"))?;
468                                writeln!(f)?;
469                                self.indent(f, format_depth)?;
470                            }
471                        }
472                        self.write_type_name(f, &value)?;
473                        self.write_punctuation(f, "::")?;
474
475                        // Variant docs are already handled above
476
477                        // Get the active variant name - we've already checked above that we can get it
478                        // This is the same variant, but we're repeating the code here to ensure consistency
479
480                        // Apply color for variant name
481                        if self.use_colors {
482                            write!(f, "{}", variant.name.bold())?;
483                        } else {
484                            write!(f, "{}", variant.name)?;
485                        }
486
487                        // Process the variant fields based on the variant kind
488                        match variant.data.kind {
489                            StructKind::Unit => {
490                                // Unit variant has no fields, nothing more to print
491                            }
492                            StructKind::Struct => self.format_struct_fields(
493                                &|i| enum_peek.field(i).unwrap().unwrap(),
494                                f,
495                                visited,
496                                format_depth,
497                                type_depth,
498                                variant.data.fields,
499                                short,
500                            )?,
501                            _ => self.format_tuple_fields(
502                                &|i| enum_peek.field(i).unwrap().unwrap(),
503                                f,
504                                visited,
505                                format_depth,
506                                type_depth,
507                                variant.data.fields,
508                                short,
509                                false,
510                            )?,
511                        }
512                    }
513                };
514            }
515
516            _ if value.into_list_like().is_ok() => {
517                let list = value.into_list_like().unwrap();
518
519                // When recursing into a list, always increment format_depth
520                // Only increment type_depth if we're moving to a different address
521
522                // Print the list name
523                self.write_type_name(f, &value)?;
524
525                if !list.is_empty() {
526                    if list.def().t().is_type::<u8>() && self.list_u8_as_bytes {
527                        self.write_punctuation(f, " [")?;
528                        for (idx, item) in list.iter().enumerate() {
529                            if !short && idx % 16 == 0 {
530                                writeln!(f)?;
531                                self.indent(f, format_depth + 1)?;
532                            }
533                            write!(f, " ")?;
534
535                            let byte = *item.get::<u8>().unwrap();
536                            if self.use_colors {
537                                let mut hasher = DefaultHasher::new();
538                                byte.hash(&mut hasher);
539                                let hash = hasher.finish();
540                                let color = self.color_generator.generate_color(hash);
541                                let rgb = Rgb(color.r, color.g, color.b);
542                                write!(f, "{}", format!("{byte:02x}").color(rgb))?;
543                            } else {
544                                write!(f, "{byte:02x}")?;
545                            }
546                        }
547                        if !short {
548                            writeln!(f)?;
549                            self.indent(f, format_depth)?;
550                        }
551                        self.write_punctuation(f, "]")?;
552                    } else {
553                        // Check if elements are simple scalars - render inline if so
554                        let elem_shape = list.def().t();
555                        let is_simple = Self::shape_chunkiness(elem_shape) <= 1;
556
557                        self.write_punctuation(f, " [")?;
558                        let len = list.len();
559                        for (idx, item) in list.iter().enumerate() {
560                            if !short && !is_simple {
561                                writeln!(f)?;
562                                self.indent(f, format_depth + 1)?;
563                            } else if idx > 0 {
564                                write!(f, " ")?;
565                            }
566                            self.format_peek_internal_(
567                                item,
568                                f,
569                                visited,
570                                format_depth + 1,
571                                type_depth + 1,
572                                short || is_simple,
573                            )?;
574
575                            if (!short && !is_simple) || idx + 1 < len {
576                                self.write_punctuation(f, ",")?;
577                            }
578                        }
579                        if !short && !is_simple {
580                            writeln!(f)?;
581                            self.indent(f, format_depth)?;
582                        }
583                        self.write_punctuation(f, "]")?;
584                    }
585                } else {
586                    self.write_punctuation(f, "[]")?;
587                }
588            }
589
590            _ if value.into_set().is_ok() => {
591                self.write_type_name(f, &value)?;
592
593                let value = value.into_set().unwrap();
594                self.write_punctuation(f, " [")?;
595                if !value.is_empty() {
596                    let len = value.len();
597                    for (idx, item) in value.iter().enumerate() {
598                        if !short {
599                            writeln!(f)?;
600                            self.indent(f, format_depth + 1)?;
601                        }
602                        self.format_peek_internal_(
603                            item,
604                            f,
605                            visited,
606                            format_depth + 1,
607                            type_depth + 1,
608                            short,
609                        )?;
610                        if !short || idx + 1 < len {
611                            self.write_punctuation(f, ",")?;
612                        } else {
613                            write!(f, " ")?;
614                        }
615                    }
616                    if !short {
617                        writeln!(f)?;
618                        self.indent(f, format_depth)?;
619                    }
620                }
621                self.write_punctuation(f, "]")?;
622            }
623
624            (Def::Map(def), _) => {
625                let key_is_short = Self::shape_chunkiness(def.k) <= 2;
626
627                self.write_type_name(f, &value)?;
628
629                let value = value.into_map().unwrap();
630                self.write_punctuation(f, " [")?;
631
632                if !value.is_empty() {
633                    let len = value.len();
634                    for (idx, (key, value)) in value.iter().enumerate() {
635                        if !short {
636                            writeln!(f)?;
637                            self.indent(f, format_depth + 1)?;
638                        }
639                        self.format_peek_internal_(
640                            key,
641                            f,
642                            visited,
643                            format_depth + 1,
644                            type_depth + 1,
645                            key_is_short,
646                        )?;
647                        self.write_punctuation(f, " => ")?;
648                        self.format_peek_internal_(
649                            value,
650                            f,
651                            visited,
652                            format_depth + 1,
653                            type_depth + 1,
654                            short,
655                        )?;
656                        if !short || idx + 1 < len {
657                            self.write_punctuation(f, ",")?;
658                        } else {
659                            write!(f, " ")?;
660                        }
661                    }
662                    if !short {
663                        writeln!(f)?;
664                        self.indent(f, format_depth)?;
665                    }
666                }
667
668                self.write_punctuation(f, "]")?;
669            }
670
671            (Def::DynamicValue(_), _) => {
672                let dyn_val = value.into_dynamic_value().unwrap();
673                match dyn_val.kind() {
674                    DynValueKind::Null => {
675                        self.write_keyword(f, "null")?;
676                    }
677                    DynValueKind::Bool => {
678                        if let Some(b) = dyn_val.as_bool() {
679                            self.write_keyword(f, if b { "true" } else { "false" })?;
680                        }
681                    }
682                    DynValueKind::Number => {
683                        if let Some(n) = dyn_val.as_i64() {
684                            self.format_number(f, &n.to_string())?;
685                        } else if let Some(n) = dyn_val.as_u64() {
686                            self.format_number(f, &n.to_string())?;
687                        } else if let Some(n) = dyn_val.as_f64() {
688                            self.format_number(f, &n.to_string())?;
689                        }
690                    }
691                    DynValueKind::String => {
692                        if let Some(s) = dyn_val.as_str() {
693                            self.format_string(f, s)?;
694                        }
695                    }
696                    DynValueKind::Bytes => {
697                        if let Some(bytes) = dyn_val.as_bytes() {
698                            self.format_bytes(f, bytes)?;
699                        }
700                    }
701                    DynValueKind::Array => {
702                        let len = dyn_val.array_len().unwrap_or(0);
703                        if len == 0 {
704                            self.write_punctuation(f, "[]")?;
705                        } else {
706                            self.write_punctuation(f, "[")?;
707                            for idx in 0..len {
708                                if !short {
709                                    writeln!(f)?;
710                                    self.indent(f, format_depth + 1)?;
711                                }
712                                if let Some(elem) = dyn_val.array_get(idx) {
713                                    self.format_peek_internal_(
714                                        elem,
715                                        f,
716                                        visited,
717                                        format_depth + 1,
718                                        type_depth + 1,
719                                        short,
720                                    )?;
721                                }
722                                if !short || idx + 1 < len {
723                                    self.write_punctuation(f, ",")?;
724                                } else {
725                                    write!(f, " ")?;
726                                }
727                            }
728                            if !short {
729                                writeln!(f)?;
730                                self.indent(f, format_depth)?;
731                            }
732                            self.write_punctuation(f, "]")?;
733                        }
734                    }
735                    DynValueKind::Object => {
736                        let len = dyn_val.object_len().unwrap_or(0);
737                        if len == 0 {
738                            self.write_punctuation(f, "{}")?;
739                        } else {
740                            self.write_punctuation(f, "{")?;
741                            for idx in 0..len {
742                                if !short {
743                                    writeln!(f)?;
744                                    self.indent(f, format_depth + 1)?;
745                                }
746                                if let Some((key, val)) = dyn_val.object_get_entry(idx) {
747                                    self.write_field_name(f, key)?;
748                                    self.write_punctuation(f, ": ")?;
749                                    self.format_peek_internal_(
750                                        val,
751                                        f,
752                                        visited,
753                                        format_depth + 1,
754                                        type_depth + 1,
755                                        short,
756                                    )?;
757                                }
758                                if !short || idx + 1 < len {
759                                    self.write_punctuation(f, ",")?;
760                                } else {
761                                    write!(f, " ")?;
762                                }
763                            }
764                            if !short {
765                                writeln!(f)?;
766                                self.indent(f, format_depth)?;
767                            }
768                            self.write_punctuation(f, "}")?;
769                        }
770                    }
771                    DynValueKind::DateTime => {
772                        // Format datetime using the vtable's get_datetime
773                        #[allow(clippy::uninlined_format_args)]
774                        if let Some((year, month, day, hour, minute, second, nanos, kind)) =
775                            dyn_val.as_datetime()
776                        {
777                            match kind {
778                                DynDateTimeKind::Offset { offset_minutes } => {
779                                    if nanos > 0 {
780                                        write!(
781                                            f,
782                                            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09}",
783                                            year, month, day, hour, minute, second, nanos
784                                        )?;
785                                    } else {
786                                        write!(
787                                            f,
788                                            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
789                                            year, month, day, hour, minute, second
790                                        )?;
791                                    }
792                                    if offset_minutes == 0 {
793                                        write!(f, "Z")?;
794                                    } else {
795                                        let sign = if offset_minutes >= 0 { '+' } else { '-' };
796                                        let abs = offset_minutes.abs();
797                                        write!(f, "{}{:02}:{:02}", sign, abs / 60, abs % 60)?;
798                                    }
799                                }
800                                DynDateTimeKind::LocalDateTime => {
801                                    if nanos > 0 {
802                                        write!(
803                                            f,
804                                            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09}",
805                                            year, month, day, hour, minute, second, nanos
806                                        )?;
807                                    } else {
808                                        write!(
809                                            f,
810                                            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
811                                            year, month, day, hour, minute, second
812                                        )?;
813                                    }
814                                }
815                                DynDateTimeKind::LocalDate => {
816                                    write!(f, "{:04}-{:02}-{:02}", year, month, day)?;
817                                }
818                                DynDateTimeKind::LocalTime => {
819                                    if nanos > 0 {
820                                        write!(
821                                            f,
822                                            "{:02}:{:02}:{:02}.{:09}",
823                                            hour, minute, second, nanos
824                                        )?;
825                                    } else {
826                                        write!(f, "{:02}:{:02}:{:02}", hour, minute, second)?;
827                                    }
828                                }
829                            }
830                        }
831                    }
832                    DynValueKind::QName => {
833                        // QName formatting is not yet supported via vtable
834                        write!(f, "<qname>")?;
835                    }
836                    DynValueKind::Uuid => {
837                        // UUID formatting is not yet supported via vtable
838                        write!(f, "<uuid>")?;
839                    }
840                }
841            }
842
843            _ => write!(f, "unsupported peek variant: {value:?}")?,
844        }
845
846        Ok(())
847    }
848
849    #[allow(clippy::too_many_arguments)]
850    fn format_tuple_fields<'mem, 'facet>(
851        &self,
852        peek_field: &dyn Fn(usize) -> Peek<'mem, 'facet>,
853        f: &mut dyn Write,
854        visited: &mut BTreeMap<ValueId, usize>,
855        format_depth: usize,
856        type_depth: usize,
857        fields: &[Field],
858        short: bool,
859        force_trailing_comma: bool,
860    ) -> fmt::Result {
861        self.write_punctuation(f, "(")?;
862        if let [field] = fields
863            && field.doc.is_empty()
864        {
865            let field = peek_field(0);
866            self.format_peek_internal_(field, f, visited, format_depth, type_depth, short)?;
867
868            if force_trailing_comma {
869                self.write_punctuation(f, ",")?;
870            }
871        } else if !fields.is_empty() {
872            for idx in 0..fields.len() {
873                if !short {
874                    writeln!(f)?;
875                    self.indent(f, format_depth + 1)?;
876
877                    if self.show_doc_comments {
878                        for &line in fields[idx].doc {
879                            self.write_comment(f, &format!("///{line}"))?;
880                            writeln!(f)?;
881                            self.indent(f, format_depth + 1)?;
882                        }
883                    }
884                }
885
886                if fields[idx].is_sensitive() {
887                    self.write_redacted(f, "[REDACTED]")?;
888                } else {
889                    self.format_peek_internal_(
890                        peek_field(idx),
891                        f,
892                        visited,
893                        format_depth + 1,
894                        type_depth + 1,
895                        short,
896                    )?;
897                }
898
899                if !short || idx + 1 < fields.len() {
900                    self.write_punctuation(f, ",")?;
901                } else {
902                    write!(f, " ")?;
903                }
904            }
905            if !short {
906                writeln!(f)?;
907                self.indent(f, format_depth)?;
908            }
909        }
910        self.write_punctuation(f, ")")?;
911        Ok(())
912    }
913
914    #[allow(clippy::too_many_arguments)]
915    fn format_struct_fields<'mem, 'facet>(
916        &self,
917        peek_field: &dyn Fn(usize) -> Peek<'mem, 'facet>,
918        f: &mut dyn Write,
919        visited: &mut BTreeMap<ValueId, usize>,
920        format_depth: usize,
921        type_depth: usize,
922        fields: &[Field],
923        short: bool,
924    ) -> fmt::Result {
925        self.write_punctuation(f, " {")?;
926        if !fields.is_empty() {
927            for idx in 0..fields.len() {
928                if !short {
929                    writeln!(f)?;
930                    self.indent(f, format_depth + 1)?;
931                }
932
933                if self.show_doc_comments {
934                    for &line in fields[idx].doc {
935                        self.write_comment(f, &format!("///{line}"))?;
936                        writeln!(f)?;
937                        self.indent(f, format_depth + 1)?;
938                    }
939                }
940
941                self.write_field_name(f, fields[idx].name)?;
942                self.write_punctuation(f, ": ")?;
943                if fields[idx].is_sensitive() {
944                    self.write_redacted(f, "[REDACTED]")?;
945                } else {
946                    self.format_peek_internal_(
947                        peek_field(idx),
948                        f,
949                        visited,
950                        format_depth + 1,
951                        type_depth + 1,
952                        short,
953                    )?;
954                }
955
956                if !short || idx + 1 < fields.len() {
957                    self.write_punctuation(f, ",")?;
958                } else {
959                    write!(f, " ")?;
960                }
961            }
962            if !short {
963                writeln!(f)?;
964                self.indent(f, format_depth)?;
965            }
966        }
967        self.write_punctuation(f, "}")?;
968        Ok(())
969    }
970
971    fn indent(&self, f: &mut dyn Write, indent: usize) -> fmt::Result {
972        if self.indent_size == usize::MAX {
973            write!(f, "{:\t<width$}", "", width = indent)
974        } else {
975            write!(f, "{: <width$}", "", width = indent * self.indent_size)
976        }
977    }
978
979    /// Internal method to format a Peek value
980    pub(crate) fn format_peek_internal(
981        &self,
982        value: Peek<'_, '_>,
983        f: &mut dyn Write,
984        visited: &mut BTreeMap<ValueId, usize>,
985    ) -> fmt::Result {
986        self.format_peek_internal_(value, f, visited, 0, 0, false)
987    }
988
989    /// Format a scalar value
990    fn format_scalar(&self, value: Peek, f: &mut dyn Write) -> fmt::Result {
991        // Generate a color for this shape
992        let mut hasher = DefaultHasher::new();
993        value.shape().id.hash(&mut hasher);
994        let hash = hasher.finish();
995        let color = self.color_generator.generate_color(hash);
996
997        // Display the value
998        struct DisplayWrapper<'mem, 'facet>(&'mem Peek<'mem, 'facet>);
999
1000        impl fmt::Display for DisplayWrapper<'_, '_> {
1001            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1002                if self.0.shape().is_display() {
1003                    write!(f, "{}", self.0)?;
1004                } else if self.0.shape().is_debug() {
1005                    write!(f, "{:?}", self.0)?;
1006                } else {
1007                    write!(f, "{}", self.0.shape())?;
1008                    write!(f, "(…)")?;
1009                }
1010                Ok(())
1011            }
1012        }
1013
1014        // Apply color if needed and display
1015        if self.use_colors {
1016            let rgb = Rgb(color.r, color.g, color.b);
1017            write!(f, "{}", DisplayWrapper(&value).color(rgb))?;
1018        } else {
1019            write!(f, "{}", DisplayWrapper(&value))?;
1020        }
1021
1022        Ok(())
1023    }
1024
1025    /// Write a keyword (null, true, false) with coloring
1026    fn write_keyword(&self, f: &mut dyn Write, keyword: &str) -> fmt::Result {
1027        if self.use_colors {
1028            write!(f, "{}", keyword.color(tokyo_night::KEYWORD))
1029        } else {
1030            write!(f, "{keyword}")
1031        }
1032    }
1033
1034    /// Format a number for dynamic values
1035    fn format_number(&self, f: &mut dyn Write, s: &str) -> fmt::Result {
1036        if self.use_colors {
1037            write!(f, "{}", s.color(tokyo_night::NUMBER))
1038        } else {
1039            write!(f, "{s}")
1040        }
1041    }
1042
1043    /// Format a string for dynamic values
1044    fn format_string(&self, f: &mut dyn Write, s: &str) -> fmt::Result {
1045        if self.use_colors {
1046            write!(f, "\"{}\"", s.color(tokyo_night::STRING))
1047        } else {
1048            write!(f, "{s:?}")
1049        }
1050    }
1051
1052    /// Format bytes for dynamic values
1053    fn format_bytes(&self, f: &mut dyn Write, bytes: &[u8]) -> fmt::Result {
1054        write!(f, "b\"")?;
1055        for byte in bytes {
1056            write!(f, "\\x{byte:02x}")?;
1057        }
1058        write!(f, "\"")
1059    }
1060
1061    /// Write styled type name to formatter
1062    fn write_type_name(&self, f: &mut dyn Write, peek: &Peek) -> fmt::Result {
1063        struct TypeNameWriter<'mem, 'facet>(&'mem Peek<'mem, 'facet>);
1064
1065        impl core::fmt::Display for TypeNameWriter<'_, '_> {
1066            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1067                self.0.type_name(f, TypeNameOpts::infinite())
1068            }
1069        }
1070        let type_name = TypeNameWriter(peek);
1071
1072        if self.use_colors {
1073            write!(f, "{}", type_name.color(tokyo_night::TYPE_NAME).bold())
1074        } else {
1075            write!(f, "{type_name}")
1076        }
1077    }
1078
1079    /// Style a type name and return it as a string
1080    #[allow(dead_code)]
1081    fn style_type_name(&self, peek: &Peek) -> String {
1082        let mut result = String::new();
1083        self.write_type_name(&mut result, peek).unwrap();
1084        result
1085    }
1086
1087    /// Write styled field name to formatter
1088    fn write_field_name(&self, f: &mut dyn Write, name: &str) -> fmt::Result {
1089        if self.use_colors {
1090            write!(f, "{}", name.color(tokyo_night::FIELD_NAME))
1091        } else {
1092            write!(f, "{name}")
1093        }
1094    }
1095
1096    /// Write styled punctuation to formatter
1097    fn write_punctuation(&self, f: &mut dyn Write, text: &str) -> fmt::Result {
1098        if self.use_colors {
1099            write!(f, "{}", text.dimmed())
1100        } else {
1101            write!(f, "{text}")
1102        }
1103    }
1104
1105    /// Write styled comment to formatter
1106    fn write_comment(&self, f: &mut dyn Write, text: &str) -> fmt::Result {
1107        if self.use_colors {
1108            write!(f, "{}", text.color(tokyo_night::MUTED))
1109        } else {
1110            write!(f, "{text}")
1111        }
1112    }
1113
1114    /// Write styled redacted value to formatter
1115    fn write_redacted(&self, f: &mut dyn Write, text: &str) -> fmt::Result {
1116        if self.use_colors {
1117            write!(f, "{}", text.color(tokyo_night::ERROR).bold())
1118        } else {
1119            write!(f, "{text}")
1120        }
1121    }
1122
1123    /// Style a redacted value and return it as a string
1124    #[allow(dead_code)]
1125    fn style_redacted(&self, text: &str) -> String {
1126        let mut result = String::new();
1127        self.write_redacted(&mut result, text).unwrap();
1128        result
1129    }
1130
1131    /// Format a value with span tracking for each path.
1132    ///
1133    /// Returns a `FormattedValue` containing the plain text output and a map
1134    /// from paths to their byte spans in the output.
1135    ///
1136    /// This is useful for creating rich diagnostics that can highlight specific
1137    /// parts of a pretty-printed value.
1138    pub fn format_peek_with_spans(&self, value: Peek<'_, '_>) -> FormattedValue {
1139        let mut output = SpanTrackingOutput::new();
1140        let printer = Self {
1141            use_colors: false, // Always disable colors for span tracking
1142            indent_size: self.indent_size,
1143            max_depth: self.max_depth,
1144            color_generator: self.color_generator.clone(),
1145            list_u8_as_bytes: self.list_u8_as_bytes,
1146            minimal_option_names: self.minimal_option_names,
1147            show_doc_comments: self.show_doc_comments,
1148        };
1149        printer
1150            .format_unified(
1151                value,
1152                &mut output,
1153                &mut BTreeMap::new(),
1154                0,
1155                0,
1156                false,
1157                vec![],
1158            )
1159            .expect("Formatting failed");
1160
1161        output.into_formatted_value()
1162    }
1163
1164    /// Unified formatting implementation that works with any FormatOutput.
1165    ///
1166    /// This is the core implementation - both `format_peek` and `format_peek_with_spans`
1167    /// use this internally with different output types.
1168    #[allow(clippy::too_many_arguments)]
1169    fn format_unified<O: FormatOutput>(
1170        &self,
1171        value: Peek<'_, '_>,
1172        out: &mut O,
1173        visited: &mut BTreeMap<ValueId, usize>,
1174        format_depth: usize,
1175        type_depth: usize,
1176        short: bool,
1177        current_path: Path,
1178    ) -> fmt::Result {
1179        let mut value = value;
1180        while let Ok(ptr) = value.into_pointer()
1181            && let Some(pointee) = ptr.borrow_inner()
1182        {
1183            value = pointee;
1184        }
1185        let shape = value.shape();
1186
1187        // Record the start of this value
1188        let value_start = out.position();
1189
1190        if let Some(prev_type_depth) = visited.insert(value.id(), type_depth) {
1191            write!(out, "{} {{ ", shape.type_identifier)?;
1192            write!(
1193                out,
1194                "/* cycle detected at {} (first seen at type_depth {}) */",
1195                value.id(),
1196                prev_type_depth,
1197            )?;
1198            visited.remove(&value.id());
1199            let value_end = out.position();
1200            out.record_span(current_path, (value_start, value_end));
1201            return Ok(());
1202        }
1203
1204        match (shape.def, shape.ty) {
1205            (_, Type::Primitive(PrimitiveType::Textual(TextualType::Str))) => {
1206                let s = value.get::<str>().unwrap();
1207                write!(out, "\"{}\"", s)?;
1208            }
1209            (Def::Scalar, _) if value.shape().id == <alloc::string::String as Facet>::SHAPE.id => {
1210                let s = value.get::<alloc::string::String>().unwrap();
1211                write!(out, "\"{}\"", s)?;
1212            }
1213            (Def::Scalar, _) => {
1214                self.format_scalar_to_output(value, out)?;
1215            }
1216            (Def::Option(_), _) => {
1217                let option = value.into_option().unwrap();
1218                if let Some(inner) = option.value() {
1219                    write!(out, "Some(")?;
1220                    self.format_unified(
1221                        inner,
1222                        out,
1223                        visited,
1224                        format_depth,
1225                        type_depth + 1,
1226                        short,
1227                        current_path.clone(),
1228                    )?;
1229                    write!(out, ")")?;
1230                } else {
1231                    write!(out, "None")?;
1232                }
1233            }
1234            (
1235                _,
1236                Type::User(UserType::Struct(
1237                    ty @ StructType {
1238                        kind: StructKind::Struct | StructKind::Unit,
1239                        ..
1240                    },
1241                )),
1242            ) => {
1243                write!(out, "{}", shape.type_identifier)?;
1244                if matches!(ty.kind, StructKind::Struct) {
1245                    let struct_peek = value.into_struct().unwrap();
1246                    write!(out, " {{")?;
1247                    for (i, field) in ty.fields.iter().enumerate() {
1248                        if !short {
1249                            writeln!(out)?;
1250                            self.indent_to_output(out, format_depth + 1)?;
1251                        }
1252                        // Record field name span
1253                        let field_name_start = out.position();
1254                        write!(out, "{}", field.name)?;
1255                        let field_name_end = out.position();
1256                        write!(out, ": ")?;
1257
1258                        // Build path for this field
1259                        let mut field_path = current_path.clone();
1260                        field_path.push(PathSegment::Field(Cow::Borrowed(field.name)));
1261
1262                        // Record field value span
1263                        let field_value_start = out.position();
1264                        if let Ok(field_value) = struct_peek.field(i) {
1265                            self.format_unified(
1266                                field_value,
1267                                out,
1268                                visited,
1269                                format_depth + 1,
1270                                type_depth + 1,
1271                                short,
1272                                field_path.clone(),
1273                            )?;
1274                        }
1275                        let field_value_end = out.position();
1276
1277                        // Record span for this field
1278                        out.record_field_span(
1279                            field_path,
1280                            (field_name_start, field_name_end),
1281                            (field_value_start, field_value_end),
1282                        );
1283
1284                        if !short || i + 1 < ty.fields.len() {
1285                            write!(out, ",")?;
1286                        }
1287                    }
1288                    if !short {
1289                        writeln!(out)?;
1290                        self.indent_to_output(out, format_depth)?;
1291                    }
1292                    write!(out, "}}")?;
1293                }
1294            }
1295            (
1296                _,
1297                Type::User(UserType::Struct(
1298                    ty @ StructType {
1299                        kind: StructKind::Tuple | StructKind::TupleStruct,
1300                        ..
1301                    },
1302                )),
1303            ) => {
1304                write!(out, "{}", shape.type_identifier)?;
1305                if matches!(ty.kind, StructKind::Tuple) {
1306                    write!(out, " ")?;
1307                }
1308                let struct_peek = value.into_struct().unwrap();
1309                write!(out, "(")?;
1310                for (i, _field) in ty.fields.iter().enumerate() {
1311                    if i > 0 {
1312                        write!(out, ", ")?;
1313                    }
1314                    let mut elem_path = current_path.clone();
1315                    elem_path.push(PathSegment::Index(i));
1316
1317                    let elem_start = out.position();
1318                    if let Ok(field_value) = struct_peek.field(i) {
1319                        self.format_unified(
1320                            field_value,
1321                            out,
1322                            visited,
1323                            format_depth + 1,
1324                            type_depth + 1,
1325                            short,
1326                            elem_path.clone(),
1327                        )?;
1328                    }
1329                    let elem_end = out.position();
1330                    out.record_span(elem_path, (elem_start, elem_end));
1331                }
1332                write!(out, ")")?;
1333            }
1334            (_, Type::User(UserType::Enum(_))) => {
1335                let enum_peek = value.into_enum().unwrap();
1336                match enum_peek.active_variant() {
1337                    Err(_) => {
1338                        write!(
1339                            out,
1340                            "{} {{ /* cannot determine variant */ }}",
1341                            shape.type_identifier
1342                        )?;
1343                    }
1344                    Ok(variant) => {
1345                        write!(out, "{}::{}", shape.type_identifier, variant.name)?;
1346
1347                        match variant.data.kind {
1348                            StructKind::Unit => {}
1349                            StructKind::Struct => {
1350                                write!(out, " {{")?;
1351                                for (i, field) in variant.data.fields.iter().enumerate() {
1352                                    if !short {
1353                                        writeln!(out)?;
1354                                        self.indent_to_output(out, format_depth + 1)?;
1355                                    }
1356                                    let field_name_start = out.position();
1357                                    write!(out, "{}", field.name)?;
1358                                    let field_name_end = out.position();
1359                                    write!(out, ": ")?;
1360
1361                                    let mut field_path = current_path.clone();
1362                                    field_path
1363                                        .push(PathSegment::Variant(Cow::Borrowed(variant.name)));
1364                                    field_path.push(PathSegment::Field(Cow::Borrowed(field.name)));
1365
1366                                    let field_value_start = out.position();
1367                                    if let Ok(Some(field_value)) = enum_peek.field(i) {
1368                                        self.format_unified(
1369                                            field_value,
1370                                            out,
1371                                            visited,
1372                                            format_depth + 1,
1373                                            type_depth + 1,
1374                                            short,
1375                                            field_path.clone(),
1376                                        )?;
1377                                    }
1378                                    let field_value_end = out.position();
1379
1380                                    out.record_field_span(
1381                                        field_path,
1382                                        (field_name_start, field_name_end),
1383                                        (field_value_start, field_value_end),
1384                                    );
1385
1386                                    if !short || i + 1 < variant.data.fields.len() {
1387                                        write!(out, ",")?;
1388                                    }
1389                                }
1390                                if !short {
1391                                    writeln!(out)?;
1392                                    self.indent_to_output(out, format_depth)?;
1393                                }
1394                                write!(out, "}}")?;
1395                            }
1396                            _ => {
1397                                write!(out, "(")?;
1398                                for (i, _field) in variant.data.fields.iter().enumerate() {
1399                                    if i > 0 {
1400                                        write!(out, ", ")?;
1401                                    }
1402                                    let mut elem_path = current_path.clone();
1403                                    elem_path
1404                                        .push(PathSegment::Variant(Cow::Borrowed(variant.name)));
1405                                    elem_path.push(PathSegment::Index(i));
1406
1407                                    let elem_start = out.position();
1408                                    if let Ok(Some(field_value)) = enum_peek.field(i) {
1409                                        self.format_unified(
1410                                            field_value,
1411                                            out,
1412                                            visited,
1413                                            format_depth + 1,
1414                                            type_depth + 1,
1415                                            short,
1416                                            elem_path.clone(),
1417                                        )?;
1418                                    }
1419                                    let elem_end = out.position();
1420                                    out.record_span(elem_path, (elem_start, elem_end));
1421                                }
1422                                write!(out, ")")?;
1423                            }
1424                        }
1425                    }
1426                }
1427            }
1428            _ if value.into_list_like().is_ok() => {
1429                let list = value.into_list_like().unwrap();
1430
1431                // Check if elements are simple scalars - render inline if so
1432                let elem_shape = list.def().t();
1433                let is_simple = Self::shape_chunkiness(elem_shape) <= 1;
1434
1435                write!(out, "[")?;
1436                let len = list.len();
1437                for (i, item) in list.iter().enumerate() {
1438                    if !short && !is_simple {
1439                        writeln!(out)?;
1440                        self.indent_to_output(out, format_depth + 1)?;
1441                    } else if i > 0 {
1442                        write!(out, " ")?;
1443                    }
1444                    let mut elem_path = current_path.clone();
1445                    elem_path.push(PathSegment::Index(i));
1446
1447                    let elem_start = out.position();
1448                    self.format_unified(
1449                        item,
1450                        out,
1451                        visited,
1452                        format_depth + 1,
1453                        type_depth + 1,
1454                        short || is_simple,
1455                        elem_path.clone(),
1456                    )?;
1457                    let elem_end = out.position();
1458                    out.record_span(elem_path, (elem_start, elem_end));
1459
1460                    if (!short && !is_simple) || i + 1 < len {
1461                        write!(out, ",")?;
1462                    }
1463                }
1464                if !short && !is_simple {
1465                    writeln!(out)?;
1466                    self.indent_to_output(out, format_depth)?;
1467                }
1468                write!(out, "]")?;
1469            }
1470            _ if value.into_map().is_ok() => {
1471                let map = value.into_map().unwrap();
1472                write!(out, "{{")?;
1473                for (i, (key, val)) in map.iter().enumerate() {
1474                    if !short {
1475                        writeln!(out)?;
1476                        self.indent_to_output(out, format_depth + 1)?;
1477                    }
1478                    // Format key
1479                    let key_start = out.position();
1480                    self.format_unified(
1481                        key,
1482                        out,
1483                        visited,
1484                        format_depth + 1,
1485                        type_depth + 1,
1486                        true, // short for keys
1487                        vec![],
1488                    )?;
1489                    let key_end = out.position();
1490
1491                    write!(out, ": ")?;
1492
1493                    // Build path for this entry (use key's string representation)
1494                    let key_str = self.format_peek(key);
1495                    let mut entry_path = current_path.clone();
1496                    entry_path.push(PathSegment::Key(Cow::Owned(key_str)));
1497
1498                    let val_start = out.position();
1499                    self.format_unified(
1500                        val,
1501                        out,
1502                        visited,
1503                        format_depth + 1,
1504                        type_depth + 1,
1505                        short,
1506                        entry_path.clone(),
1507                    )?;
1508                    let val_end = out.position();
1509
1510                    out.record_field_span(entry_path, (key_start, key_end), (val_start, val_end));
1511
1512                    if !short || i + 1 < map.len() {
1513                        write!(out, ",")?;
1514                    }
1515                }
1516                if !short && !map.is_empty() {
1517                    writeln!(out)?;
1518                    self.indent_to_output(out, format_depth)?;
1519                }
1520                write!(out, "}}")?;
1521            }
1522            _ => {
1523                // Fallback: just write the type name
1524                write!(out, "{} {{ ... }}", shape.type_identifier)?;
1525            }
1526        }
1527
1528        visited.remove(&value.id());
1529
1530        // Record span for this value
1531        let value_end = out.position();
1532        out.record_span(current_path, (value_start, value_end));
1533
1534        Ok(())
1535    }
1536
1537    fn format_scalar_to_output(&self, value: Peek<'_, '_>, out: &mut impl Write) -> fmt::Result {
1538        // Use Display or Debug trait to format scalar values
1539        if value.shape().is_display() {
1540            write!(out, "{}", value)
1541        } else if value.shape().is_debug() {
1542            write!(out, "{:?}", value)
1543        } else {
1544            write!(out, "{}(…)", value.shape())
1545        }
1546    }
1547
1548    fn indent_to_output(&self, out: &mut impl Write, depth: usize) -> fmt::Result {
1549        for _ in 0..depth {
1550            for _ in 0..self.indent_size {
1551                out.write_char(' ')?;
1552            }
1553        }
1554        Ok(())
1555    }
1556}
1557
1558/// Result of formatting a value with span tracking
1559#[derive(Debug)]
1560pub struct FormattedValue {
1561    /// The formatted text (plain text, no ANSI colors)
1562    pub text: String,
1563    /// Map from paths to their byte spans in `text`
1564    pub spans: BTreeMap<Path, FieldSpan>,
1565}
1566
1567/// Trait for output destinations that may optionally track spans.
1568///
1569/// This allows a single formatting implementation to work with both
1570/// simple string output and span-tracking output.
1571trait FormatOutput: Write {
1572    /// Get the current byte position in the output (for span tracking)
1573    fn position(&self) -> usize;
1574
1575    /// Record a span for a path (value only, key=value)
1576    fn record_span(&mut self, _path: Path, _span: Span) {}
1577
1578    /// Record a span with separate key and value spans
1579    fn record_field_span(&mut self, _path: Path, _key_span: Span, _value_span: Span) {}
1580}
1581
1582/// A wrapper around any Write that implements FormatOutput but doesn't track spans.
1583/// Position tracking is approximated by counting bytes written.
1584#[allow(dead_code)]
1585struct NonTrackingOutput<W> {
1586    inner: W,
1587    position: usize,
1588}
1589
1590#[allow(dead_code)]
1591impl<W> NonTrackingOutput<W> {
1592    fn new(inner: W) -> Self {
1593        Self { inner, position: 0 }
1594    }
1595}
1596
1597impl<W: Write> Write for NonTrackingOutput<W> {
1598    fn write_str(&mut self, s: &str) -> fmt::Result {
1599        self.position += s.len();
1600        self.inner.write_str(s)
1601    }
1602}
1603
1604impl<W: Write> FormatOutput for NonTrackingOutput<W> {
1605    fn position(&self) -> usize {
1606        self.position
1607    }
1608    // Uses default no-op implementations for span recording
1609}
1610
1611/// Context for tracking spans during value formatting
1612struct SpanTrackingOutput {
1613    output: String,
1614    spans: BTreeMap<Path, FieldSpan>,
1615}
1616
1617impl SpanTrackingOutput {
1618    fn new() -> Self {
1619        Self {
1620            output: String::new(),
1621            spans: BTreeMap::new(),
1622        }
1623    }
1624
1625    fn into_formatted_value(self) -> FormattedValue {
1626        FormattedValue {
1627            text: self.output,
1628            spans: self.spans,
1629        }
1630    }
1631}
1632
1633impl Write for SpanTrackingOutput {
1634    fn write_str(&mut self, s: &str) -> fmt::Result {
1635        self.output.push_str(s);
1636        Ok(())
1637    }
1638}
1639
1640impl FormatOutput for SpanTrackingOutput {
1641    fn position(&self) -> usize {
1642        self.output.len()
1643    }
1644
1645    fn record_span(&mut self, path: Path, span: Span) {
1646        self.spans.insert(
1647            path,
1648            FieldSpan {
1649                key: span,
1650                value: span,
1651            },
1652        );
1653    }
1654
1655    fn record_field_span(&mut self, path: Path, key_span: Span, value_span: Span) {
1656        self.spans.insert(
1657            path,
1658            FieldSpan {
1659                key: key_span,
1660                value: value_span,
1661            },
1662        );
1663    }
1664}
1665
1666#[cfg(test)]
1667mod tests {
1668    use super::*;
1669
1670    // Basic tests for the PrettyPrinter
1671    #[test]
1672    fn test_pretty_printer_default() {
1673        let printer = PrettyPrinter::default();
1674        assert_eq!(printer.indent_size, 2);
1675        assert_eq!(printer.max_depth, None);
1676        // use_colors defaults to true unless NO_COLOR is set
1677        // In tests, NO_COLOR=1 is set via nextest config for consistent snapshots
1678        assert_eq!(printer.use_colors, std::env::var_os("NO_COLOR").is_none());
1679    }
1680
1681    #[test]
1682    fn test_pretty_printer_with_methods() {
1683        let printer = PrettyPrinter::new()
1684            .with_indent_size(4)
1685            .with_max_depth(3)
1686            .with_colors(false);
1687
1688        assert_eq!(printer.indent_size, 4);
1689        assert_eq!(printer.max_depth, Some(3));
1690        assert!(!printer.use_colors);
1691    }
1692
1693    #[test]
1694    fn test_format_peek_with_spans() {
1695        use crate::PathSegment;
1696        use facet_reflect::Peek;
1697
1698        // Test with a simple tuple - no need for custom struct
1699        let value = ("Alice", 30u32);
1700
1701        let printer = PrettyPrinter::new();
1702        let formatted = printer.format_peek_with_spans(Peek::new(&value));
1703
1704        // Check that we got output
1705        assert!(!formatted.text.is_empty());
1706        assert!(formatted.text.contains("Alice"));
1707        assert!(formatted.text.contains("30"));
1708
1709        // Check that spans were recorded
1710        assert!(!formatted.spans.is_empty());
1711
1712        // Check that the root span exists (empty path)
1713        assert!(formatted.spans.contains_key(&vec![]));
1714
1715        // Check that index spans exist
1716        let idx0_path = vec![PathSegment::Index(0)];
1717        let idx1_path = vec![PathSegment::Index(1)];
1718        assert!(
1719            formatted.spans.contains_key(&idx0_path),
1720            "index 0 span not found"
1721        );
1722        assert!(
1723            formatted.spans.contains_key(&idx1_path),
1724            "index 1 span not found"
1725        );
1726    }
1727}