1use 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
15const RESET: &str = "\u{1b}[0m";
17const BOLD_CYAN: &str = "\u{1b}[1;36m";
19const BRIGHT_YELLOW: &str = "\u{1b}[93m";
21const BRIGHT_BLACK: &str = "\u{1b}[90m";
23const RED: &str = "\u{1b}[31m";
25const GREEN: &str = "\u{1b}[32m";
27const YELLOW: &str = "\u{1b}[33m";
29const BLUE: &str = "\u{1b}[34m";
31const MAGENTA: &str = "\u{1b}[35m";
33const CYAN: &str = "\u{1b}[36m";
35const WHITE: &str = "\u{1b}[37m";
37const NON_TERMINAL_BLUE: &str = BLUE;
39const DIM: &str = "\u{1b}[2m";
41
42pub trait PrettyTree {
51 fn pretty_tree(&self) -> PrettyNode;
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct PrettyNode {
63 name: String,
65 detail: Option<PrettyDetail>,
67 span: Option<Span>,
69 role: PrettyNodeRole,
71 transparent: bool,
73 children: Vec<PrettyEdge>,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct PrettyDetail {
83 text: String,
85 role: PrettyDetailRole,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum PrettyDetailRole {
95 Text,
97 Glyph,
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum PrettyNodeRole {
109 Syntax,
111 Category,
113 Token,
115 Delimiter,
117 Collection,
119 Absent,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct PrettyEdge {
129 label: String,
131 node: PrettyNode,
133}
134
135impl PrettyDetail {
136 pub fn text(&self) -> &str {
138 &self.text
139 }
140
141 pub fn role(&self) -> PrettyDetailRole {
143 self.role
144 }
145}
146
147impl PrettyEdge {
148 pub fn label(&self) -> &str {
150 &self.label
151 }
152
153 pub fn node(&self) -> &PrettyNode {
155 &self.node
156 }
157}
158
159impl PrettyNode {
160 pub fn name(&self) -> &str {
162 &self.name
163 }
164
165 pub fn detail(&self) -> Option<&PrettyDetail> {
167 self.detail.as_ref()
168 }
169
170 pub fn span(&self) -> Option<&Span> {
172 self.span.as_ref()
173 }
174
175 pub fn role(&self) -> PrettyNodeRole {
177 self.role
178 }
179
180 pub fn children(&self) -> &[PrettyEdge] {
182 &self.children
183 }
184
185 #[doc(hidden)]
187 pub fn syntax(name: impl Into<String>, span: Option<Span>) -> Self {
188 Self::new(name, span, PrettyNodeRole::Syntax)
189 }
190
191 #[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 #[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 #[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 #[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" | "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 #[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 #[doc(hidden)]
269 pub fn group(name: impl Into<String>, span: Option<Span>) -> Self {
270 Self::new(name, span, PrettyNodeRole::Collection)
271 }
272
273 #[doc(hidden)]
275 pub fn absent() -> Self {
276 Self::new("None", None, PrettyNodeRole::Absent)
277 }
278
279 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 #[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 #[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
320fn 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
404fn item_count(name: &str, count: usize) -> String {
406 let suffix = if count == 1 { "item" } else { "items" };
407 format!("{name} · {count} {suffix}")
408}
409
410#[derive(Debug, Clone, Copy, PartialEq, Eq)]
412pub struct PrettyOptions {
413 pub color: bool,
415 pub spans: bool,
417 pub trivia: bool,
419 pub width: usize,
421}
422
423impl PrettyOptions {
424 pub const PLAIN: Self = Self {
426 color: false,
427 spans: true,
428 trivia: false,
429 width: 150,
430 };
431
432 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
447pub struct PrettyReport<'source> {
454 source: &'source str,
456 tokens: TokenStream,
458 cst: PrettyNode,
460 options: PrettyOptions,
462}
463
464impl<'source> PrettyReport<'source> {
465 #[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 pub fn ansi(mut self, enabled: bool) -> Self {
478 self.options.color = enabled;
479 self
480 }
481
482 pub fn spans(mut self, enabled: bool) -> Self {
484 self.options.spans = enabled;
485 self
486 }
487
488 pub fn trivia(mut self, enabled: bool) -> Self {
490 self.options.trivia = enabled;
491 self
492 }
493
494 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
519pub(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
530pub(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
537pub fn format_pretty_tree(value: &impl PrettyTree) -> String {
539 format_tree(&value.pretty_tree(), PrettyOptions::PLAIN)
540}
541
542fn 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
586fn 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
604fn 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
639fn 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)]
653enum FlatKind {
655 Adj,
657 Open,
659 Close,
661 Ident,
663 Literal,
665 Punct,
667 Trivia,
669}
670
671#[derive(Clone)]
672struct FlatToken {
674 depth: usize,
676 kind: FlatKind,
678 text: String,
680 span: Span,
682}
683
684pub(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
691pub fn format_pretty_tokens(tokens: &TokenStream) -> String {
693 format_tokens(tokens, PrettyOptions::PLAIN)
694}
695
696pub fn format_pretty_cell_tokens(tokens: &CellTokenStream) -> String {
698 format_tokens(
699 &tokens.clone().into_parser_token_stream(),
700 PrettyOptions::PLAIN,
701 )
702}
703
704fn 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
715fn 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
757fn token_band_width(tokens: &[FlatToken]) -> usize {
759 tokens.iter().map(token_column_width).sum::<usize>() + tokens.len().saturating_sub(1)
760}
761
762fn 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
772fn 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
813fn 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
821fn 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
833fn 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
904fn 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
919fn 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
937fn 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
950fn 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
963fn 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
978fn write_tree(output: &mut impl FmtWrite, root: &PrettyNode, options: PrettyOptions) -> FmtResult {
980 write_node(output, root, "", None, true, options)
981}
982
983fn 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
1061fn 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
1073fn detail_color(role: PrettyDetailRole) -> &'static str {
1075 match role {
1076 PrettyDetailRole::Text => WHITE,
1077 PrettyDetailRole::Glyph => BRIGHT_BLACK,
1078 }
1079}
1080
1081fn format_span(span: &Span) -> String {
1083 let Some(range) = span.line_column_range() else {
1084 return "@unlocated".into();
1085 };
1086 format!("@{range}")
1087}
1088
1089fn 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}