Skip to main content

m2_syn/
pretty.rs

1//! Terminal-oriented inspection of source, raw tokens, and typed syntax trees.
2//!
3//! [`PrettyReport`] combines the three useful views of a parsed file. The
4//! renderer deliberately owns only presentation data; the generated typed CST
5//! remains the source of truth and implements [`PrettyTree`] directly.
6
7use std::fmt::{Display, Formatter, Result as FmtResult, Write as FmtWrite};
8use std::io::{self, IsTerminal as _, Write as IoWrite};
9
10use crate::{
11    CellTokenStream, CommentTrivia, DelimiterKind, DoubleSpan, Punctuated, Span, Spanned,
12    TokenStream, TokenTree, Trivia, TriviaKind, token_stream::delim::Delimiter,
13};
14
15/// The RESET constant.
16const RESET: &str = "\u{1b}[0m";
17/// The BOLD CYAN constant.
18const BOLD_CYAN: &str = "\u{1b}[1;36m";
19/// The BRIGHT YELLOW constant.
20const BRIGHT_YELLOW: &str = "\u{1b}[93m";
21/// The BRIGHT BLACK constant.
22const BRIGHT_BLACK: &str = "\u{1b}[90m";
23/// The RED constant.
24const RED: &str = "\u{1b}[31m";
25/// The GREEN constant.
26const GREEN: &str = "\u{1b}[32m";
27/// The YELLOW constant.
28const YELLOW: &str = "\u{1b}[33m";
29/// The BLUE constant.
30const BLUE: &str = "\u{1b}[34m";
31/// The MAGENTA constant.
32const MAGENTA: &str = "\u{1b}[35m";
33/// The CYAN constant.
34const CYAN: &str = "\u{1b}[36m";
35/// The WHITE constant.
36const WHITE: &str = "\u{1b}[37m";
37/// The NON TERMINAL BLUE constant.
38const NON_TERMINAL_BLUE: &str = BLUE;
39/// The DIM constant.
40const DIM: &str = "\u{1b}[2m";
41
42/// Converts typed syntax into the structural presentation used by the pretty
43/// printer.
44///
45/// Implementations for generated nodes are emitted from `syntax!`, so
46/// field names, punctuation, and newly added concrete node types appear in the
47/// tree without a handwritten registry. Semantic category enums remain visible
48/// because they define visitor dispatch, while generic containers are
49/// transparent storage details rather than CST nodes.
50pub trait PrettyTree {
51    /// Builds the presentation node for this syntax value.
52    fn pretty_tree(&self) -> PrettyNode;
53}
54
55/// One owned node in a syntax presentation tree.
56///
57/// The structure is independent of terminal layout. External renderers can
58/// recursively inspect [`Self::children`], use [`Self::role`] and
59/// [`PrettyDetail::role`] for styling, and apply their own width and glyph
60/// policies.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct PrettyNode {
63    /// The name.
64    name: String,
65    /// The detail.
66    detail: Option<PrettyDetail>,
67    /// The span.
68    span: Option<Span>,
69    /// The role.
70    role: PrettyNodeRole,
71    /// The transparent.
72    transparent: bool,
73    /// The children.
74    children: Vec<PrettyEdge>,
75}
76
77/// Additional text displayed alongside a [`PrettyNode`].
78///
79/// Token nodes use details for their source spelling, while virtual syntax
80/// elements use them for their conventional display glyph.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct PrettyDetail {
83    /// The text.
84    text: String,
85    /// The role.
86    role: PrettyDetailRole,
87}
88
89/// The semantic role of a [`PrettyDetail`].
90///
91/// Renderers can use this role to select their own highlight group without
92/// depending on the built-in ANSI palette.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum PrettyDetailRole {
95    /// Source text belonging to a lexical token.
96    Text,
97    /// A conventional glyph representing a virtual syntax element.
98    Glyph,
99}
100
101/// The semantic role of a [`PrettyNode`].
102///
103/// This is presentation metadata rather than a replacement for the generated
104/// typed syntax hierarchy. It lets external renderers distinguish structural
105/// nodes, lexical leaves, delimiter boundaries, collection fragments, and
106/// absent optional values.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum PrettyNodeRole {
109    /// A concrete node from the generated typed syntax hierarchy.
110    Syntax,
111    /// A semantic category through which visitor specialization dispatches.
112    Category,
113    /// A lexical token or virtual syntax leaf.
114    Token,
115    /// One boundary projected from a paired delimiter atom.
116    Delimiter,
117    /// A transparent syntax container such as a list or delimiter pair.
118    Collection,
119    /// An optional syntax value that is not present.
120    Absent,
121}
122
123/// A labeled parent-to-child edge in a [`PrettyNode`] tree.
124///
125/// The label is normally the generated field name or the index of an element
126/// in a collection.
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct PrettyEdge {
129    /// The label.
130    label: String,
131    /// The node.
132    node: PrettyNode,
133}
134
135impl PrettyDetail {
136    /// Returns the text carried by this detail.
137    pub fn text(&self) -> &str {
138        &self.text
139    }
140
141    /// Returns the semantic role of this detail.
142    pub fn role(&self) -> PrettyDetailRole {
143        self.role
144    }
145}
146
147impl PrettyEdge {
148    /// Returns the field name or collection index labeling this edge.
149    pub fn label(&self) -> &str {
150        &self.label
151    }
152
153    /// Returns the child presentation node.
154    pub fn node(&self) -> &PrettyNode {
155        &self.node
156    }
157}
158
159impl PrettyNode {
160    /// Returns the display name of this node.
161    pub fn name(&self) -> &str {
162        &self.name
163    }
164
165    /// Returns the optional token spelling or virtual-element glyph.
166    pub fn detail(&self) -> Option<&PrettyDetail> {
167        self.detail.as_ref()
168    }
169
170    /// Returns the source span represented by this node, when available.
171    pub fn span(&self) -> Option<&Span> {
172        self.span.as_ref()
173    }
174
175    /// Returns the semantic presentation role of this node.
176    pub fn role(&self) -> PrettyNodeRole {
177        self.role
178    }
179
180    /// Returns the labeled children in source-tree order.
181    pub fn children(&self) -> &[PrettyEdge] {
182        &self.children
183    }
184
185    /// Creates a generated concrete syntax node.
186    #[doc(hidden)]
187    pub fn syntax(name: impl Into<String>, span: Option<Span>) -> Self {
188        Self::new(name, span, PrettyNodeRole::Syntax)
189    }
190
191    /// Creates a generated enum-variant node.
192    #[doc(hidden)]
193    pub fn variant(category: &str, _variant: &str, span: Option<Span>, value: PrettyNode) -> Self {
194        let mut node = Self::new(category, span, PrettyNodeRole::Category);
195        node.push_node("", value);
196        node
197    }
198
199    /// Creates a delimiter collection with explicit opening and closing boundaries.
200    #[doc(hidden)]
201    pub fn delimiter(kind: DelimiterKind, spans: DoubleSpan) -> Self {
202        let delimiter = Delimiter::new(kind, spans);
203        let mut node = Self::collection(
204            format!("{}…{} · 2 tokens", kind.opening(), kind.closing()),
205            delimiter.try_span(),
206        );
207        node.push_node(
208            "opening",
209            Self::delimiter_boundary(kind, true, delimiter.opening_span()),
210        );
211        if let Some(closing) = delimiter.closing_span() {
212            node.push_node("closing", Self::delimiter_boundary(kind, false, closing));
213        }
214        node
215    }
216
217    /// Creates one displayed boundary of a canonical paired delimiter atom.
218    #[doc(hidden)]
219    pub fn delimiter_boundary(kind: DelimiterKind, opening: bool, span: Span) -> Self {
220        let name = match kind {
221            DelimiterKind::Parenthesis => "Delimiter![()]",
222            DelimiterKind::Bracket => "Delimiter![[]]",
223            DelimiterKind::Brace => "Delimiter![{}]",
224            DelimiterKind::AngleBar => "Delimiter![<| |>]",
225        };
226        let spelling = if opening {
227            kind.opening()
228        } else {
229            kind.closing()
230        };
231        let mut node = Self::new(name, Some(span), PrettyNodeRole::Delimiter);
232        node.detail = Some(PrettyDetail {
233            text: spelling.into(),
234            role: PrettyDetailRole::Text,
235        });
236        node
237    }
238
239    /// Creates a leaf containing token or source text.
240    #[doc(hidden)]
241    pub fn token(name: impl Into<String>, text: impl Into<String>, span: Span) -> Self {
242        let name = name.into();
243        let text = text.into();
244        if name == "Delimiter"
245            && let Some(kind) = delimiter_notation(&text)
246        {
247            return Self::delimiter(kind, DoubleSpan::new(span.clone(), span));
248        }
249        let role = if matches!(text.as_str(), "∅" | "EOC" | "BOF" | "EOF") {
250            PrettyDetailRole::Glyph
251        } else {
252            PrettyDetailRole::Text
253        };
254        let mut node = Self::new(name, Some(span), PrettyNodeRole::Token);
255        node.detail = Some(PrettyDetail { text, role });
256        node
257    }
258
259    /// Creates a collection wrapper.
260    #[doc(hidden)]
261    pub fn collection(name: impl Into<String>, span: Option<Span>) -> Self {
262        let mut node = Self::new(name, span, PrettyNodeRole::Collection);
263        node.transparent = true;
264        node
265    }
266
267    /// Creates a visible grouping node that has collection styling.
268    #[doc(hidden)]
269    pub fn group(name: impl Into<String>, span: Option<Span>) -> Self {
270        Self::new(name, span, PrettyNodeRole::Collection)
271    }
272
273    /// Creates an absent optional value.
274    #[doc(hidden)]
275    pub fn absent() -> Self {
276        Self::new("None", None, PrettyNodeRole::Absent)
277    }
278
279    /// Creates a new value.
280    fn new(name: impl Into<String>, span: Option<Span>, role: PrettyNodeRole) -> Self {
281        Self {
282            name: name.into(),
283            detail: None,
284            span,
285            role,
286            transparent: false,
287            children: Vec::new(),
288        }
289    }
290
291    /// Adds a named child to this node.
292    #[doc(hidden)]
293    pub fn push(&mut self, label: impl Into<String>, child: impl PrettyTree) {
294        self.push_node(label, child.pretty_tree());
295    }
296
297    /// Adds an already constructed named child to this node.
298    ///
299    /// Collection nodes are presentation fragments rather than CST nodes, so
300    /// their children are inserted directly into this node.
301    #[doc(hidden)]
302    pub fn push_node(&mut self, label: impl Into<String>, node: PrettyNode) {
303        if node.transparent {
304            for child in node.children {
305                self.push_node(child.label, child.node);
306            }
307            return;
308        }
309        let mut node = node;
310        if node.span.is_none() {
311            node.span.clone_from(&self.span);
312        }
313        self.children.push(PrettyEdge {
314            label: label.into(),
315            node,
316        });
317    }
318}
319
320/// Handles delimiter notation.
321fn delimiter_notation(notation: &str) -> Option<DelimiterKind> {
322    match notation {
323        "(…)" => Some(DelimiterKind::Parenthesis),
324        "[…]" => Some(DelimiterKind::Bracket),
325        "{…}" => Some(DelimiterKind::Brace),
326        "<|…|>" => Some(DelimiterKind::AngleBar),
327        _ => None,
328    }
329}
330
331impl<T: PrettyTree + ?Sized> PrettyTree for &T {
332    fn pretty_tree(&self) -> PrettyNode {
333        T::pretty_tree(self)
334    }
335}
336
337impl<T: PrettyTree> PrettyTree for Box<T> {
338    fn pretty_tree(&self) -> PrettyNode {
339        T::pretty_tree(self)
340    }
341}
342
343impl<T: PrettyTree + Spanned> PrettyTree for Option<T> {
344    fn pretty_tree(&self) -> PrettyNode {
345        match self {
346            Some(value) => value.pretty_tree(),
347            None => PrettyNode::absent(),
348        }
349    }
350}
351
352impl<T: PrettyTree + Spanned> PrettyTree for Vec<T> {
353    fn pretty_tree(&self) -> PrettyNode {
354        let span = Span::join_all(self.iter().map(Spanned::span));
355        let mut node = PrettyNode::collection(item_count("List", self.len()), span);
356        for (index, value) in self.iter().enumerate() {
357            node.push(format!("[{index}]"), value);
358        }
359        node
360    }
361}
362
363impl PrettyTree for Trivia {
364    fn pretty_tree(&self) -> PrettyNode {
365        let name = match self.kind() {
366            TriviaKind::LineComment => "LineCommentBlock",
367            TriviaKind::BlockComment => "BlockComment",
368        };
369        PrettyNode::token(name, self.text(), self.span())
370    }
371}
372
373impl PrettyTree for CommentTrivia {
374    fn pretty_tree(&self) -> PrettyNode {
375        let count = self.outer().len() + self.inner().len();
376        let mut node = PrettyNode::group(item_count("Comments", count), self.try_span());
377        for (index, comment) in self.outer().iter().enumerate() {
378            node.push(format!("outer[{index}]"), comment);
379        }
380        for (index, comment) in self.inner().iter().enumerate() {
381            node.push(format!("inner[{index}]"), comment);
382        }
383        node
384    }
385}
386
387impl<T, P> PrettyTree for Punctuated<T, P>
388where
389    T: PrettyTree + Spanned,
390    P: PrettyTree + Spanned,
391{
392    fn pretty_tree(&self) -> PrettyNode {
393        let mut node =
394            PrettyNode::collection(item_count("Punctuated", self.len()), self.try_span());
395        node.push("[0]", self.first());
396        for (index, (punctuation, value)) in self.pairs().enumerate() {
397            node.push("punctuation", punctuation);
398            node.push(format!("[{}]", index + 1), value);
399        }
400        node
401    }
402}
403
404/// Handles item count.
405fn item_count(name: &str, count: usize) -> String {
406    let suffix = if count == 1 { "item" } else { "items" };
407    format!("{name} · {count} {suffix}")
408}
409
410/// Formatting choices shared by token, CST, and combined reports.
411#[derive(Debug, Clone, Copy, PartialEq, Eq)]
412pub struct PrettyOptions {
413    /// Whether to emit ANSI color escapes.
414    pub color: bool,
415    /// Whether to display source spans.
416    pub spans: bool,
417    /// Whether to include trivia in raw token views.
418    pub trivia: bool,
419    /// The preferred output width.
420    pub width: usize,
421}
422
423impl PrettyOptions {
424    /// Plain, deterministic output suitable for tests and redirected output.
425    pub const PLAIN: Self = Self {
426        color: false,
427        spans: true,
428        trivia: false,
429        width: 150,
430    };
431
432    /// ANSI-colored output intended for an interactive terminal.
433    pub const ANSI: Self = Self {
434        color: true,
435        spans: true,
436        trivia: false,
437        width: 150,
438    };
439}
440
441impl Default for PrettyOptions {
442    fn default() -> Self {
443        Self::PLAIN
444    }
445}
446
447/// A displayable three-part inspection report for one parsed source file.
448///
449/// The report shows the retained input snapshot, a recursively flattened raw
450/// token table, and the complete generated typed CST. Use [`Self::ansi`] for
451/// explicit terminal color or [`crate::ParsedFile::print_pretty`] for automatic
452/// terminal detection.
453pub struct PrettyReport<'source> {
454    /// The source.
455    source: &'source str,
456    /// The tokens.
457    tokens: TokenStream,
458    /// The CST.
459    cst: PrettyNode,
460    /// The options.
461    options: PrettyOptions,
462}
463
464impl<'source> PrettyReport<'source> {
465    /// Creates a plain report from its three projections.
466    #[doc(hidden)]
467    pub fn new(source: &'source str, tokens: TokenStream, cst: PrettyNode) -> Self {
468        Self {
469            source,
470            tokens,
471            cst,
472            options: PrettyOptions::PLAIN,
473        }
474    }
475
476    /// Enables or disables ANSI terminal styling.
477    pub fn ansi(mut self, enabled: bool) -> Self {
478        self.options.color = enabled;
479        self
480    }
481
482    /// Enables or disables source ranges.
483    pub fn spans(mut self, enabled: bool) -> Self {
484        self.options.spans = enabled;
485        self
486    }
487
488    /// Includes or hides coalesced trivia in the flat token view.
489    pub fn trivia(mut self, enabled: bool) -> Self {
490        self.options.trivia = enabled;
491        self
492    }
493
494    /// Sets the preferred report width used to wrap horizontal token bands.
495    pub fn width(mut self, width: usize) -> Self {
496        self.options.width = width.max(40);
497        self
498    }
499}
500
501impl Display for PrettyReport<'_> {
502    fn fmt(&self, output: &mut Formatter<'_>) -> FmtResult {
503        let mut source = String::new();
504        write_source(&mut source, self.source)?;
505        write_box(output, "SOURCE", &source, self.options)?;
506        writeln!(output)?;
507
508        let mut tokens = String::new();
509        write_tokens(&mut tokens, &self.tokens, self.options)?;
510        write_box(output, "TOKEN STREAM", &tokens, self.options)?;
511        writeln!(output)?;
512
513        let mut cst = String::new();
514        write_tree(&mut cst, &self.cst, self.options)?;
515        write_box(output, "TYPED CST", &cst, self.options)
516    }
517}
518
519/// Handles print report.
520pub(crate) fn print_report(report: PrettyReport<'_>) -> io::Result<()> {
521    let mut output = io::stdout().lock();
522    let width = std::env::var("COLUMNS")
523        .ok()
524        .and_then(|width| width.parse().ok())
525        .unwrap_or(120);
526    let report = report.ansi(output.is_terminal()).width(width);
527    write!(output, "{report}")
528}
529
530/// Formats tree.
531pub(crate) fn format_tree(node: &PrettyNode, options: PrettyOptions) -> String {
532    let mut output = String::new();
533    write_tree(&mut output, node, options).expect("writing to a string cannot fail");
534    output
535}
536
537/// Formats one typed syntax value as a Unicode tree without terminal colors.
538pub fn format_pretty_tree(value: &impl PrettyTree) -> String {
539    format_tree(&value.pretty_tree(), PrettyOptions::PLAIN)
540}
541
542/// Writes box.
543fn write_box(
544    output: &mut impl FmtWrite,
545    title: &str,
546    contents: &str,
547    options: PrettyOptions,
548) -> FmtResult {
549    let inner_width = options.width.saturating_sub(4);
550    writeln!(
551        output,
552        "{}{}{}{}",
553        paint(DIM, "╭─ ", options),
554        paint(BOLD_CYAN, title, options),
555        paint(
556            DIM,
557            &format!(
558                " {}",
559                "─".repeat(options.width.saturating_sub(title.chars().count() + 5))
560            ),
561            options,
562        ),
563        paint(DIM, "╮", options),
564    )?;
565    for line in contents.lines() {
566        let line = clip_ansi(line, inner_width);
567        let line_width = ansi_width(&line);
568        writeln!(
569            output,
570            "{} {}{} {}",
571            paint(DIM, "│", options),
572            line,
573            " ".repeat(inner_width.saturating_sub(line_width)),
574            paint(DIM, "│", options),
575        )?;
576    }
577    writeln!(
578        output,
579        "{}{}{}",
580        paint(DIM, "╰", options),
581        paint(DIM, &"─".repeat(options.width.saturating_sub(2)), options),
582        paint(DIM, "╯", options),
583    )
584}
585
586/// Handles ansi width.
587fn ansi_width(text: &str) -> usize {
588    let mut characters = text.chars().peekable();
589    let mut width = 0;
590    while let Some(character) = characters.next() {
591        if character == '\u{1b}' && characters.peek() == Some(&'[') {
592            for escaped in characters.by_ref() {
593                if escaped == 'm' {
594                    break;
595                }
596            }
597        } else {
598            width += 1;
599        }
600    }
601    width
602}
603
604/// Handles clip ansi.
605fn clip_ansi(text: &str, width: usize) -> String {
606    if ansi_width(text) <= width {
607        return text.to_owned();
608    }
609
610    let limit = width.saturating_sub(1);
611    let mut output = String::new();
612    let mut characters = text.chars().peekable();
613    let mut visible = 0;
614    let mut styled = false;
615    while let Some(character) = characters.next() {
616        if character == '\u{1b}' && characters.peek() == Some(&'[') {
617            styled = true;
618            output.push(character);
619            for escaped in characters.by_ref() {
620                output.push(escaped);
621                if escaped == 'm' {
622                    break;
623                }
624            }
625        } else if visible < limit {
626            output.push(character);
627            visible += 1;
628        } else {
629            break;
630        }
631    }
632    output.push('…');
633    if styled {
634        output.push_str(RESET);
635    }
636    output
637}
638
639/// Writes source.
640fn write_source(output: &mut impl FmtWrite, source: &str) -> FmtResult {
641    let width = source.lines().count().max(1).to_string().len();
642    if source.is_empty() {
643        writeln!(output, "∅")?;
644    } else {
645        for (index, line) in source.lines().enumerate() {
646            writeln!(output, "{:>width$} │ {}", index + 1, line)?;
647        }
648    }
649    Ok(())
650}
651
652#[derive(Clone, Copy)]
653/// Internal alternatives for FlatKind.
654enum FlatKind {
655    /// The Adj alternative.
656    Adj,
657    /// The Open alternative.
658    Open,
659    /// The Close alternative.
660    Close,
661    /// The Ident alternative.
662    Ident,
663    /// The Literal alternative.
664    Literal,
665    /// The Punct alternative.
666    Punct,
667    /// The Trivia alternative.
668    Trivia,
669}
670
671#[derive(Clone)]
672/// Internal state for FlatToken.
673struct FlatToken {
674    /// The depth.
675    depth: usize,
676    /// The kind.
677    kind: FlatKind,
678    /// The text.
679    text: String,
680    /// The span.
681    span: Span,
682}
683
684/// Formats tokens.
685pub(crate) fn format_tokens(tokens: &TokenStream, options: PrettyOptions) -> String {
686    let mut output = String::new();
687    write_tokens(&mut output, tokens, options).expect("writing to a string cannot fail");
688    output
689}
690
691/// Formats a raw token stream as a table with explicit group boundaries.
692pub fn format_pretty_tokens(tokens: &TokenStream) -> String {
693    format_tokens(tokens, PrettyOptions::PLAIN)
694}
695
696/// Formats one framed cell as a table with explicit inferred adjacency.
697pub fn format_pretty_cell_tokens(tokens: &CellTokenStream) -> String {
698    format_tokens(
699        &tokens.clone().into_parser_token_stream(),
700        PrettyOptions::PLAIN,
701    )
702}
703
704/// Writes tokens.
705fn write_tokens(
706    output: &mut impl FmtWrite,
707    tokens: &TokenStream,
708    options: PrettyOptions,
709) -> FmtResult {
710    let mut flat = Vec::new();
711    flatten(tokens, 0, &mut flat);
712    write_flat(output, flat, options)
713}
714
715/// Writes flat.
716fn write_flat(
717    output: &mut impl FmtWrite,
718    mut flat: Vec<FlatToken>,
719    options: PrettyOptions,
720) -> FmtResult {
721    if !options.trivia {
722        flat.retain(|token| !matches!(token.kind, FlatKind::Trivia));
723    }
724    if flat.is_empty() {
725        return writeln!(output, "{}", paint(DIM, "∅", options));
726    }
727    let available = options.width.saturating_sub(12).max(24);
728    let mut band = Vec::new();
729    let mut used = 0;
730    let mut wrote_band = false;
731    for chunk in flat.iter().map(std::slice::from_ref) {
732        let chunk_width = token_band_width(chunk);
733        if !band.is_empty() && used + 1 + chunk_width > available {
734            if wrote_band {
735                writeln!(output)?;
736            }
737            write_token_band(output, &band, options)?;
738            wrote_band = true;
739            band.clear();
740            used = 0;
741        }
742        if !band.is_empty() {
743            used += 1;
744        }
745        used += chunk_width;
746        band.extend_from_slice(chunk);
747    }
748    if !band.is_empty() {
749        if wrote_band {
750            writeln!(output)?;
751        }
752        write_token_band(output, &band, options)?;
753    }
754    Ok(())
755}
756
757/// Handles token band width.
758fn token_band_width(tokens: &[FlatToken]) -> usize {
759    tokens.iter().map(token_column_width).sum::<usize>() + tokens.len().saturating_sub(1)
760}
761
762/// Handles token column width.
763fn token_column_width(token: &FlatToken) -> usize {
764    visible_text(&token.text)
765        .chars()
766        .count()
767        .max(flat_kind(token.kind).chars().count())
768        .clamp(3, 24)
769        + 2
770}
771
772/// Writes token band.
773fn write_token_band(
774    output: &mut impl FmtWrite,
775    tokens: &[FlatToken],
776    options: PrettyOptions,
777) -> FmtResult {
778    write!(
779        output,
780        "{} {} ",
781        paint(BRIGHT_YELLOW, "token", options),
782        paint(DIM, "│", options)
783    )?;
784    for (index, token) in tokens.iter().enumerate() {
785        if index != 0 {
786            write!(output, "{}", paint(DIM, "│", options))?;
787        }
788        let width = token_column_width(token) - 2;
789        let text = clipped(&visible_text(&token.text), width);
790        let cell = centered_cell(&text, width);
791        write!(output, "{}", paint(token_color(token.kind), &cell, options))?;
792    }
793    writeln!(output)?;
794
795    write!(
796        output,
797        "{} {} ",
798        paint(BRIGHT_YELLOW, " type", options),
799        paint(DIM, "│", options)
800    )?;
801    for (index, token) in tokens.iter().enumerate() {
802        if index != 0 {
803            write!(output, "{}", paint(DIM, "│", options))?;
804        }
805        let width = token_column_width(token) - 2;
806        let kind = clipped(flat_kind(token.kind), width);
807        let cell = centered_cell(&kind, width);
808        write!(output, "{}", paint(token_color(token.kind), &cell, options))?;
809    }
810    writeln!(output)
811}
812
813/// Handles centered cell.
814fn centered_cell(text: &str, width: usize) -> String {
815    let padding = width.saturating_sub(text.chars().count());
816    let left = padding / 2;
817    let right = padding - left;
818    format!(" {}{}{} ", " ".repeat(left), text, " ".repeat(right))
819}
820
821/// Handles clipped.
822fn clipped(text: &str, width: usize) -> String {
823    let count = text.chars().count();
824    if count <= width {
825        return text.to_owned();
826    }
827    text.chars()
828        .take(width.saturating_sub(1))
829        .chain(std::iter::once('…'))
830        .collect()
831}
832
833/// Handles flatten.
834fn flatten(tokens: &TokenStream, depth: usize, output: &mut Vec<FlatToken>) {
835    flatten_trivia(tokens.prefix_trivia(), depth, output);
836    for token in tokens.iter() {
837        flatten_trivia(token.leading_trivia().as_slice(), depth, output);
838        match token {
839            TokenTree::Group(group) => {
840                let kind = group.delim_kind();
841                push_flat(
842                    output,
843                    FlatToken {
844                        depth,
845                        kind: FlatKind::Open,
846                        text: kind.opening().into(),
847                        span: group.delimiter().opening_span(),
848                    },
849                );
850                flatten(group.stream(), depth + 1, output);
851                if let Some(closing) = group.delimiter().closing_span() {
852                    push_flat(
853                        output,
854                        FlatToken {
855                            depth,
856                            kind: FlatKind::Close,
857                            text: kind.closing().into(),
858                            span: closing,
859                        },
860                    );
861                }
862            }
863            TokenTree::Ident(token) => push_flat(
864                output,
865                FlatToken {
866                    depth,
867                    kind: FlatKind::Ident,
868                    text: token.text().into(),
869                    span: token.expect_span(),
870                },
871            ),
872            TokenTree::Literal(token) => push_flat(
873                output,
874                FlatToken {
875                    depth,
876                    kind: FlatKind::Literal,
877                    text: token.text().into(),
878                    span: token.expect_span(),
879                },
880            ),
881            TokenTree::Punct(token) => push_flat(
882                output,
883                FlatToken {
884                    depth,
885                    kind: if token.text().is_empty() {
886                        FlatKind::Adj
887                    } else {
888                        FlatKind::Punct
889                    },
890                    text: if token.text().is_empty() {
891                        "Adj".into()
892                    } else {
893                        token.text().into()
894                    },
895                    span: token.expect_span(),
896                },
897            ),
898        }
899        flatten_trivia(token.trailing_trivia().as_slice(), depth, output);
900    }
901    flatten_trivia(tokens.suffix_trivia(), depth, output);
902}
903
904/// Handles flatten trivia.
905fn flatten_trivia(trivia: &[crate::Trivia], depth: usize, output: &mut Vec<FlatToken>) {
906    for token in trivia {
907        push_flat(
908            output,
909            FlatToken {
910                depth,
911                kind: FlatKind::Trivia,
912                text: token.text().into(),
913                span: token.expect_span(),
914            },
915        );
916    }
917}
918
919/// Pushes flat.
920fn push_flat(output: &mut Vec<FlatToken>, token: FlatToken) {
921    if matches!(token.kind, FlatKind::Trivia)
922        && let Some(previous) = output.last_mut()
923        && matches!(previous.kind, FlatKind::Trivia)
924        && previous.depth == token.depth
925    {
926        previous.text.push_str(&token.text);
927        if let Some(span) = previous.span.join(token.span.clone()) {
928            previous.span = span;
929        } else {
930            output.push(token);
931        }
932    } else {
933        output.push(token);
934    }
935}
936
937/// Handles flat kind.
938fn flat_kind(kind: FlatKind) -> &'static str {
939    match kind {
940        FlatKind::Adj => "ADJ",
941        FlatKind::Open => "DELIM->",
942        FlatKind::Close => "<-DELIM",
943        FlatKind::Ident => "IDENT",
944        FlatKind::Literal => "LITERAL",
945        FlatKind::Punct => "PUNCT",
946        FlatKind::Trivia => "TRIVIA",
947    }
948}
949
950/// Handles token color.
951fn token_color(kind: FlatKind) -> &'static str {
952    match kind {
953        FlatKind::Adj => BRIGHT_BLACK,
954        FlatKind::Open => BLUE,
955        FlatKind::Close => MAGENTA,
956        FlatKind::Ident => CYAN,
957        FlatKind::Literal => YELLOW,
958        FlatKind::Punct => GREEN,
959        FlatKind::Trivia => RED,
960    }
961}
962
963/// Handles visible text.
964fn visible_text(text: &str) -> String {
965    let mut visible = String::new();
966    for character in text.chars() {
967        match character {
968            ' ' => visible.push_str("' '"),
969            '\t' => visible.push('⇥'),
970            '\n' => visible.push('↵'),
971            '\r' => visible.push('␍'),
972            character => visible.push(character),
973        }
974    }
975    visible
976}
977
978/// Writes tree.
979fn write_tree(output: &mut impl FmtWrite, root: &PrettyNode, options: PrettyOptions) -> FmtResult {
980    write_node(output, root, "", None, true, options)
981}
982
983/// Writes node.
984fn write_node(
985    output: &mut impl FmtWrite,
986    node: &PrettyNode,
987    prefix: &str,
988    label: Option<&str>,
989    last: bool,
990    options: PrettyOptions,
991) -> FmtResult {
992    let connector = if label.is_none() {
993        ""
994    } else if last {
995        "└─ "
996    } else {
997        "├─ "
998    };
999    write!(
1000        output,
1001        "{}{}",
1002        paint(DIM, prefix, options),
1003        paint(DIM, connector, options)
1004    )?;
1005    if let Some(detail) = &node.detail {
1006        write!(
1007            output,
1008            "{}  ",
1009            paint(
1010                detail_color(detail.role),
1011                &visible_text(&detail.text),
1012                options
1013            )
1014        )?;
1015    }
1016    if let Some(label) = label.filter(|label| !label.is_empty()) {
1017        write!(
1018            output,
1019            "{} {} ",
1020            paint(BRIGHT_YELLOW, label, options),
1021            paint(DIM, "→", options)
1022        )?;
1023    }
1024    write!(
1025        output,
1026        "{}",
1027        paint(node_color(node.role), &node.name, options)
1028    )?;
1029    if options.spans {
1030        write!(
1031            output,
1032            "  {}",
1033            paint(
1034                DIM,
1035                &node.span.as_ref().map_or_else(|| "@?".into(), format_span),
1036                options,
1037            )
1038        )?;
1039    }
1040    writeln!(output)?;
1041
1042    let child_prefix = if label.is_none() {
1043        String::new()
1044    } else {
1045        format!("{prefix}{}", if last { "   " } else { "│  " })
1046    };
1047    let child_count = node.children.len();
1048    for (index, child) in node.children.iter().enumerate() {
1049        write_node(
1050            output,
1051            &child.node,
1052            &child_prefix,
1053            Some(&child.label),
1054            index + 1 == child_count,
1055            options,
1056        )?;
1057    }
1058    Ok(())
1059}
1060
1061/// Handles node color.
1062fn node_color(role: PrettyNodeRole) -> &'static str {
1063    match role {
1064        PrettyNodeRole::Syntax => NON_TERMINAL_BLUE,
1065        PrettyNodeRole::Category => BRIGHT_YELLOW,
1066        PrettyNodeRole::Token => BOLD_CYAN,
1067        PrettyNodeRole::Delimiter => MAGENTA,
1068        PrettyNodeRole::Collection => BRIGHT_YELLOW,
1069        PrettyNodeRole::Absent => DIM,
1070    }
1071}
1072
1073/// Handles detail color.
1074fn detail_color(role: PrettyDetailRole) -> &'static str {
1075    match role {
1076        PrettyDetailRole::Text => WHITE,
1077        PrettyDetailRole::Glyph => BRIGHT_BLACK,
1078    }
1079}
1080
1081/// Formats span.
1082fn format_span(span: &Span) -> String {
1083    let Some(range) = span.line_column_range() else {
1084        return "@unlocated".into();
1085    };
1086    format!("@{range}")
1087}
1088
1089/// Handles paint.
1090fn paint(style: &'static str, text: &str, options: PrettyOptions) -> String {
1091    if options.color && !text.is_empty() {
1092        format!("{style}{text}{RESET}")
1093    } else {
1094        text.to_owned()
1095    }
1096}
1097
1098#[cfg(test)]
1099mod tests {
1100    use super::*;
1101    use crate::lex_str;
1102
1103    #[test]
1104    fn adjacent_trivia_is_hidden_or_rendered_as_one_token() {
1105        let source = "-- note";
1106        let tokens = lex_str(source).unwrap();
1107
1108        let hidden = format_tokens(&tokens, PrettyOptions::PLAIN);
1109        assert!(!hidden.contains("TRIVIA"));
1110        let shown = format_tokens(
1111            &tokens,
1112            PrettyOptions {
1113                trivia: true,
1114                ..PrettyOptions::PLAIN
1115            },
1116        );
1117        assert_eq!(shown.matches("TRIVIA").count(), 1);
1118    }
1119
1120    #[test]
1121    fn token_bands_use_distinct_class_colors() {
1122        let source = "name = 1\n(next) -- note";
1123        let tokens = lex_str(source).unwrap();
1124        let plain = format_tokens(&tokens, PrettyOptions::PLAIN);
1125
1126        assert!(plain.contains("token │"));
1127
1128        let colored = format_tokens(
1129            &tokens,
1130            PrettyOptions {
1131                trivia: true,
1132                ..PrettyOptions::ANSI
1133            },
1134        );
1135        for color in [RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN] {
1136            assert!(colored.contains(color));
1137        }
1138    }
1139
1140    #[test]
1141    fn raw_tokens_have_no_virtual_cell_boundaries() {
1142        let source = "first\nsecond";
1143        let tokens = lex_str(source).unwrap();
1144        let rendered = format_tokens(&tokens, PrettyOptions::PLAIN);
1145
1146        assert_eq!(rendered.matches("token │").count(), 1);
1147        assert!(!rendered.contains("EOC"));
1148        assert!(rendered.contains("second"));
1149    }
1150
1151    #[test]
1152    fn token_cells_are_centered() {
1153        assert_eq!(centered_cell("x", 5), "   x   ");
1154        assert_eq!(centered_cell("xy", 5), "  xy   ");
1155    }
1156
1157    #[test]
1158    fn enum_presentation_retains_the_dispatch_category() {
1159        let span = Span::for_text("x");
1160        let value = PrettyNode::token("Symbol", "x", span.clone());
1161        let rendered = PrettyNode::variant("Expr", "Symbol", Some(span), value);
1162
1163        assert_eq!(rendered.name, "Expr");
1164        assert_eq!(rendered.role, PrettyNodeRole::Category);
1165        assert_eq!(rendered.children.len(), 1);
1166        assert_eq!(rendered.children[0].node.name, "Symbol");
1167    }
1168
1169    #[test]
1170    fn tree_values_lead_their_field_and_distinguish_text_from_glyphs() {
1171        let source = Span::for_text(";");
1172        let mut root = PrettyNode::syntax("Root", Some(source.clone()));
1173        root.push_node(
1174            "semicolon",
1175            PrettyNode::token("Token![;]", ";", source.clone()),
1176        );
1177        root.push_node("operator", PrettyNode::token("Adj", "∅", source.at(1)));
1178
1179        let plain = format_tree(&root, PrettyOptions::PLAIN);
1180        assert!(plain.contains(";  semicolon → Token![;]"));
1181        assert!(plain.contains("∅  operator → Adj"));
1182
1183        let colored = format_tree(&root, PrettyOptions::ANSI);
1184        assert!(colored.contains(&format!("{WHITE};{RESET}")));
1185        assert!(colored.contains(&format!("{BRIGHT_BLACK}∅{RESET}")));
1186    }
1187
1188    #[test]
1189    fn delimiter_is_presented_as_a_collection() {
1190        let source = Span::for_text("()");
1191        let delimiter = PrettyNode::delimiter(
1192            DelimiterKind::Parenthesis,
1193            DoubleSpan::new(source.subspan(0, 1), source.subspan(1, 2)),
1194        );
1195
1196        assert_eq!(delimiter.role, PrettyNodeRole::Collection);
1197        assert_eq!(delimiter.name, "(…) · 2 tokens");
1198        assert!(delimiter.children.iter().all(|edge| {
1199            edge.node.name == "Delimiter![()]" && edge.node.role == PrettyNodeRole::Delimiter
1200        }));
1201    }
1202
1203    #[test]
1204    fn report_boxes_have_four_fixed_width_walls() {
1205        let mut rendered = String::new();
1206        let long_line = "x".repeat(PrettyOptions::PLAIN.width * 2);
1207        write_box(
1208            &mut rendered,
1209            "TEST",
1210            &format!("one\ntwo\n{long_line}"),
1211            PrettyOptions::PLAIN,
1212        )
1213        .unwrap();
1214
1215        for line in rendered.lines() {
1216            assert_eq!(line.chars().count(), PrettyOptions::PLAIN.width);
1217        }
1218        assert!(rendered.contains('…'));
1219        assert!(rendered.starts_with("╭─ TEST "));
1220        assert!(rendered.lines().next().unwrap().ends_with('╮'));
1221        assert!(rendered.lines().nth(1).unwrap().ends_with('│'));
1222        assert!(rendered.lines().last().unwrap().ends_with('╯'));
1223    }
1224}