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