1use std::fmt::{Display, Formatter, Result as FmtResult, Write as FmtWrite};
8use std::io::{self, IsTerminal as _, Write as IoWrite};
9
10use crate::{
11 DelimiterKind, DoubleSpan, Punctuated, Span, Spanned, TokenStream, TokenTree,
12 token_stream::delim::Delimiter,
13};
14
15const RESET: &str = "\u{1b}[0m";
16const BOLD_CYAN: &str = "\u{1b}[1;36m";
17const BRIGHT_YELLOW: &str = "\u{1b}[93m";
18const NON_TERMINAL_BLUE: &str = "\u{1b}[38;5;75m";
19const DIM: &str = "\u{1b}[2m";
20const IDENT_COLOR: &str = "\u{1b}[38;5;45m";
21const LITERAL_COLOR: &str = "\u{1b}[38;5;207m";
22const PUNCT_COLOR: &str = "\u{1b}[38;5;82m";
23const DELIM_COLOR: &str = "\u{1b}[38;5;75m";
24const TRIVIA_COLOR: &str = "\u{1b}[38;5;245m";
25
26pub trait PrettyTree {
34 fn pretty_tree(&self) -> PrettyNode;
36}
37
38#[doc(hidden)]
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct PrettyNode {
45 name: String,
46 detail: Option<String>,
47 span: Span,
48 role: NodeRole,
49 children: Vec<PrettyEdge>,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53enum NodeRole {
54 Syntax,
55 Token,
56 Collection,
57 Absent,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
61struct PrettyEdge {
62 label: String,
63 node: PrettyNode,
64}
65
66impl PrettyNode {
67 #[doc(hidden)]
69 pub fn syntax(name: impl Into<String>, span: Span) -> Self {
70 Self::new(name, span, NodeRole::Syntax)
71 }
72
73 #[doc(hidden)]
75 pub fn variant(_category: &str, _variant: &str, span: Span, mut value: PrettyNode) -> Self {
76 if span != Span::Detached {
77 value.span = span;
78 }
79 value
80 }
81
82 #[doc(hidden)]
84 pub fn delimiter(kind: DelimiterKind, spans: DoubleSpan) -> Self {
85 let delimiter = Delimiter::new(kind, spans);
86 let mut node = Self::new("Delimiter", delimiter.span(), NodeRole::Token);
87 node.push_node(
88 "opening",
89 Self::token("Token", kind.opening(), delimiter.opening_span()),
90 );
91 node.push_node(
92 "closing",
93 Self::token("Token", kind.closing(), delimiter.closing_span()),
94 );
95 node
96 }
97
98 #[doc(hidden)]
100 pub fn token(name: impl Into<String>, text: impl Into<String>, span: Span) -> Self {
101 let name = name.into();
102 let text = text.into();
103 if name == "Delimiter"
104 && let Some(kind) = delimiter_notation(&text)
105 {
106 return Self::delimiter(kind, DoubleSpan::new(span.clone(), span));
107 }
108 let mut node = Self::new(name, span, NodeRole::Token);
109 node.detail = Some(text);
110 node
111 }
112
113 #[doc(hidden)]
115 pub fn collection(name: impl Into<String>, span: Span) -> Self {
116 Self::new(name, span, NodeRole::Collection)
117 }
118
119 #[doc(hidden)]
121 pub fn absent() -> Self {
122 Self::new("None", Span::detached(), NodeRole::Absent)
123 }
124
125 fn new(name: impl Into<String>, span: Span, role: NodeRole) -> Self {
126 Self {
127 name: name.into(),
128 detail: None,
129 span,
130 role,
131 children: Vec::new(),
132 }
133 }
134
135 #[doc(hidden)]
137 pub fn push(&mut self, label: impl Into<String>, child: impl PrettyTree) {
138 self.push_node(label, child.pretty_tree());
139 }
140
141 #[doc(hidden)]
143 pub fn push_node(&mut self, label: impl Into<String>, node: PrettyNode) {
144 let mut node = node;
145 if node.span == Span::Detached {
146 node.span = self.span.clone();
147 }
148 self.children.push(PrettyEdge {
149 label: label.into(),
150 node,
151 });
152 }
153}
154
155fn delimiter_notation(notation: &str) -> Option<DelimiterKind> {
156 match notation {
157 "(…)" => Some(DelimiterKind::Parenthesis),
158 "[…]" => Some(DelimiterKind::Bracket),
159 "{…}" => Some(DelimiterKind::Brace),
160 "<|…|>" => Some(DelimiterKind::AngleBar),
161 _ => None,
162 }
163}
164
165impl<T: PrettyTree + ?Sized> PrettyTree for &T {
166 fn pretty_tree(&self) -> PrettyNode {
167 T::pretty_tree(self)
168 }
169}
170
171impl<T: PrettyTree> PrettyTree for Box<T> {
172 fn pretty_tree(&self) -> PrettyNode {
173 T::pretty_tree(self)
174 }
175}
176
177impl<T: PrettyTree + Spanned> PrettyTree for Option<T> {
178 fn pretty_tree(&self) -> PrettyNode {
179 match self {
180 Some(value) => value.pretty_tree(),
181 None => PrettyNode::absent(),
182 }
183 }
184}
185
186impl<T: PrettyTree + Spanned> PrettyTree for Vec<T> {
187 fn pretty_tree(&self) -> PrettyNode {
188 let mut node = PrettyNode::collection(item_count("List", self.len()), self.span());
189 for (index, value) in self.iter().enumerate() {
190 node.push(format!("[{index}]"), value);
191 }
192 node
193 }
194}
195
196impl<T, P> PrettyTree for Punctuated<T, P>
197where
198 T: PrettyTree + Spanned,
199 P: PrettyTree + Spanned,
200{
201 fn pretty_tree(&self) -> PrettyNode {
202 let mut node = PrettyNode::collection(item_count("Punctuated", self.len()), self.span());
203 node.push("[0]", self.first());
204 for (index, (punctuation, value)) in self.rest().iter().enumerate() {
205 node.push("punctuation", punctuation);
206 node.push(format!("[{}]", index + 1), value);
207 }
208 node
209 }
210}
211
212fn item_count(name: &str, count: usize) -> String {
213 let suffix = if count == 1 { "item" } else { "items" };
214 format!("{name} · {count} {suffix}")
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219pub struct PrettyOptions {
220 pub color: bool,
222 pub spans: bool,
224 pub trivia: bool,
226 pub width: usize,
228}
229
230impl PrettyOptions {
231 pub const PLAIN: Self = Self {
233 color: false,
234 spans: true,
235 trivia: false,
236 width: 150,
237 };
238
239 pub const ANSI: Self = Self {
241 color: true,
242 spans: true,
243 trivia: false,
244 width: 150,
245 };
246}
247
248impl Default for PrettyOptions {
249 fn default() -> Self {
250 Self::PLAIN
251 }
252}
253
254pub struct PrettyReport<'source> {
261 source: &'source str,
262 tokens: TokenStream,
263 cst: PrettyNode,
264 options: PrettyOptions,
265}
266
267impl<'source> PrettyReport<'source> {
268 #[doc(hidden)]
270 pub fn new(source: &'source str, tokens: TokenStream, cst: PrettyNode) -> Self {
271 Self {
272 source,
273 tokens,
274 cst,
275 options: PrettyOptions::PLAIN,
276 }
277 }
278
279 pub fn ansi(mut self, enabled: bool) -> Self {
281 self.options.color = enabled;
282 self
283 }
284
285 pub fn spans(mut self, enabled: bool) -> Self {
287 self.options.spans = enabled;
288 self
289 }
290
291 pub fn trivia(mut self, enabled: bool) -> Self {
293 self.options.trivia = enabled;
294 self
295 }
296
297 pub fn width(mut self, width: usize) -> Self {
299 self.options.width = width.max(40);
300 self
301 }
302}
303
304impl Display for PrettyReport<'_> {
305 fn fmt(&self, output: &mut Formatter<'_>) -> FmtResult {
306 section_title(output, "SOURCE", self.options)?;
307 write_source(output, self.source, self.options)?;
308 section_title(output, "TOKEN STREAM", self.options)?;
309 write_tokens(output, &self.tokens, self.options)?;
310 section_title(output, "TYPED CST", self.options)?;
311 write_tree(output, &self.cst, self.options)
312 }
313}
314
315pub(crate) fn print_report(report: PrettyReport<'_>) -> io::Result<()> {
316 let mut output = io::stdout().lock();
317 let width = std::env::var("COLUMNS")
318 .ok()
319 .and_then(|width| width.parse().ok())
320 .unwrap_or(120);
321 let report = report.ansi(output.is_terminal()).width(width);
322 write!(output, "{report}")
323}
324
325pub(crate) fn format_tree(node: &PrettyNode, options: PrettyOptions) -> String {
326 let mut output = String::new();
327 write_tree(&mut output, node, options).expect("writing to a string cannot fail");
328 output
329}
330
331pub fn format_pretty_tree(value: &impl PrettyTree) -> String {
333 format_tree(&value.pretty_tree(), PrettyOptions::PLAIN)
334}
335
336fn section_title(output: &mut impl FmtWrite, title: &str, options: PrettyOptions) -> FmtResult {
337 writeln!(
338 output,
339 "{}╭─ {} {}",
340 paint(DIM, "", options),
341 paint(BOLD_CYAN, title, options),
342 paint(
343 DIM,
344 &"─".repeat(options.width.saturating_sub(title.len() + 5)),
345 options
346 ),
347 )
348}
349
350fn write_source(output: &mut impl FmtWrite, source: &str, options: PrettyOptions) -> FmtResult {
351 let width = source.lines().count().max(1).to_string().len();
352 if source.is_empty() {
353 writeln!(output, "{}", paint(DIM, "│ ∅", options))?;
354 } else {
355 for (index, line) in source.lines().enumerate() {
356 writeln!(
357 output,
358 "{} {:>width$} {} {}",
359 paint(DIM, "│", options),
360 index + 1,
361 paint(DIM, "│", options),
362 line,
363 )?;
364 }
365 }
366 writeln!(output, "{}", paint(DIM, "╰─", options))
367}
368
369#[derive(Clone, Copy)]
370enum FlatKind {
371 Eoc,
372 Open,
373 Close,
374 Ident,
375 Literal,
376 Punct,
377 Trivia,
378}
379
380#[derive(Clone)]
381struct FlatToken {
382 depth: usize,
383 kind: FlatKind,
384 text: String,
385 span: Span,
386}
387
388pub(crate) fn format_tokens(tokens: &TokenStream, options: PrettyOptions) -> String {
389 let mut output = String::new();
390 write_tokens(&mut output, tokens, options).expect("writing to a string cannot fail");
391 output
392}
393
394pub fn format_pretty_tokens(tokens: &TokenStream) -> String {
396 format_tokens(tokens, PrettyOptions::PLAIN)
397}
398
399fn write_tokens(
400 output: &mut impl FmtWrite,
401 tokens: &TokenStream,
402 options: PrettyOptions,
403) -> FmtResult {
404 let mut flat = Vec::new();
405 flatten(tokens, 0, &mut flat);
406 write_flat(output, flat, options)
407}
408
409fn write_flat(
410 output: &mut impl FmtWrite,
411 mut flat: Vec<FlatToken>,
412 options: PrettyOptions,
413) -> FmtResult {
414 if !options.trivia {
415 flat.retain(|token| !matches!(token.kind, FlatKind::Trivia));
416 }
417 if flat.is_empty() {
418 return writeln!(output, "{}", paint(DIM, "∅", options));
419 }
420 let chunks = structural_chunks(&flat);
421 let available = options.width.saturating_sub(8).max(24);
422 let mut band = Vec::new();
423 let mut used = 0;
424 let mut wrote_band = false;
425 for chunk in chunks {
426 let chunk_width = token_band_width(chunk);
427 if !band.is_empty() && used + 1 + chunk_width > available {
428 if wrote_band {
429 writeln!(output)?;
430 }
431 write_token_band(output, &band, options)?;
432 wrote_band = true;
433 band.clear();
434 used = 0;
435 }
436 if !band.is_empty() {
437 used += 1;
438 }
439 used += chunk_width;
440 band.extend_from_slice(chunk);
441 }
442 if !band.is_empty() {
443 if wrote_band {
444 writeln!(output)?;
445 }
446 write_token_band(output, &band, options)?;
447 }
448 Ok(())
449}
450
451fn structural_chunks(tokens: &[FlatToken]) -> Vec<&[FlatToken]> {
452 let mut chunks = Vec::new();
453 let mut start = 0;
454 for (index, token) in tokens.iter().enumerate() {
455 if matches!(token.kind, FlatKind::Open | FlatKind::Close) {
456 chunks.push(&tokens[start..=index]);
457 start = index + 1;
458 }
459 }
460 if start < tokens.len() {
461 chunks.push(&tokens[start..]);
462 }
463 chunks
464}
465
466fn token_band_width(tokens: &[FlatToken]) -> usize {
467 tokens.iter().map(token_column_width).sum::<usize>() + tokens.len().saturating_sub(1)
468}
469
470fn token_column_width(token: &FlatToken) -> usize {
471 visible_text(&token.text)
472 .chars()
473 .count()
474 .max(flat_kind(token.kind).chars().count())
475 .clamp(3, 24)
476 + 2
477}
478
479fn write_token_band(
480 output: &mut impl FmtWrite,
481 tokens: &[FlatToken],
482 options: PrettyOptions,
483) -> FmtResult {
484 write!(
485 output,
486 "{} {} ",
487 paint(BRIGHT_YELLOW, "token", options),
488 paint(DIM, "│", options)
489 )?;
490 for (index, token) in tokens.iter().enumerate() {
491 if index != 0 {
492 write!(output, "{}", paint(DIM, "│", options))?;
493 }
494 let width = token_column_width(token) - 2;
495 let text = clipped(&visible_text(&token.text), width);
496 let cell = centered_cell(&text, width);
497 write!(output, "{}", paint(BRIGHT_YELLOW, &cell, options))?;
498 }
499 writeln!(output)?;
500
501 write!(
502 output,
503 "{} {} ",
504 paint(BRIGHT_YELLOW, " type", options),
505 paint(DIM, "│", options)
506 )?;
507 for (index, token) in tokens.iter().enumerate() {
508 if index != 0 {
509 write!(output, "{}", paint(DIM, "│", options))?;
510 }
511 let width = token_column_width(token) - 2;
512 let kind = clipped(flat_kind(token.kind), width);
513 let cell = centered_cell(&kind, width);
514 write!(output, "{}", paint(token_color(token.kind), &cell, options))?;
515 }
516 writeln!(output)
517}
518
519fn centered_cell(text: &str, width: usize) -> String {
520 let padding = width.saturating_sub(text.chars().count());
521 let left = padding / 2;
522 let right = padding - left;
523 format!(" {}{}{} ", " ".repeat(left), text, " ".repeat(right))
524}
525
526fn clipped(text: &str, width: usize) -> String {
527 let count = text.chars().count();
528 if count <= width {
529 return text.to_owned();
530 }
531 text.chars()
532 .take(width.saturating_sub(1))
533 .chain(std::iter::once('…'))
534 .collect()
535}
536
537fn flatten(tokens: &TokenStream, depth: usize, output: &mut Vec<FlatToken>) {
538 for token in tokens.iter() {
539 match token {
540 TokenTree::Eoc(token) => push_flat(
541 output,
542 FlatToken {
543 depth,
544 kind: FlatKind::Eoc,
545 text: "EOC".into(),
546 span: token.span(),
547 },
548 ),
549 TokenTree::Group(group) => {
550 let kind = group.delim_kind();
551 push_flat(
552 output,
553 FlatToken {
554 depth,
555 kind: FlatKind::Open,
556 text: kind.opening().into(),
557 span: group.delimiter().opening_span(),
558 },
559 );
560 flatten(group.stream(), depth + 1, output);
561 push_flat(
562 output,
563 FlatToken {
564 depth,
565 kind: FlatKind::Close,
566 text: kind.closing().into(),
567 span: group.delimiter().closing_span(),
568 },
569 );
570 }
571 TokenTree::Ident(token) => push_flat(
572 output,
573 FlatToken {
574 depth,
575 kind: FlatKind::Ident,
576 text: token.text().into(),
577 span: token.span(),
578 },
579 ),
580 TokenTree::Literal(token) => push_flat(
581 output,
582 FlatToken {
583 depth,
584 kind: FlatKind::Literal,
585 text: token.text().into(),
586 span: token.span(),
587 },
588 ),
589 TokenTree::Punct(token) => push_flat(
590 output,
591 FlatToken {
592 depth,
593 kind: FlatKind::Punct,
594 text: token.text().into(),
595 span: token.span(),
596 },
597 ),
598 TokenTree::Trivia(token) => push_flat(
599 output,
600 FlatToken {
601 depth,
602 kind: FlatKind::Trivia,
603 text: token.text().into(),
604 span: token.span(),
605 },
606 ),
607 }
608 }
609}
610
611fn push_flat(output: &mut Vec<FlatToken>, token: FlatToken) {
612 if matches!(token.kind, FlatKind::Trivia)
613 && let Some(previous) = output.last_mut()
614 && matches!(previous.kind, FlatKind::Trivia)
615 && previous.depth == token.depth
616 {
617 previous.text.push_str(&token.text);
618 previous.span = previous.span.join(token.span);
619 } else {
620 output.push(token);
621 }
622}
623
624fn flat_kind(kind: FlatKind) -> &'static str {
625 match kind {
626 FlatKind::Eoc => "EOC",
627 FlatKind::Open => "DELIM->",
628 FlatKind::Close => "<-DELIM",
629 FlatKind::Ident => "IDENT",
630 FlatKind::Literal => "LITERAL",
631 FlatKind::Punct => "PUNCT",
632 FlatKind::Trivia => "TRIVIA",
633 }
634}
635
636fn token_color(kind: FlatKind) -> &'static str {
637 match kind {
638 FlatKind::Eoc => PUNCT_COLOR,
639 FlatKind::Open | FlatKind::Close => DELIM_COLOR,
640 FlatKind::Ident => IDENT_COLOR,
641 FlatKind::Literal => LITERAL_COLOR,
642 FlatKind::Punct => PUNCT_COLOR,
643 FlatKind::Trivia => TRIVIA_COLOR,
644 }
645}
646
647fn visible_text(text: &str) -> String {
648 let mut visible = String::new();
649 for character in text.chars() {
650 match character {
651 ' ' => visible.push_str("' '"),
652 '\t' => visible.push('⇥'),
653 '\n' => visible.push('↵'),
654 '\r' => visible.push('␍'),
655 character => visible.push(character),
656 }
657 }
658 visible
659}
660
661fn write_tree(output: &mut impl FmtWrite, root: &PrettyNode, options: PrettyOptions) -> FmtResult {
662 write_node(output, root, "", None, true, options)
663}
664
665fn write_node(
666 output: &mut impl FmtWrite,
667 node: &PrettyNode,
668 prefix: &str,
669 label: Option<&str>,
670 last: bool,
671 options: PrettyOptions,
672) -> FmtResult {
673 let connector = if label.is_none() {
674 ""
675 } else if last {
676 "└─ "
677 } else {
678 "├─ "
679 };
680 write!(
681 output,
682 "{}{}",
683 paint(DIM, prefix, options),
684 paint(DIM, connector, options)
685 )?;
686 if let Some(label) = label.filter(|label| !label.is_empty()) {
687 write!(
688 output,
689 "{} {} ",
690 paint(BRIGHT_YELLOW, label, options),
691 paint(DIM, "→", options)
692 )?;
693 }
694 if let Some(detail) = &node.detail {
695 write!(
696 output,
697 "{} ",
698 paint(BRIGHT_YELLOW, &visible_text(detail), options)
699 )?;
700 }
701 write!(
702 output,
703 "{}",
704 paint(node_color(node.role), &node.name, options)
705 )?;
706 if options.spans {
707 write!(
708 output,
709 " {}",
710 paint(DIM, &format_span(&node.span), options)
711 )?;
712 }
713 writeln!(output)?;
714
715 let child_prefix = if label.is_none() {
716 String::new()
717 } else {
718 format!("{prefix}{}", if last { " " } else { "│ " })
719 };
720 let child_count = node.children.len();
721 for (index, child) in node.children.iter().enumerate() {
722 write_node(
723 output,
724 &child.node,
725 &child_prefix,
726 Some(&child.label),
727 index + 1 == child_count,
728 options,
729 )?;
730 }
731 Ok(())
732}
733
734fn node_color(role: NodeRole) -> &'static str {
735 match role {
736 NodeRole::Syntax => NON_TERMINAL_BLUE,
737 NodeRole::Token => BOLD_CYAN,
738 NodeRole::Collection => BRIGHT_YELLOW,
739 NodeRole::Absent => DIM,
740 }
741}
742
743fn format_span(span: &Span) -> String {
744 let Some((start, end)) = span.bounds() else {
745 return "@detached".into();
746 };
747 if start == end {
751 format!("@{start}")
752 } else {
753 format!("@{start}–{end}")
754 }
755}
756
757fn paint(style: &'static str, text: &str, options: PrettyOptions) -> String {
758 if options.color && !text.is_empty() {
759 format!("{style}{text}{RESET}")
760 } else {
761 text.to_owned()
762 }
763}
764
765#[cfg(test)]
766mod tests {
767 use super::*;
768 use crate::{Trivia, TriviaKind, lex_str};
769
770 #[test]
771 fn adjacent_trivia_is_hidden_or_rendered_as_one_token() {
772 let mut tokens = TokenStream::new();
773 tokens.extend([
774 Trivia::new(TriviaKind::Whitespace, " ", Span::detached()),
775 Trivia::new(TriviaKind::LineBreak, "\n", Span::detached()),
776 ]);
777
778 let hidden = format_tokens(&tokens, PrettyOptions::PLAIN);
779 assert!(!hidden.contains("TRIVIA"));
780 let shown = format_tokens(
781 &tokens,
782 PrettyOptions {
783 trivia: true,
784 ..PrettyOptions::PLAIN
785 },
786 );
787 assert_eq!(shown.matches("TRIVIA").count(), 1);
788 assert!(shown.contains("' '"));
789 }
790
791 #[test]
792 fn token_bands_use_distinct_class_colors() {
793 let tokens = lex_str("name = 1\nnext").unwrap();
794 let plain = format_tokens(&tokens, PrettyOptions::PLAIN);
795
796 assert!(plain.contains("token │"));
797
798 let colored = format_tokens(&tokens, PrettyOptions::ANSI);
799 assert!(colored.contains(&format!("{BRIGHT_YELLOW} name ")));
800 for color in [IDENT_COLOR, PUNCT_COLOR, LITERAL_COLOR] {
801 assert!(colored.contains(color));
802 }
803 }
804
805 #[test]
806 fn token_cells_are_centered() {
807 assert_eq!(centered_cell("x", 5), " x ");
808 assert_eq!(centered_cell("xy", 5), " xy ");
809 }
810
811 #[test]
812 fn enum_presentation_is_transparent() {
813 let value = PrettyNode::token("Symbol", "x", Span::detached());
814 let rendered = PrettyNode::variant("Expr", "Symbol", Span::detached(), value);
815
816 assert_eq!(rendered.name, "Symbol");
817 }
818}