Skip to main content

facet_pretty/
shape.rs

1//! Pretty printer for Shape types as Rust-like code
2//!
3//! This module provides functionality to format a `Shape` as Rust source code,
4//! showing the type definition with its attributes.
5
6use alloc::borrow::Cow;
7use alloc::collections::BTreeMap;
8use alloc::collections::BTreeSet;
9use alloc::string::String;
10use alloc::vec;
11use alloc::vec::Vec;
12use core::fmt::Write;
13
14use facet_core::{
15    Attr, ConstTypeId, Def, EnumRepr, EnumType, Field, PointerType, Shape, StructKind, StructType,
16    Type, UserType, Variant,
17};
18use owo_colors::OwoColorize;
19
20/// Tokyo Night color scheme for syntax highlighting
21pub mod colors {
22    use owo_colors::Style;
23
24    /// Keywords: struct, enum, pub, etc. (purple)
25    pub const fn keyword() -> Style {
26        Style::new().fg_rgb::<187, 154, 247>()
27    }
28
29    /// Type names and identifiers (light blue)
30    pub const fn type_name() -> Style {
31        Style::new().fg_rgb::<192, 202, 245>()
32    }
33
34    /// Field names (cyan)
35    pub const fn field_name() -> Style {
36        Style::new().fg_rgb::<125, 207, 255>()
37    }
38
39    /// Primitive types: u8, i32, bool, String, etc. (teal)
40    pub const fn primitive() -> Style {
41        Style::new().fg_rgb::<115, 218, 202>()
42    }
43
44    /// Punctuation: {, }, (, ), :, etc. (gray-blue)
45    pub const fn punctuation() -> Style {
46        Style::new().fg_rgb::<154, 165, 206>()
47    }
48
49    /// Attribute markers: #[...] (light cyan)
50    pub const fn attribute() -> Style {
51        Style::new().fg_rgb::<137, 221, 255>()
52    }
53
54    /// Attribute content: derive, facet, repr (blue)
55    pub const fn attribute_content() -> Style {
56        Style::new().fg_rgb::<122, 162, 247>()
57    }
58
59    /// String literals (green)
60    pub const fn string() -> Style {
61        Style::new().fg_rgb::<158, 206, 106>()
62    }
63
64    /// Container types: Vec, Option, HashMap (orange)
65    pub const fn container() -> Style {
66        Style::new().fg_rgb::<255, 158, 100>()
67    }
68
69    /// Doc comments (muted gray)
70    pub const fn comment() -> Style {
71        Style::new().fg_rgb::<86, 95, 137>()
72    }
73}
74
75/// Configuration options for shape formatting
76#[derive(Clone, Debug, Default)]
77pub struct ShapeFormatConfig {
78    /// Whether to include doc comments in the output
79    pub show_doc_comments: bool,
80    /// Whether to include third-party (namespaced) attributes
81    pub show_third_party_attrs: bool,
82    /// Whether to expand and print nested types (default: true)
83    pub expand_nested_types: bool,
84}
85
86impl ShapeFormatConfig {
87    /// Create a new config with default settings (no doc comments, no third-party attrs, expand nested)
88    pub fn new() -> Self {
89        Self {
90            expand_nested_types: true,
91            ..Self::default()
92        }
93    }
94
95    /// Enable doc comment display
96    pub const fn with_doc_comments(mut self) -> Self {
97        self.show_doc_comments = true;
98        self
99    }
100
101    /// Enable third-party attribute display
102    pub const fn with_third_party_attrs(mut self) -> Self {
103        self.show_third_party_attrs = true;
104        self
105    }
106
107    /// Enable all metadata (doc comments and third-party attrs)
108    pub const fn with_all_metadata(mut self) -> Self {
109        self.show_doc_comments = true;
110        self.show_third_party_attrs = true;
111        self
112    }
113
114    /// Disable nested type expansion (only format the root type)
115    pub const fn without_nested_types(mut self) -> Self {
116        self.expand_nested_types = false;
117        self
118    }
119}
120
121/// A segment in a path through a type structure
122#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
123pub enum PathSegment {
124    /// A field name in a struct
125    Field(Cow<'static, str>),
126    /// A variant name in an enum
127    Variant(Cow<'static, str>),
128    /// An index in a list/array/tuple
129    Index(usize),
130    /// A key in a map (stored as formatted string representation)
131    Key(Cow<'static, str>),
132}
133
134/// A path to a location within a type structure
135pub type Path = Vec<PathSegment>;
136
137/// A byte span in formatted output (start, end)
138pub type Span = (usize, usize);
139
140/// Spans for a field or variant, tracking both key (name) and value (type) positions
141#[derive(Clone, Debug, Default, PartialEq, Eq)]
142pub struct FieldSpan {
143    /// Span of the field/variant name (e.g., "max_retries" in "max_retries: u8")
144    pub key: Span,
145    /// Span of the type annotation (e.g., "u8" in "max_retries: u8")
146    pub value: Span,
147}
148
149/// Result of formatting a shape with span tracking
150#[derive(Debug)]
151pub struct FormattedShape {
152    /// The formatted text (plain text, no ANSI colors)
153    pub text: String,
154    /// Map from paths to their field spans (key + value) in `text`
155    pub spans: BTreeMap<Path, FieldSpan>,
156    /// Span of the type name (e.g., "Server" in "struct Server {")
157    pub type_name_span: Option<Span>,
158    /// Span covering the whole type definition
159    pub type_span: Option<Span>,
160    /// Span of the end token for the type definition (e.g., "}" or ";")
161    pub type_end_span: Option<Span>,
162}
163
164/// Strip ANSI escape codes from a string
165pub fn strip_ansi(s: &str) -> String {
166    let mut result = String::with_capacity(s.len());
167    let mut chars = s.chars().peekable();
168
169    while let Some(c) = chars.next() {
170        if c == '\x1b' {
171            // Skip escape sequence
172            if chars.peek() == Some(&'[') {
173                chars.next(); // consume '['
174                // Skip until we hit a letter (the terminator)
175                while let Some(&next) = chars.peek() {
176                    chars.next();
177                    if next.is_ascii_alphabetic() {
178                        break;
179                    }
180                }
181            }
182        } else {
183            result.push(c);
184        }
185    }
186    result
187}
188
189/// Format a Shape as Rust-like source code (plain text, no colors)
190///
191/// By default, this includes doc comments and third-party attributes.
192pub fn format_shape(shape: &Shape) -> String {
193    strip_ansi(&format_shape_colored(shape))
194}
195
196/// Format a Shape as Rust-like source code with config options (plain text, no colors)
197pub fn format_shape_with_config(shape: &Shape, config: &ShapeFormatConfig) -> String {
198    strip_ansi(&format_shape_colored_with_config(shape, config))
199}
200
201/// Format a Shape with span tracking for each field/variant
202/// Note: spans are computed on the plain text (no ANSI codes)
203///
204/// By default, this includes doc comments and third-party attributes.
205pub fn format_shape_with_spans(shape: &Shape) -> FormattedShape {
206    format_shape_with_spans_and_config(shape, &ShapeFormatConfig::default().with_all_metadata())
207}
208
209/// Format a Shape with span tracking and config options
210/// Note: spans are computed on the plain text (no ANSI codes)
211pub fn format_shape_with_spans_and_config(
212    shape: &Shape,
213    config: &ShapeFormatConfig,
214) -> FormattedShape {
215    let mut ctx = SpanTrackingContext::new(config);
216    format_shape_into_with_spans(shape, &mut ctx).expect("Formatting failed");
217    FormattedShape {
218        text: ctx.output,
219        spans: ctx.spans,
220        type_name_span: ctx.type_name_span,
221        type_span: ctx.type_span,
222        type_end_span: ctx.type_end_span,
223    }
224}
225
226/// Format a Shape as Rust-like source code with ANSI colors (Tokyo Night theme)
227///
228/// By default, this includes doc comments and third-party attributes.
229pub fn format_shape_colored(shape: &Shape) -> String {
230    format_shape_colored_with_config(shape, &ShapeFormatConfig::default().with_all_metadata())
231}
232
233/// Format a Shape as Rust-like source code with ANSI colors and config options
234pub fn format_shape_colored_with_config(shape: &Shape, config: &ShapeFormatConfig) -> String {
235    let mut output = String::new();
236    format_shape_colored_into_with_config(shape, &mut output, config).expect("Formatting failed");
237    output
238}
239
240/// Format a Shape with ANSI colors into an existing String
241pub fn format_shape_colored_into(shape: &Shape, output: &mut String) -> core::fmt::Result {
242    format_shape_colored_into_with_config(shape, output, &ShapeFormatConfig::default())
243}
244
245/// Format a Shape with ANSI colors into an existing String with config options
246pub fn format_shape_colored_into_with_config(
247    shape: &Shape,
248    output: &mut String,
249    config: &ShapeFormatConfig,
250) -> core::fmt::Result {
251    let mut printed: BTreeSet<ConstTypeId> = BTreeSet::new();
252    let mut queue: Vec<&Shape> = Vec::new();
253    queue.push(shape);
254
255    while let Some(current) = queue.pop() {
256        if !printed.insert(current.id) {
257            continue;
258        }
259
260        if printed.len() > 1 {
261            writeln!(output)?;
262            writeln!(output)?;
263        }
264
265        match current.def {
266            Def::Map(_) | Def::List(_) | Def::Option(_) | Def::Array(_) => {
267                printed.remove(&current.id);
268                continue;
269            }
270            _ => {}
271        }
272
273        match &current.ty {
274            Type::User(user_type) => match user_type {
275                UserType::Struct(struct_type) => {
276                    format_struct_colored(current, struct_type, output, config)?;
277                    collect_nested_types(struct_type, &mut queue);
278                }
279                UserType::Enum(enum_type) => {
280                    format_enum_colored(current, enum_type, output, config)?;
281                    for variant in enum_type.variants {
282                        collect_nested_types(&variant.data, &mut queue);
283                    }
284                }
285                UserType::Union(_) | UserType::Opaque => {
286                    printed.remove(&current.id);
287                }
288            },
289            _ => {
290                printed.remove(&current.id);
291            }
292        }
293    }
294    Ok(())
295}
296
297fn format_struct_colored(
298    shape: &Shape,
299    struct_type: &StructType,
300    output: &mut String,
301    config: &ShapeFormatConfig,
302) -> core::fmt::Result {
303    // Write doc comments for the struct if enabled
304    if config.show_doc_comments {
305        write_doc_comments_colored(shape.doc, output, "")?;
306    }
307
308    // #[derive(Facet)]
309    write!(output, "{}", "#[".style(colors::attribute()))?;
310    write!(output, "{}", "derive".style(colors::attribute_content()))?;
311    write!(output, "{}", "(".style(colors::attribute()))?;
312    write!(output, "{}", "Facet".style(colors::attribute_content()))?;
313    writeln!(output, "{}", ")]".style(colors::attribute()))?;
314
315    // Write facet attributes if any
316    write_facet_attrs_colored(shape, output)?;
317
318    // Write third-party attributes if enabled
319    if config.show_third_party_attrs {
320        write_third_party_attrs_colored(shape.attributes, output, "")?;
321    }
322
323    match struct_type.kind {
324        StructKind::Struct => {
325            write!(output, "{} ", "struct".style(colors::keyword()))?;
326            write!(
327                output,
328                "{}",
329                shape.type_identifier.style(colors::type_name())
330            )?;
331            writeln!(output, " {}", "{".style(colors::punctuation()))?;
332
333            for (i, field) in struct_type.fields.iter().enumerate() {
334                // Blank line between fields (not before the first one)
335                if i > 0 {
336                    writeln!(output)?;
337                }
338                // Write doc comments for the field if enabled
339                if config.show_doc_comments {
340                    write_doc_comments_colored(field.doc, output, "    ")?;
341                }
342                // Write third-party attributes for the field if enabled
343                if config.show_third_party_attrs {
344                    write_field_third_party_attrs_colored(field, output, "    ")?;
345                }
346                write!(output, "    {}", field.name.style(colors::field_name()))?;
347                write!(output, "{} ", ":".style(colors::punctuation()))?;
348                write_type_name_colored(field.shape(), output)?;
349                writeln!(output, "{}", ",".style(colors::punctuation()))?;
350            }
351            write!(output, "{}", "}".style(colors::punctuation()))?;
352        }
353        StructKind::Tuple | StructKind::TupleStruct => {
354            write!(output, "{} ", "struct".style(colors::keyword()))?;
355            write!(
356                output,
357                "{}",
358                shape.type_identifier.style(colors::type_name())
359            )?;
360            write!(output, "{}", "(".style(colors::punctuation()))?;
361            for (i, field) in struct_type.fields.iter().enumerate() {
362                if i > 0 {
363                    write!(output, "{} ", ",".style(colors::punctuation()))?;
364                }
365                write_type_name_colored(field.shape(), output)?;
366            }
367            write!(
368                output,
369                "{}{}",
370                ")".style(colors::punctuation()),
371                ";".style(colors::punctuation())
372            )?;
373        }
374        StructKind::Unit => {
375            write!(output, "{} ", "struct".style(colors::keyword()))?;
376            write!(
377                output,
378                "{}",
379                shape.type_identifier.style(colors::type_name())
380            )?;
381            write!(output, "{}", ";".style(colors::punctuation()))?;
382        }
383    }
384    Ok(())
385}
386
387fn format_enum_colored(
388    shape: &Shape,
389    enum_type: &EnumType,
390    output: &mut String,
391    config: &ShapeFormatConfig,
392) -> core::fmt::Result {
393    // Write doc comments for the enum if enabled
394    if config.show_doc_comments {
395        write_doc_comments_colored(shape.doc, output, "")?;
396    }
397
398    // #[derive(Facet)]
399    write!(output, "{}", "#[".style(colors::attribute()))?;
400    write!(output, "{}", "derive".style(colors::attribute_content()))?;
401    write!(output, "{}", "(".style(colors::attribute()))?;
402    write!(output, "{}", "Facet".style(colors::attribute_content()))?;
403    writeln!(output, "{}", ")]".style(colors::attribute()))?;
404
405    // Write repr for the discriminant type
406    let repr_str = match enum_type.enum_repr {
407        EnumRepr::Rust => None,
408        EnumRepr::RustNPO => None,
409        EnumRepr::U8 => Some("u8"),
410        EnumRepr::U16 => Some("u16"),
411        EnumRepr::U32 => Some("u32"),
412        EnumRepr::U64 => Some("u64"),
413        EnumRepr::USize => Some("usize"),
414        EnumRepr::I8 => Some("i8"),
415        EnumRepr::I16 => Some("i16"),
416        EnumRepr::I32 => Some("i32"),
417        EnumRepr::I64 => Some("i64"),
418        EnumRepr::ISize => Some("isize"),
419    };
420
421    if let Some(repr) = repr_str {
422        write!(output, "{}", "#[".style(colors::attribute()))?;
423        write!(output, "{}", "repr".style(colors::attribute_content()))?;
424        write!(output, "{}", "(".style(colors::attribute()))?;
425        write!(output, "{}", repr.style(colors::primitive()))?;
426        writeln!(output, "{}", ")]".style(colors::attribute()))?;
427    }
428
429    // Write facet attributes if any
430    write_facet_attrs_colored(shape, output)?;
431
432    // Write third-party attributes if enabled
433    if config.show_third_party_attrs {
434        write_third_party_attrs_colored(shape.attributes, output, "")?;
435    }
436
437    // enum Name {
438    write!(output, "{} ", "enum".style(colors::keyword()))?;
439    write!(
440        output,
441        "{}",
442        shape.type_identifier.style(colors::type_name())
443    )?;
444    writeln!(output, " {}", "{".style(colors::punctuation()))?;
445
446    for (vi, variant) in enum_type.variants.iter().enumerate() {
447        // Blank line between variants (not before the first one)
448        if vi > 0 {
449            writeln!(output)?;
450        }
451        // Write doc comments for the variant if enabled
452        if config.show_doc_comments {
453            write_doc_comments_colored(variant.doc, output, "    ")?;
454        }
455        // Write third-party attributes for the variant if enabled
456        if config.show_third_party_attrs {
457            write_variant_third_party_attrs_colored(variant, output, "    ")?;
458        }
459
460        match variant.data.kind {
461            StructKind::Unit => {
462                write!(output, "    {}", variant.name.style(colors::type_name()))?;
463                writeln!(output, "{}", ",".style(colors::punctuation()))?;
464            }
465            StructKind::Tuple | StructKind::TupleStruct => {
466                write!(output, "    {}", variant.name.style(colors::type_name()))?;
467                write!(output, "{}", "(".style(colors::punctuation()))?;
468                for (i, field) in variant.data.fields.iter().enumerate() {
469                    if i > 0 {
470                        write!(output, "{} ", ",".style(colors::punctuation()))?;
471                    }
472                    write_type_name_colored(field.shape(), output)?;
473                }
474                write!(output, "{}", ")".style(colors::punctuation()))?;
475                writeln!(output, "{}", ",".style(colors::punctuation()))?;
476            }
477            StructKind::Struct => {
478                write!(output, "    {}", variant.name.style(colors::type_name()))?;
479                writeln!(output, " {}", "{".style(colors::punctuation()))?;
480                for (fi, field) in variant.data.fields.iter().enumerate() {
481                    // Blank line between variant fields (not before the first one)
482                    if fi > 0 {
483                        writeln!(output)?;
484                    }
485                    // Write doc comments for variant fields if enabled
486                    if config.show_doc_comments {
487                        write_doc_comments_colored(field.doc, output, "        ")?;
488                    }
489                    // Write third-party attributes for variant fields if enabled
490                    if config.show_third_party_attrs {
491                        write_field_third_party_attrs_colored(field, output, "        ")?;
492                    }
493                    write!(output, "        {}", field.name.style(colors::field_name()))?;
494                    write!(output, "{} ", ":".style(colors::punctuation()))?;
495                    write_type_name_colored(field.shape(), output)?;
496                    writeln!(output, "{}", ",".style(colors::punctuation()))?;
497                }
498                write!(output, "    {}", "}".style(colors::punctuation()))?;
499                writeln!(output, "{}", ",".style(colors::punctuation()))?;
500            }
501        }
502    }
503
504    write!(output, "{}", "}".style(colors::punctuation()))?;
505    Ok(())
506}
507
508fn write_facet_attrs_colored(shape: &Shape, output: &mut String) -> core::fmt::Result {
509    let mut attrs: Vec<String> = Vec::new();
510
511    if let Some(tag) = shape.get_tag_attr() {
512        if let Some(content) = shape.get_content_attr() {
513            attrs.push(alloc::format!(
514                "{}{}{}{}{}{}{}{}{}",
515                "tag".style(colors::attribute_content()),
516                " = ".style(colors::punctuation()),
517                "\"".style(colors::string()),
518                tag.style(colors::string()),
519                "\"".style(colors::string()),
520                ", ".style(colors::punctuation()),
521                "content".style(colors::attribute_content()),
522                " = ".style(colors::punctuation()),
523                format!("\"{content}\"").style(colors::string()),
524            ));
525        } else {
526            attrs.push(alloc::format!(
527                "{}{}{}",
528                "tag".style(colors::attribute_content()),
529                " = ".style(colors::punctuation()),
530                format!("\"{tag}\"").style(colors::string()),
531            ));
532        }
533    }
534
535    if shape.is_untagged() {
536        attrs.push(alloc::format!(
537            "{}",
538            "untagged".style(colors::attribute_content())
539        ));
540    }
541
542    if shape.has_deny_unknown_fields_attr() {
543        attrs.push(alloc::format!(
544            "{}",
545            "deny_unknown_fields".style(colors::attribute_content())
546        ));
547    }
548
549    if !attrs.is_empty() {
550        write!(output, "{}", "#[".style(colors::attribute()))?;
551        write!(output, "{}", "facet".style(colors::attribute_content()))?;
552        write!(output, "{}", "(".style(colors::attribute()))?;
553        write!(
554            output,
555            "{}",
556            attrs.join(&format!("{}", ", ".style(colors::punctuation())))
557        )?;
558        writeln!(output, "{}", ")]".style(colors::attribute()))?;
559    }
560
561    Ok(())
562}
563
564/// Write doc comments with the given indentation prefix
565fn write_doc_comments_colored(
566    doc: &[&str],
567    output: &mut String,
568    indent: &str,
569) -> core::fmt::Result {
570    for line in doc {
571        write!(output, "{indent}")?;
572        writeln!(output, "{}", format!("///{line}").style(colors::comment()))?;
573    }
574    Ok(())
575}
576
577/// Write third-party (namespaced) attributes from a Shape's attributes
578/// Groups attributes by namespace, e.g. `#[facet(args::named, args::short)]`
579fn write_third_party_attrs_colored(
580    attributes: &[Attr],
581    output: &mut String,
582    indent: &str,
583) -> core::fmt::Result {
584    // Group attributes by namespace
585    let mut by_namespace: BTreeMap<&'static str, Vec<&'static str>> = BTreeMap::new();
586    for attr in attributes {
587        if let Some(ns) = attr.ns {
588            by_namespace.entry(ns).or_default().push(attr.key);
589        }
590    }
591
592    // Write one line per namespace with all keys
593    for (ns, keys) in by_namespace {
594        write!(output, "{indent}")?;
595        write!(output, "{}", "#[".style(colors::attribute()))?;
596        write!(output, "{}", "facet".style(colors::attribute_content()))?;
597        write!(output, "{}", "(".style(colors::attribute()))?;
598
599        for (i, key) in keys.iter().enumerate() {
600            if i > 0 {
601                write!(output, "{}", ", ".style(colors::punctuation()))?;
602            }
603            write!(output, "{}", ns.style(colors::attribute_content()))?;
604            write!(output, "{}", "::".style(colors::punctuation()))?;
605            write!(output, "{}", key.style(colors::attribute_content()))?;
606        }
607
608        write!(output, "{}", ")".style(colors::attribute()))?;
609        writeln!(output, "{}", "]".style(colors::attribute()))?;
610    }
611    Ok(())
612}
613
614/// Write third-party attributes for a field
615fn write_field_third_party_attrs_colored(
616    field: &Field,
617    output: &mut String,
618    indent: &str,
619) -> core::fmt::Result {
620    write_third_party_attrs_colored(field.attributes, output, indent)
621}
622
623/// Write third-party attributes for a variant
624fn write_variant_third_party_attrs_colored(
625    variant: &Variant,
626    output: &mut String,
627    indent: &str,
628) -> core::fmt::Result {
629    write_third_party_attrs_colored(variant.attributes, output, indent)
630}
631
632fn write_type_name_colored(shape: &Shape, output: &mut String) -> core::fmt::Result {
633    match shape.def {
634        Def::Scalar => {
635            // Check if it's a primitive type
636            let id = shape.type_identifier;
637            if is_primitive_type(id) {
638                write!(output, "{}", id.style(colors::primitive()))?;
639            } else {
640                write!(output, "{}", id.style(colors::type_name()))?;
641            }
642        }
643        Def::Pointer(_) => {
644            if let Type::Pointer(PointerType::Reference(r)) = shape.ty
645                && let Def::Array(array_def) = r.target.def
646            {
647                write!(output, "{}", "&[".style(colors::punctuation()))?;
648                write_type_name_colored(array_def.t, output)?;
649                write!(
650                    output,
651                    "{}{}{}",
652                    "; ".style(colors::punctuation()),
653                    array_def.n.style(colors::primitive()),
654                    "]".style(colors::punctuation())
655                )?;
656                return Ok(());
657            }
658            write!(
659                output,
660                "{}",
661                shape.type_identifier.style(colors::type_name())
662            )?;
663        }
664        Def::List(list_def) => {
665            write!(output, "{}", "Vec".style(colors::container()))?;
666            write!(output, "{}", "<".style(colors::punctuation()))?;
667            write_type_name_colored(list_def.t, output)?;
668            write!(output, "{}", ">".style(colors::punctuation()))?;
669        }
670        Def::Array(array_def) => {
671            write!(output, "{}", "[".style(colors::punctuation()))?;
672            write_type_name_colored(array_def.t, output)?;
673            write!(
674                output,
675                "{}{}{}",
676                "; ".style(colors::punctuation()),
677                array_def.n.style(colors::primitive()),
678                "]".style(colors::punctuation())
679            )?;
680        }
681        Def::Map(map_def) => {
682            let map_name = if shape.type_identifier.contains("BTreeMap") {
683                "BTreeMap"
684            } else {
685                "HashMap"
686            };
687            write!(output, "{}", map_name.style(colors::container()))?;
688            write!(output, "{}", "<".style(colors::punctuation()))?;
689            write_type_name_colored(map_def.k, output)?;
690            write!(output, "{} ", ",".style(colors::punctuation()))?;
691            write_type_name_colored(map_def.v, output)?;
692            write!(output, "{}", ">".style(colors::punctuation()))?;
693        }
694        Def::Option(option_def) => {
695            write!(output, "{}", "Option".style(colors::container()))?;
696            write!(output, "{}", "<".style(colors::punctuation()))?;
697            write_type_name_colored(option_def.t, output)?;
698            write!(output, "{}", ">".style(colors::punctuation()))?;
699        }
700        _ => {
701            let id = shape.type_identifier;
702            if is_primitive_type(id) {
703                write!(output, "{}", id.style(colors::primitive()))?;
704            } else {
705                write!(output, "{}", id.style(colors::type_name()))?;
706            }
707        }
708    }
709    Ok(())
710}
711
712/// Check if a type identifier is a primitive type
713fn is_primitive_type(id: &str) -> bool {
714    matches!(
715        id,
716        "u8" | "u16"
717            | "u32"
718            | "u64"
719            | "u128"
720            | "usize"
721            | "i8"
722            | "i16"
723            | "i32"
724            | "i64"
725            | "i128"
726            | "isize"
727            | "f32"
728            | "f64"
729            | "bool"
730            | "char"
731            | "str"
732            | "&str"
733            | "String"
734    )
735}
736
737/// Context for tracking spans during formatting
738struct SpanTrackingContext<'a> {
739    output: String,
740    spans: BTreeMap<Path, FieldSpan>,
741    /// Span of the type name (struct/enum identifier)
742    type_name_span: Option<Span>,
743    /// Span covering the whole type definition
744    type_span: Option<Span>,
745    /// Span of the end token for the type definition (e.g., "}" or ";")
746    type_end_span: Option<Span>,
747    /// Current path prefix (for nested types)
748    current_type: Option<&'static str>,
749    /// Configuration for what to include
750    config: &'a ShapeFormatConfig,
751}
752
753impl<'a> SpanTrackingContext<'a> {
754    const fn new(config: &'a ShapeFormatConfig) -> Self {
755        Self {
756            output: String::new(),
757            spans: BTreeMap::new(),
758            type_name_span: None,
759            type_span: None,
760            type_end_span: None,
761            current_type: None,
762            config,
763        }
764    }
765
766    const fn len(&self) -> usize {
767        self.output.len()
768    }
769
770    fn record_field_span(&mut self, path: Path, key_span: Span, value_span: Span) {
771        self.spans.insert(
772            path,
773            FieldSpan {
774                key: key_span,
775                value: value_span,
776            },
777        );
778    }
779}
780
781/// Format a Shape with span tracking
782fn format_shape_into_with_spans(shape: &Shape, ctx: &mut SpanTrackingContext) -> core::fmt::Result {
783    // Track which types we've already printed to avoid duplicates
784    let mut printed: BTreeSet<ConstTypeId> = BTreeSet::new();
785    // Queue of types to print
786    let mut queue: Vec<&Shape> = Vec::new();
787
788    // Start with the root shape
789    queue.push(shape);
790
791    while let Some(current) = queue.pop() {
792        // Skip if we've already printed this type
793        if !printed.insert(current.id) {
794            continue;
795        }
796
797        // Add separator between type definitions
798        if printed.len() > 1 {
799            writeln!(ctx.output)?;
800            writeln!(ctx.output)?;
801        }
802
803        // First check def for container types (Map, List, Option, Array)
804        // These have rich generic info even when ty is Opaque
805        match current.def {
806            Def::Map(_) | Def::List(_) | Def::Option(_) | Def::Array(_) => {
807                // Don't print container types as definitions, they're inline
808                printed.remove(&current.id);
809                continue;
810            }
811            _ => {}
812        }
813
814        // Then check ty for user-defined types
815        match &current.ty {
816            Type::User(user_type) => match user_type {
817                UserType::Struct(struct_type) => {
818                    ctx.current_type = Some(current.type_identifier);
819                    format_struct_with_spans(current, struct_type, ctx)?;
820                    ctx.current_type = None;
821                    // Queue nested types from fields (if expansion enabled)
822                    if ctx.config.expand_nested_types {
823                        collect_nested_types(struct_type, &mut queue);
824                    }
825                }
826                UserType::Enum(enum_type) => {
827                    ctx.current_type = Some(current.type_identifier);
828                    format_enum_with_spans(current, enum_type, ctx)?;
829                    ctx.current_type = None;
830                    // Queue nested types from variants (if expansion enabled)
831                    if ctx.config.expand_nested_types {
832                        for variant in enum_type.variants {
833                            collect_nested_types(&variant.data, &mut queue);
834                        }
835                    }
836                }
837                UserType::Union(_) | UserType::Opaque => {
838                    // For union/opaque types, just show the type identifier
839                    // Don't actually print anything since we can't expand them
840                    printed.remove(&current.id);
841                }
842            },
843            _ => {
844                // For non-user types (primitives, pointers, etc.), don't print
845                printed.remove(&current.id);
846            }
847        }
848    }
849    Ok(())
850}
851
852fn format_struct_with_spans(
853    shape: &Shape,
854    struct_type: &StructType,
855    ctx: &mut SpanTrackingContext,
856) -> core::fmt::Result {
857    // Track start of the whole type definition
858    let type_start = ctx.len();
859
860    // Write doc comments if enabled
861    if ctx.config.show_doc_comments {
862        write_doc_comments(shape.doc, &mut ctx.output, "")?;
863    }
864
865    // Write #[derive(Facet)]
866    writeln!(ctx.output, "#[derive(Facet)]")?;
867
868    // Write facet attributes if any
869    write_facet_attrs(shape, &mut ctx.output)?;
870
871    // Write third-party attributes if enabled
872    if ctx.config.show_third_party_attrs {
873        write_third_party_attrs(shape.attributes, &mut ctx.output, "")?;
874    }
875
876    // Write struct definition
877    match struct_type.kind {
878        StructKind::Struct => {
879            write!(ctx.output, "struct ")?;
880            let type_name_start = ctx.len();
881            write!(ctx.output, "{}", shape.type_identifier)?;
882            let type_name_end = ctx.len();
883            ctx.type_name_span = Some((type_name_start, type_name_end));
884            writeln!(ctx.output, " {{")?;
885            for field in struct_type.fields {
886                // Write doc comments for the field if enabled
887                if ctx.config.show_doc_comments {
888                    write_doc_comments(field.doc, &mut ctx.output, "    ")?;
889                }
890                // Write third-party attributes for the field if enabled
891                if ctx.config.show_third_party_attrs {
892                    write_field_third_party_attrs(field, &mut ctx.output, "    ")?;
893                }
894                write!(ctx.output, "    ")?;
895                // Track the span of the field name (key)
896                let key_start = ctx.len();
897                write!(ctx.output, "{}", field.name)?;
898                let key_end = ctx.len();
899                write!(ctx.output, ": ")?;
900                // Track the span of the type annotation (value)
901                let value_start = ctx.len();
902                write_type_name(field.shape(), &mut ctx.output)?;
903                let value_end = ctx.len();
904                ctx.record_field_span(
905                    vec![PathSegment::Field(Cow::Borrowed(field.name))],
906                    (key_start, key_end),
907                    (value_start, value_end),
908                );
909                writeln!(ctx.output, ",")?;
910            }
911            let type_end_start = ctx.len();
912            write!(ctx.output, "}}")?;
913            let type_end_end = ctx.len();
914            ctx.type_end_span = Some((type_end_start, type_end_end));
915        }
916        StructKind::Tuple | StructKind::TupleStruct => {
917            write!(ctx.output, "struct ")?;
918            let type_name_start = ctx.len();
919            write!(ctx.output, "{}", shape.type_identifier)?;
920            let type_name_end = ctx.len();
921            ctx.type_name_span = Some((type_name_start, type_name_end));
922            write!(ctx.output, "(")?;
923            for (i, field) in struct_type.fields.iter().enumerate() {
924                if i > 0 {
925                    write!(ctx.output, ", ")?;
926                }
927                // For tuple structs, key and value span are the same (just the type)
928                let type_start = ctx.len();
929                write_type_name(field.shape(), &mut ctx.output)?;
930                let type_end = ctx.len();
931                // Use field name if available, otherwise use index as string
932                let field_name = if !field.name.is_empty() {
933                    field.name
934                } else {
935                    // Tuple fields don't have names, skip span tracking
936                    continue;
937                };
938                ctx.record_field_span(
939                    vec![PathSegment::Field(Cow::Borrowed(field_name))],
940                    (type_start, type_end), // key is the type itself for tuples
941                    (type_start, type_end),
942                );
943            }
944            let type_end_start = ctx.len();
945            write!(ctx.output, ");")?;
946            let type_end_end = ctx.len();
947            ctx.type_end_span = Some((type_end_start, type_end_end));
948        }
949        StructKind::Unit => {
950            write!(ctx.output, "struct ")?;
951            let type_name_start = ctx.len();
952            write!(ctx.output, "{}", shape.type_identifier)?;
953            let type_name_end = ctx.len();
954            ctx.type_name_span = Some((type_name_start, type_name_end));
955            let type_end_start = ctx.len();
956            write!(ctx.output, ";")?;
957            let type_end_end = ctx.len();
958            ctx.type_end_span = Some((type_end_start, type_end_end));
959        }
960    }
961
962    // Record span for the root (empty path) covering the whole type
963    let type_end = ctx.len();
964    ctx.type_span = Some((type_start, type_end));
965    ctx.record_field_span(vec![], (type_start, type_end), (type_start, type_end));
966
967    Ok(())
968}
969
970fn format_enum_with_spans(
971    shape: &Shape,
972    enum_type: &EnumType,
973    ctx: &mut SpanTrackingContext,
974) -> core::fmt::Result {
975    // Track start of the whole type definition
976    let type_start = ctx.len();
977
978    // Write doc comments if enabled
979    if ctx.config.show_doc_comments {
980        write_doc_comments(shape.doc, &mut ctx.output, "")?;
981    }
982
983    // Write #[derive(Facet)]
984    writeln!(ctx.output, "#[derive(Facet)]")?;
985
986    // Write repr for the discriminant type
987    let repr_str = match enum_type.enum_repr {
988        EnumRepr::Rust => None,
989        EnumRepr::RustNPO => None,
990        EnumRepr::U8 => Some("u8"),
991        EnumRepr::U16 => Some("u16"),
992        EnumRepr::U32 => Some("u32"),
993        EnumRepr::U64 => Some("u64"),
994        EnumRepr::USize => Some("usize"),
995        EnumRepr::I8 => Some("i8"),
996        EnumRepr::I16 => Some("i16"),
997        EnumRepr::I32 => Some("i32"),
998        EnumRepr::I64 => Some("i64"),
999        EnumRepr::ISize => Some("isize"),
1000    };
1001
1002    if let Some(repr) = repr_str {
1003        writeln!(ctx.output, "#[repr({repr})]")?;
1004    }
1005
1006    // Write facet attributes if any
1007    write_facet_attrs(shape, &mut ctx.output)?;
1008
1009    // Write third-party attributes if enabled
1010    if ctx.config.show_third_party_attrs {
1011        write_third_party_attrs(shape.attributes, &mut ctx.output, "")?;
1012    }
1013
1014    // Write enum definition
1015    write!(ctx.output, "enum ")?;
1016    let type_name_start = ctx.len();
1017    write!(ctx.output, "{}", shape.type_identifier)?;
1018    let type_name_end = ctx.len();
1019    ctx.type_name_span = Some((type_name_start, type_name_end));
1020    writeln!(ctx.output, " {{")?;
1021
1022    for variant in enum_type.variants {
1023        // Write doc comments for the variant if enabled
1024        if ctx.config.show_doc_comments {
1025            write_doc_comments(variant.doc, &mut ctx.output, "    ")?;
1026        }
1027        // Write third-party attributes for the variant if enabled
1028        if ctx.config.show_third_party_attrs {
1029            write_variant_third_party_attrs(variant, &mut ctx.output, "    ")?;
1030        }
1031
1032        match variant.data.kind {
1033            StructKind::Unit => {
1034                write!(ctx.output, "    ")?;
1035                // For unit variants, key and value are the same (just the variant name)
1036                let name_start = ctx.len();
1037                write!(ctx.output, "{}", variant.name)?;
1038                let name_end = ctx.len();
1039                ctx.record_field_span(
1040                    vec![PathSegment::Variant(Cow::Borrowed(variant.name))],
1041                    (name_start, name_end),
1042                    (name_start, name_end),
1043                );
1044                writeln!(ctx.output, ",")?;
1045            }
1046            StructKind::Tuple | StructKind::TupleStruct => {
1047                write!(ctx.output, "    ")?;
1048                let variant_name_start = ctx.len();
1049                write!(ctx.output, "{}", variant.name)?;
1050                let variant_name_end = ctx.len();
1051                write!(ctx.output, "(")?;
1052                let tuple_start = ctx.len();
1053                for (i, field) in variant.data.fields.iter().enumerate() {
1054                    if i > 0 {
1055                        write!(ctx.output, ", ")?;
1056                    }
1057                    let type_start = ctx.len();
1058                    write_type_name(field.shape(), &mut ctx.output)?;
1059                    let type_end = ctx.len();
1060                    // Track span for variant field
1061                    if !field.name.is_empty() {
1062                        ctx.record_field_span(
1063                            vec![
1064                                PathSegment::Variant(Cow::Borrowed(variant.name)),
1065                                PathSegment::Field(Cow::Borrowed(field.name)),
1066                            ],
1067                            (type_start, type_end),
1068                            (type_start, type_end),
1069                        );
1070                    }
1071                }
1072                write!(ctx.output, ")")?;
1073                let tuple_end = ctx.len();
1074                // Record variant span: key is the name, value is the tuple contents
1075                ctx.record_field_span(
1076                    vec![PathSegment::Variant(Cow::Borrowed(variant.name))],
1077                    (variant_name_start, variant_name_end),
1078                    (tuple_start, tuple_end),
1079                );
1080                writeln!(ctx.output, ",")?;
1081            }
1082            StructKind::Struct => {
1083                write!(ctx.output, "    ")?;
1084                let variant_name_start = ctx.len();
1085                write!(ctx.output, "{}", variant.name)?;
1086                let variant_name_end = ctx.len();
1087                writeln!(ctx.output, " {{")?;
1088                let struct_start = ctx.len();
1089                for field in variant.data.fields {
1090                    // Write doc comments for variant fields if enabled
1091                    if ctx.config.show_doc_comments {
1092                        write_doc_comments(field.doc, &mut ctx.output, "        ")?;
1093                    }
1094                    // Write third-party attributes for variant fields if enabled
1095                    if ctx.config.show_third_party_attrs {
1096                        write_field_third_party_attrs(field, &mut ctx.output, "        ")?;
1097                    }
1098                    write!(ctx.output, "        ")?;
1099                    let key_start = ctx.len();
1100                    write!(ctx.output, "{}", field.name)?;
1101                    let key_end = ctx.len();
1102                    write!(ctx.output, ": ")?;
1103                    let value_start = ctx.len();
1104                    write_type_name(field.shape(), &mut ctx.output)?;
1105                    let value_end = ctx.len();
1106                    ctx.record_field_span(
1107                        vec![
1108                            PathSegment::Variant(Cow::Borrowed(variant.name)),
1109                            PathSegment::Field(Cow::Borrowed(field.name)),
1110                        ],
1111                        (key_start, key_end),
1112                        (value_start, value_end),
1113                    );
1114                    writeln!(ctx.output, ",")?;
1115                }
1116                write!(ctx.output, "    }}")?;
1117                let struct_end = ctx.len();
1118                // Record variant span: key is the name, value is the struct body
1119                ctx.record_field_span(
1120                    vec![PathSegment::Variant(Cow::Borrowed(variant.name))],
1121                    (variant_name_start, variant_name_end),
1122                    (struct_start, struct_end),
1123                );
1124                writeln!(ctx.output, ",")?;
1125            }
1126        }
1127    }
1128
1129    let type_end_start = ctx.len();
1130    write!(ctx.output, "}}")?;
1131    let type_end_end = ctx.len();
1132    ctx.type_end_span = Some((type_end_start, type_end_end));
1133
1134    // Record span for the root (empty path) covering the whole type
1135    let type_end = ctx.len();
1136    ctx.type_span = Some((type_start, type_end));
1137    ctx.record_field_span(vec![], (type_start, type_end), (type_start, type_end));
1138
1139    Ok(())
1140}
1141
1142/// Collect nested user-defined types from struct fields
1143fn collect_nested_types<'a>(struct_type: &'a StructType, queue: &mut Vec<&'a Shape>) {
1144    for field in struct_type.fields {
1145        collect_from_shape(field.shape(), queue);
1146    }
1147}
1148
1149/// Recursively collect user-defined types from a shape (handles containers)
1150fn collect_from_shape<'a>(shape: &'a Shape, queue: &mut Vec<&'a Shape>) {
1151    match shape.def {
1152        Def::List(list_def) => collect_from_shape(list_def.t, queue),
1153        Def::Array(array_def) => collect_from_shape(array_def.t, queue),
1154        Def::Map(map_def) => {
1155            collect_from_shape(map_def.k, queue);
1156            collect_from_shape(map_def.v, queue);
1157        }
1158        Def::Option(option_def) => collect_from_shape(option_def.t, queue),
1159        _ => {
1160            // Check if it's a user-defined type worth expanding
1161            if let Type::User(UserType::Struct(_) | UserType::Enum(_)) = &shape.ty {
1162                queue.push(shape);
1163            }
1164        }
1165    }
1166}
1167
1168// Plain text helpers for span tracking (ANSI codes would break byte offsets)
1169fn write_facet_attrs(shape: &Shape, output: &mut String) -> core::fmt::Result {
1170    let mut attrs: Vec<String> = Vec::new();
1171
1172    if let Some(tag) = shape.get_tag_attr() {
1173        if let Some(content) = shape.get_content_attr() {
1174            attrs.push(alloc::format!("tag = \"{tag}\", content = \"{content}\""));
1175        } else {
1176            attrs.push(alloc::format!("tag = \"{tag}\""));
1177        }
1178    }
1179
1180    if shape.is_untagged() {
1181        attrs.push("untagged".into());
1182    }
1183
1184    if shape.has_deny_unknown_fields_attr() {
1185        attrs.push("deny_unknown_fields".into());
1186    }
1187
1188    if !attrs.is_empty() {
1189        writeln!(output, "#[facet({})]", attrs.join(", "))?;
1190    }
1191
1192    Ok(())
1193}
1194
1195/// Write doc comments (plain text) with the given indentation prefix
1196fn write_doc_comments(doc: &[&str], output: &mut String, indent: &str) -> core::fmt::Result {
1197    for line in doc {
1198        write!(output, "{indent}")?;
1199        writeln!(output, "///{line}")?;
1200    }
1201    Ok(())
1202}
1203
1204/// Write third-party (namespaced) attributes (plain text)
1205fn write_third_party_attrs(
1206    attributes: &[Attr],
1207    output: &mut String,
1208    indent: &str,
1209) -> core::fmt::Result {
1210    // Group attributes by namespace
1211    let mut by_namespace: BTreeMap<&'static str, Vec<&'static str>> = BTreeMap::new();
1212    for attr in attributes {
1213        if let Some(ns) = attr.ns {
1214            by_namespace.entry(ns).or_default().push(attr.key);
1215        }
1216    }
1217
1218    // Write one line per namespace with all keys
1219    for (ns, keys) in by_namespace {
1220        write!(output, "{indent}")?;
1221        write!(output, "#[facet(")?;
1222        for (i, key) in keys.iter().enumerate() {
1223            if i > 0 {
1224                write!(output, ", ")?;
1225            }
1226            write!(output, "{ns}::{key}")?;
1227        }
1228        writeln!(output, ")]")?;
1229    }
1230    Ok(())
1231}
1232
1233/// Write third-party attributes for a field (plain text)
1234fn write_field_third_party_attrs(
1235    field: &Field,
1236    output: &mut String,
1237    indent: &str,
1238) -> core::fmt::Result {
1239    write_third_party_attrs(field.attributes, output, indent)
1240}
1241
1242/// Write third-party attributes for a variant (plain text)
1243fn write_variant_third_party_attrs(
1244    variant: &Variant,
1245    output: &mut String,
1246    indent: &str,
1247) -> core::fmt::Result {
1248    write_third_party_attrs(variant.attributes, output, indent)
1249}
1250
1251fn write_type_name(shape: &Shape, output: &mut String) -> core::fmt::Result {
1252    match shape.def {
1253        Def::Scalar => {
1254            write!(output, "{}", shape.type_identifier)?;
1255        }
1256        Def::Pointer(_) => {
1257            if let Type::Pointer(PointerType::Reference(r)) = shape.ty
1258                && let Def::Array(array_def) = r.target.def
1259            {
1260                write!(output, "&[")?;
1261                write_type_name(array_def.t, output)?;
1262                write!(output, "; {}]", array_def.n)?;
1263                return Ok(());
1264            }
1265            write!(output, "{}", shape.type_identifier)?;
1266        }
1267        Def::List(list_def) => {
1268            write!(output, "Vec<")?;
1269            write_type_name(list_def.t, output)?;
1270            write!(output, ">")?;
1271        }
1272        Def::Array(array_def) => {
1273            write!(output, "[")?;
1274            write_type_name(array_def.t, output)?;
1275            write!(output, "; {}]", array_def.n)?;
1276        }
1277        Def::Map(map_def) => {
1278            let map_name = if shape.type_identifier.contains("BTreeMap") {
1279                "BTreeMap"
1280            } else {
1281                "HashMap"
1282            };
1283            write!(output, "{map_name}<")?;
1284            write_type_name(map_def.k, output)?;
1285            write!(output, ", ")?;
1286            write_type_name(map_def.v, output)?;
1287            write!(output, ">")?;
1288        }
1289        Def::Option(option_def) => {
1290            write!(output, "Option<")?;
1291            write_type_name(option_def.t, output)?;
1292            write!(output, ">")?;
1293        }
1294        _ => {
1295            write!(output, "{}", shape.type_identifier)?;
1296        }
1297    }
1298    Ok(())
1299}
1300
1301#[cfg(test)]
1302mod tests {
1303    use super::*;
1304    use facet::Facet;
1305
1306    #[test]
1307    fn test_simple_struct() {
1308        #[derive(Facet)]
1309        struct Simple {
1310            name: String,
1311            count: u32,
1312        }
1313
1314        let output = format_shape(Simple::SHAPE);
1315        assert!(output.contains("struct Simple"));
1316        assert!(output.contains("name: String"));
1317        assert!(output.contains("count: u32"));
1318    }
1319
1320    #[test]
1321    fn test_enum_with_tag() {
1322        #[derive(Facet)]
1323        #[repr(C)]
1324        #[facet(tag = "type")]
1325        #[allow(dead_code)]
1326        enum Tagged {
1327            A { x: i32 },
1328            B { y: String },
1329        }
1330
1331        let output = format_shape(Tagged::SHAPE);
1332        assert!(output.contains("enum Tagged"));
1333        assert!(output.contains("#[facet(tag = \"type\")]"));
1334    }
1335
1336    #[test]
1337    fn test_nested_types() {
1338        #[derive(Facet)]
1339        #[allow(dead_code)]
1340        struct Inner {
1341            value: i32,
1342        }
1343
1344        #[derive(Facet)]
1345        #[allow(dead_code)]
1346        struct Outer {
1347            inner: Inner,
1348            name: String,
1349        }
1350
1351        let output = format_shape(Outer::SHAPE);
1352        // Should contain both Outer and Inner definitions
1353        assert!(output.contains("struct Outer"), "Missing Outer: {output}");
1354        assert!(
1355            output.contains("inner: Inner"),
1356            "Missing inner field: {output}"
1357        );
1358        assert!(
1359            output.contains("struct Inner"),
1360            "Missing Inner definition: {output}"
1361        );
1362        assert!(
1363            output.contains("value: i32"),
1364            "Missing value field: {output}"
1365        );
1366    }
1367
1368    #[test]
1369    fn test_nested_in_vec() {
1370        #[derive(Facet)]
1371        #[allow(dead_code)]
1372        struct Item {
1373            id: u32,
1374        }
1375
1376        #[derive(Facet)]
1377        #[allow(dead_code)]
1378        struct Container {
1379            items: Vec<Item>,
1380        }
1381
1382        let output = format_shape(Container::SHAPE);
1383        // Should contain both Container and Item definitions
1384        assert!(
1385            output.contains("struct Container"),
1386            "Missing Container: {output}"
1387        );
1388        assert!(
1389            output.contains("items: Vec<Item>"),
1390            "Missing items field: {output}"
1391        );
1392        assert!(
1393            output.contains("struct Item"),
1394            "Missing Item definition: {output}"
1395        );
1396    }
1397
1398    #[test]
1399    fn test_format_shape_with_spans() {
1400        #[derive(Facet)]
1401        #[allow(dead_code)]
1402        struct Config {
1403            name: String,
1404            max_retries: u8,
1405            enabled: bool,
1406        }
1407
1408        let result = format_shape_with_spans(Config::SHAPE);
1409
1410        // Check that spans were recorded for each field
1411        let name_path = vec![PathSegment::Field(Cow::Borrowed("name"))];
1412        let retries_path = vec![PathSegment::Field(Cow::Borrowed("max_retries"))];
1413        let enabled_path = vec![PathSegment::Field(Cow::Borrowed("enabled"))];
1414
1415        assert!(
1416            result.spans.contains_key(&name_path),
1417            "Missing span for 'name' field. Spans: {:?}",
1418            result.spans
1419        );
1420        assert!(
1421            result.spans.contains_key(&retries_path),
1422            "Missing span for 'max_retries' field. Spans: {:?}",
1423            result.spans
1424        );
1425        assert!(
1426            result.spans.contains_key(&enabled_path),
1427            "Missing span for 'enabled' field. Spans: {:?}",
1428            result.spans
1429        );
1430
1431        // Verify the span for max_retries points to "u8"
1432        let field_span = &result.spans[&retries_path];
1433        let spanned_text = &result.text[field_span.value.0..field_span.value.1];
1434        assert_eq!(spanned_text, "u8", "Expected 'u8', got '{spanned_text}'");
1435    }
1436
1437    #[test]
1438    fn test_format_enum_with_spans() {
1439        #[derive(Facet)]
1440        #[repr(u8)]
1441        #[allow(dead_code)]
1442        enum Status {
1443            Active,
1444            Pending,
1445            Error { code: i32, message: String },
1446        }
1447
1448        let result = format_shape_with_spans(Status::SHAPE);
1449
1450        // Check variant spans
1451        let active_path = vec![PathSegment::Variant(Cow::Borrowed("Active"))];
1452        let error_path = vec![PathSegment::Variant(Cow::Borrowed("Error"))];
1453        let error_code_path = vec![
1454            PathSegment::Variant(Cow::Borrowed("Error")),
1455            PathSegment::Field(Cow::Borrowed("code")),
1456        ];
1457
1458        assert!(
1459            result.spans.contains_key(&active_path),
1460            "Missing span for 'Active' variant. Spans: {:?}",
1461            result.spans
1462        );
1463        assert!(
1464            result.spans.contains_key(&error_path),
1465            "Missing span for 'Error' variant. Spans: {:?}",
1466            result.spans
1467        );
1468        assert!(
1469            result.spans.contains_key(&error_code_path),
1470            "Missing span for 'Error.code' field. Spans: {:?}",
1471            result.spans
1472        );
1473
1474        // Verify the span for code points to "i32"
1475        let field_span = &result.spans[&error_code_path];
1476        let spanned_text = &result.text[field_span.value.0..field_span.value.1];
1477        assert_eq!(spanned_text, "i32", "Expected 'i32', got '{spanned_text}'");
1478    }
1479
1480    #[test]
1481    fn test_format_with_doc_comments() {
1482        /// A configuration struct for the application.
1483        #[derive(Facet)]
1484        #[allow(dead_code)]
1485        struct Config {
1486            /// The name of the configuration.
1487            name: String,
1488            /// Maximum number of retries.
1489            max_retries: u8,
1490        }
1491
1492        // With doc comments (default)
1493        let output = format_shape(Config::SHAPE);
1494        assert!(
1495            output.contains("/// A configuration struct"),
1496            "Should contain struct doc comment: {output}"
1497        );
1498        assert!(
1499            output.contains("/// The name of the configuration"),
1500            "Should contain field doc comment: {output}"
1501        );
1502        assert!(
1503            output.contains("/// Maximum number of retries"),
1504            "Should contain field doc comment: {output}"
1505        );
1506
1507        // Without doc comments (explicit config)
1508        let config = ShapeFormatConfig::new();
1509        let output_without = format_shape_with_config(Config::SHAPE, &config);
1510        assert!(
1511            !output_without.contains("///"),
1512            "Should not contain doc comments when disabled: {output_without}"
1513        );
1514    }
1515
1516    #[test]
1517    fn test_format_enum_with_doc_comments() {
1518        /// Status of an operation.
1519        #[derive(Facet)]
1520        #[repr(u8)]
1521        #[allow(dead_code)]
1522        enum Status {
1523            /// The operation is active.
1524            Active,
1525            /// The operation failed with an error.
1526            Error {
1527                /// Error code.
1528                code: i32,
1529            },
1530        }
1531
1532        let config = ShapeFormatConfig::new().with_doc_comments();
1533        let output = format_shape_with_config(Status::SHAPE, &config);
1534
1535        assert!(
1536            output.contains("/// Status of an operation"),
1537            "Should contain enum doc comment: {output}"
1538        );
1539        assert!(
1540            output.contains("/// The operation is active"),
1541            "Should contain variant doc comment: {output}"
1542        );
1543        assert!(
1544            output.contains("/// The operation failed"),
1545            "Should contain variant doc comment: {output}"
1546        );
1547        assert!(
1548            output.contains("/// Error code"),
1549            "Should contain variant field doc comment: {output}"
1550        );
1551    }
1552}