1use std::fmt::{Display, Formatter, Result as FmtResult, Write as FmtWrite};
8use std::io::{self, IsTerminal as _, Write as IoWrite};
9
10use crate::{
11 CommentTrivia, DelimiterKind, DoubleSpan, Punctuated, Span, Spanned, TokenStream, TokenTree,
12 Trivia, TriviaKind, 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 BRIGHT_BLACK: &str = "\u{1b}[90m";
19const RED: &str = "\u{1b}[31m";
20const GREEN: &str = "\u{1b}[32m";
21const YELLOW: &str = "\u{1b}[33m";
22const BLUE: &str = "\u{1b}[34m";
23const MAGENTA: &str = "\u{1b}[35m";
24const CYAN: &str = "\u{1b}[36m";
25const WHITE: &str = "\u{1b}[37m";
26const NON_TERMINAL_BLUE: &str = BLUE;
27const DIM: &str = "\u{1b}[2m";
28
29pub trait PrettyTree {
37 fn pretty_tree(&self) -> PrettyNode;
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct PrettyNode {
49 name: String,
50 detail: Option<PrettyDetail>,
51 span: Option<Span>,
52 role: PrettyNodeRole,
53 children: Vec<PrettyEdge>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct PrettyDetail {
62 text: String,
63 role: PrettyDetailRole,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum PrettyDetailRole {
72 Text,
74 Glyph,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum PrettyNodeRole {
85 Syntax,
87 Token,
89 Collection,
91 Absent,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct PrettyEdge {
101 label: String,
102 node: PrettyNode,
103}
104
105impl PrettyDetail {
106 pub fn text(&self) -> &str {
108 &self.text
109 }
110
111 pub fn role(&self) -> PrettyDetailRole {
113 self.role
114 }
115}
116
117impl PrettyEdge {
118 pub fn label(&self) -> &str {
120 &self.label
121 }
122
123 pub fn node(&self) -> &PrettyNode {
125 &self.node
126 }
127}
128
129impl PrettyNode {
130 pub fn name(&self) -> &str {
132 &self.name
133 }
134
135 pub fn detail(&self) -> Option<&PrettyDetail> {
137 self.detail.as_ref()
138 }
139
140 pub fn span(&self) -> Option<&Span> {
142 self.span.as_ref()
143 }
144
145 pub fn role(&self) -> PrettyNodeRole {
147 self.role
148 }
149
150 pub fn children(&self) -> &[PrettyEdge] {
152 &self.children
153 }
154
155 #[doc(hidden)]
157 pub fn syntax(name: impl Into<String>, span: Option<Span>) -> Self {
158 Self::new(name, span, PrettyNodeRole::Syntax)
159 }
160
161 #[doc(hidden)]
163 pub fn variant(
164 _category: &str,
165 _variant: &str,
166 span: Option<Span>,
167 mut value: PrettyNode,
168 ) -> Self {
169 value.span = span;
170 value
171 }
172
173 #[doc(hidden)]
175 pub fn delimiter(kind: DelimiterKind, spans: DoubleSpan) -> Self {
176 let delimiter = Delimiter::new(kind, spans);
177 let mut node = Self::collection(
178 format!("{}…{} · 2 tokens", kind.opening(), kind.closing()),
179 delimiter.try_span(),
180 );
181 node.push_node(
182 "opening",
183 Self::token("Token", kind.opening(), delimiter.opening_span()),
184 );
185 if let Some(closing) = delimiter.closing_span() {
186 node.push_node("closing", Self::token("Token", kind.closing(), closing));
187 }
188 node
189 }
190
191 #[doc(hidden)]
193 pub fn token(name: impl Into<String>, text: impl Into<String>, span: Span) -> Self {
194 let name = name.into();
195 let text = text.into();
196 if name == "Delimiter"
197 && let Some(kind) = delimiter_notation(&text)
198 {
199 return Self::delimiter(kind, DoubleSpan::new(span.clone(), span));
200 }
201 let role = if matches!(text.as_str(), "∅" | "EOC" | "EOF") {
202 PrettyDetailRole::Glyph
203 } else {
204 PrettyDetailRole::Text
205 };
206 let mut node = Self::new(name, Some(span), PrettyNodeRole::Token);
207 node.detail = Some(PrettyDetail { text, role });
208 node
209 }
210
211 #[doc(hidden)]
213 pub fn collection(name: impl Into<String>, span: Option<Span>) -> Self {
214 Self::new(name, span, PrettyNodeRole::Collection)
215 }
216
217 #[doc(hidden)]
219 pub fn absent() -> Self {
220 Self::new("None", None, PrettyNodeRole::Absent)
221 }
222
223 fn new(name: impl Into<String>, span: Option<Span>, role: PrettyNodeRole) -> Self {
224 Self {
225 name: name.into(),
226 detail: None,
227 span,
228 role,
229 children: Vec::new(),
230 }
231 }
232
233 #[doc(hidden)]
235 pub fn push(&mut self, label: impl Into<String>, child: impl PrettyTree) {
236 self.push_node(label, child.pretty_tree());
237 }
238
239 #[doc(hidden)]
241 pub fn push_node(&mut self, label: impl Into<String>, node: PrettyNode) {
242 let mut node = node;
243 if node.span.is_none() {
244 node.span.clone_from(&self.span);
245 }
246 self.children.push(PrettyEdge {
247 label: label.into(),
248 node,
249 });
250 }
251}
252
253fn delimiter_notation(notation: &str) -> Option<DelimiterKind> {
254 match notation {
255 "(…)" => Some(DelimiterKind::Parenthesis),
256 "[…]" => Some(DelimiterKind::Bracket),
257 "{…}" => Some(DelimiterKind::Brace),
258 "<|…|>" => Some(DelimiterKind::AngleBar),
259 _ => None,
260 }
261}
262
263impl<T: PrettyTree + ?Sized> PrettyTree for &T {
264 fn pretty_tree(&self) -> PrettyNode {
265 T::pretty_tree(self)
266 }
267}
268
269impl<T: PrettyTree> PrettyTree for Box<T> {
270 fn pretty_tree(&self) -> PrettyNode {
271 T::pretty_tree(self)
272 }
273}
274
275impl<T: PrettyTree + Spanned> PrettyTree for Option<T> {
276 fn pretty_tree(&self) -> PrettyNode {
277 match self {
278 Some(value) => value.pretty_tree(),
279 None => PrettyNode::absent(),
280 }
281 }
282}
283
284impl<T: PrettyTree + Spanned> PrettyTree for Vec<T> {
285 fn pretty_tree(&self) -> PrettyNode {
286 let span = Span::join_all(self.iter().map(Spanned::span));
287 let mut node = PrettyNode::collection(item_count("List", self.len()), span);
288 for (index, value) in self.iter().enumerate() {
289 node.push(format!("[{index}]"), value);
290 }
291 node
292 }
293}
294
295impl PrettyTree for Trivia {
296 fn pretty_tree(&self) -> PrettyNode {
297 let name = match self.kind() {
298 TriviaKind::LineComment => "LineCommentBlock",
299 TriviaKind::BlockComment => "BlockComment",
300 };
301 PrettyNode::token(name, self.text(), self.span())
302 }
303}
304
305impl PrettyTree for CommentTrivia {
306 fn pretty_tree(&self) -> PrettyNode {
307 let count = self.outer().len() + self.inner().len();
308 let mut node = PrettyNode::collection(item_count("Comments", count), self.try_span());
309 for (index, comment) in self.outer().iter().enumerate() {
310 node.push(format!("outer[{index}]"), comment);
311 }
312 for (index, comment) in self.inner().iter().enumerate() {
313 node.push(format!("inner[{index}]"), comment);
314 }
315 node
316 }
317}
318
319impl<T, P> PrettyTree for Punctuated<T, P>
320where
321 T: PrettyTree + Spanned,
322 P: PrettyTree + Spanned,
323{
324 fn pretty_tree(&self) -> PrettyNode {
325 let mut node =
326 PrettyNode::collection(item_count("Punctuated", self.len()), self.try_span());
327 node.push("[0]", self.first());
328 for (index, (punctuation, value)) in self.rest().iter().enumerate() {
329 node.push("punctuation", punctuation);
330 node.push(format!("[{}]", index + 1), value);
331 }
332 node
333 }
334}
335
336fn item_count(name: &str, count: usize) -> String {
337 let suffix = if count == 1 { "item" } else { "items" };
338 format!("{name} · {count} {suffix}")
339}
340
341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
343pub struct PrettyOptions {
344 pub color: bool,
346 pub spans: bool,
348 pub trivia: bool,
350 pub width: usize,
352}
353
354impl PrettyOptions {
355 pub const PLAIN: Self = Self {
357 color: false,
358 spans: true,
359 trivia: false,
360 width: 150,
361 };
362
363 pub const ANSI: Self = Self {
365 color: true,
366 spans: true,
367 trivia: false,
368 width: 150,
369 };
370}
371
372impl Default for PrettyOptions {
373 fn default() -> Self {
374 Self::PLAIN
375 }
376}
377
378pub struct PrettyReport<'source> {
385 source: &'source str,
386 tokens: TokenStream,
387 cst: PrettyNode,
388 options: PrettyOptions,
389}
390
391impl<'source> PrettyReport<'source> {
392 #[doc(hidden)]
394 pub fn new(source: &'source str, tokens: TokenStream, cst: PrettyNode) -> Self {
395 Self {
396 source,
397 tokens,
398 cst,
399 options: PrettyOptions::PLAIN,
400 }
401 }
402
403 pub fn ansi(mut self, enabled: bool) -> Self {
405 self.options.color = enabled;
406 self
407 }
408
409 pub fn spans(mut self, enabled: bool) -> Self {
411 self.options.spans = enabled;
412 self
413 }
414
415 pub fn trivia(mut self, enabled: bool) -> Self {
417 self.options.trivia = enabled;
418 self
419 }
420
421 pub fn width(mut self, width: usize) -> Self {
423 self.options.width = width.max(40);
424 self
425 }
426}
427
428impl Display for PrettyReport<'_> {
429 fn fmt(&self, output: &mut Formatter<'_>) -> FmtResult {
430 let mut source = String::new();
431 write_source(&mut source, self.source)?;
432 write_box(output, "SOURCE", &source, self.options)?;
433 writeln!(output)?;
434
435 let mut tokens = String::new();
436 write_tokens(&mut tokens, &self.tokens, self.options)?;
437 write_box(output, "TOKEN STREAM", &tokens, self.options)?;
438 writeln!(output)?;
439
440 let mut cst = String::new();
441 write_tree(&mut cst, &self.cst, self.options)?;
442 write_box(output, "TYPED CST", &cst, self.options)
443 }
444}
445
446pub(crate) fn print_report(report: PrettyReport<'_>) -> io::Result<()> {
447 let mut output = io::stdout().lock();
448 let width = std::env::var("COLUMNS")
449 .ok()
450 .and_then(|width| width.parse().ok())
451 .unwrap_or(120);
452 let report = report.ansi(output.is_terminal()).width(width);
453 write!(output, "{report}")
454}
455
456pub(crate) fn format_tree(node: &PrettyNode, options: PrettyOptions) -> String {
457 let mut output = String::new();
458 write_tree(&mut output, node, options).expect("writing to a string cannot fail");
459 output
460}
461
462pub fn format_pretty_tree(value: &impl PrettyTree) -> String {
464 format_tree(&value.pretty_tree(), PrettyOptions::PLAIN)
465}
466
467fn write_box(
468 output: &mut impl FmtWrite,
469 title: &str,
470 contents: &str,
471 options: PrettyOptions,
472) -> FmtResult {
473 let inner_width = options.width.saturating_sub(4);
474 writeln!(
475 output,
476 "{}{}{}{}",
477 paint(DIM, "╭─ ", options),
478 paint(BOLD_CYAN, title, options),
479 paint(
480 DIM,
481 &format!(
482 " {}",
483 "─".repeat(options.width.saturating_sub(title.chars().count() + 5))
484 ),
485 options,
486 ),
487 paint(DIM, "╮", options),
488 )?;
489 for line in contents.lines() {
490 let line = clip_ansi(line, inner_width);
491 let line_width = ansi_width(&line);
492 writeln!(
493 output,
494 "{} {}{} {}",
495 paint(DIM, "│", options),
496 line,
497 " ".repeat(inner_width.saturating_sub(line_width)),
498 paint(DIM, "│", options),
499 )?;
500 }
501 writeln!(
502 output,
503 "{}{}{}",
504 paint(DIM, "╰", options),
505 paint(DIM, &"─".repeat(options.width.saturating_sub(2)), options),
506 paint(DIM, "╯", options),
507 )
508}
509
510fn ansi_width(text: &str) -> usize {
511 let mut characters = text.chars().peekable();
512 let mut width = 0;
513 while let Some(character) = characters.next() {
514 if character == '\u{1b}' && characters.peek() == Some(&'[') {
515 for escaped in characters.by_ref() {
516 if escaped == 'm' {
517 break;
518 }
519 }
520 } else {
521 width += 1;
522 }
523 }
524 width
525}
526
527fn clip_ansi(text: &str, width: usize) -> String {
528 if ansi_width(text) <= width {
529 return text.to_owned();
530 }
531
532 let limit = width.saturating_sub(1);
533 let mut output = String::new();
534 let mut characters = text.chars().peekable();
535 let mut visible = 0;
536 let mut styled = false;
537 while let Some(character) = characters.next() {
538 if character == '\u{1b}' && characters.peek() == Some(&'[') {
539 styled = true;
540 output.push(character);
541 for escaped in characters.by_ref() {
542 output.push(escaped);
543 if escaped == 'm' {
544 break;
545 }
546 }
547 } else if visible < limit {
548 output.push(character);
549 visible += 1;
550 } else {
551 break;
552 }
553 }
554 output.push('…');
555 if styled {
556 output.push_str(RESET);
557 }
558 output
559}
560
561fn write_source(output: &mut impl FmtWrite, source: &str) -> FmtResult {
562 let width = source.lines().count().max(1).to_string().len();
563 if source.is_empty() {
564 writeln!(output, "∅")?;
565 } else {
566 for (index, line) in source.lines().enumerate() {
567 writeln!(output, "{:>width$} │ {}", index + 1, line)?;
568 }
569 }
570 Ok(())
571}
572
573#[derive(Clone, Copy)]
574enum FlatKind {
575 Adj,
576 Open,
577 Close,
578 Ident,
579 Literal,
580 Punct,
581 Trivia,
582}
583
584#[derive(Clone)]
585struct FlatToken {
586 depth: usize,
587 kind: FlatKind,
588 text: String,
589 span: Span,
590}
591
592pub(crate) fn format_tokens(tokens: &TokenStream, options: PrettyOptions) -> String {
593 let mut output = String::new();
594 write_tokens(&mut output, tokens, options).expect("writing to a string cannot fail");
595 output
596}
597
598pub fn format_pretty_tokens(tokens: &TokenStream) -> String {
600 format_tokens(tokens, PrettyOptions::PLAIN)
601}
602
603fn write_tokens(
604 output: &mut impl FmtWrite,
605 tokens: &TokenStream,
606 options: PrettyOptions,
607) -> FmtResult {
608 let mut flat = Vec::new();
609 flatten(tokens, 0, &mut flat);
610 write_flat(output, flat, options)
611}
612
613fn write_flat(
614 output: &mut impl FmtWrite,
615 mut flat: Vec<FlatToken>,
616 options: PrettyOptions,
617) -> FmtResult {
618 if !options.trivia {
619 flat.retain(|token| !matches!(token.kind, FlatKind::Trivia));
620 }
621 if flat.is_empty() {
622 return writeln!(output, "{}", paint(DIM, "∅", options));
623 }
624 let available = options.width.saturating_sub(12).max(24);
625 let mut band = Vec::new();
626 let mut used = 0;
627 let mut wrote_band = false;
628 for chunk in flat.iter().map(std::slice::from_ref) {
629 let chunk_width = token_band_width(chunk);
630 if !band.is_empty() && used + 1 + chunk_width > available {
631 if wrote_band {
632 writeln!(output)?;
633 }
634 write_token_band(output, &band, options)?;
635 wrote_band = true;
636 band.clear();
637 used = 0;
638 }
639 if !band.is_empty() {
640 used += 1;
641 }
642 used += chunk_width;
643 band.extend_from_slice(chunk);
644 }
645 if !band.is_empty() {
646 if wrote_band {
647 writeln!(output)?;
648 }
649 write_token_band(output, &band, options)?;
650 }
651 Ok(())
652}
653
654fn token_band_width(tokens: &[FlatToken]) -> usize {
655 tokens.iter().map(token_column_width).sum::<usize>() + tokens.len().saturating_sub(1)
656}
657
658fn token_column_width(token: &FlatToken) -> usize {
659 visible_text(&token.text)
660 .chars()
661 .count()
662 .max(flat_kind(token.kind).chars().count())
663 .clamp(3, 24)
664 + 2
665}
666
667fn write_token_band(
668 output: &mut impl FmtWrite,
669 tokens: &[FlatToken],
670 options: PrettyOptions,
671) -> FmtResult {
672 write!(
673 output,
674 "{} {} ",
675 paint(BRIGHT_YELLOW, "token", options),
676 paint(DIM, "│", options)
677 )?;
678 for (index, token) in tokens.iter().enumerate() {
679 if index != 0 {
680 write!(output, "{}", paint(DIM, "│", options))?;
681 }
682 let width = token_column_width(token) - 2;
683 let text = clipped(&visible_text(&token.text), width);
684 let cell = centered_cell(&text, width);
685 write!(output, "{}", paint(token_color(token.kind), &cell, options))?;
686 }
687 writeln!(output)?;
688
689 write!(
690 output,
691 "{} {} ",
692 paint(BRIGHT_YELLOW, " type", options),
693 paint(DIM, "│", options)
694 )?;
695 for (index, token) in tokens.iter().enumerate() {
696 if index != 0 {
697 write!(output, "{}", paint(DIM, "│", options))?;
698 }
699 let width = token_column_width(token) - 2;
700 let kind = clipped(flat_kind(token.kind), width);
701 let cell = centered_cell(&kind, width);
702 write!(output, "{}", paint(token_color(token.kind), &cell, options))?;
703 }
704 writeln!(output)
705}
706
707fn centered_cell(text: &str, width: usize) -> String {
708 let padding = width.saturating_sub(text.chars().count());
709 let left = padding / 2;
710 let right = padding - left;
711 format!(" {}{}{} ", " ".repeat(left), text, " ".repeat(right))
712}
713
714fn clipped(text: &str, width: usize) -> String {
715 let count = text.chars().count();
716 if count <= width {
717 return text.to_owned();
718 }
719 text.chars()
720 .take(width.saturating_sub(1))
721 .chain(std::iter::once('…'))
722 .collect()
723}
724
725fn flatten(tokens: &TokenStream, depth: usize, output: &mut Vec<FlatToken>) {
726 flatten_trivia(tokens.prefix_trivia(), depth, output);
727 for token in tokens.iter() {
728 flatten_trivia(token.leading_trivia().as_slice(), depth, output);
729 match token {
730 TokenTree::Group(group) => {
731 let kind = group.delim_kind();
732 push_flat(
733 output,
734 FlatToken {
735 depth,
736 kind: FlatKind::Open,
737 text: kind.opening().into(),
738 span: group.delimiter().opening_span(),
739 },
740 );
741 flatten(group.stream(), depth + 1, output);
742 if let Some(closing) = group.delimiter().closing_span() {
743 push_flat(
744 output,
745 FlatToken {
746 depth,
747 kind: FlatKind::Close,
748 text: kind.closing().into(),
749 span: closing,
750 },
751 );
752 }
753 }
754 TokenTree::Ident(token) => push_flat(
755 output,
756 FlatToken {
757 depth,
758 kind: FlatKind::Ident,
759 text: token.text().into(),
760 span: token.expect_span(),
761 },
762 ),
763 TokenTree::Literal(token) => push_flat(
764 output,
765 FlatToken {
766 depth,
767 kind: FlatKind::Literal,
768 text: token.text().into(),
769 span: token.expect_span(),
770 },
771 ),
772 TokenTree::Punct(token) => push_flat(
773 output,
774 FlatToken {
775 depth,
776 kind: if token.text().is_empty() {
777 FlatKind::Adj
778 } else {
779 FlatKind::Punct
780 },
781 text: if token.text().is_empty() {
782 "Adj".into()
783 } else {
784 token.text().into()
785 },
786 span: token.expect_span(),
787 },
788 ),
789 }
790 flatten_trivia(token.trailing_trivia().as_slice(), depth, output);
791 }
792 flatten_trivia(tokens.suffix_trivia(), depth, output);
793}
794
795fn flatten_trivia(trivia: &[crate::Trivia], depth: usize, output: &mut Vec<FlatToken>) {
796 for token in trivia {
797 push_flat(
798 output,
799 FlatToken {
800 depth,
801 kind: FlatKind::Trivia,
802 text: token.text().into(),
803 span: token.expect_span(),
804 },
805 );
806 }
807}
808
809fn push_flat(output: &mut Vec<FlatToken>, token: FlatToken) {
810 if matches!(token.kind, FlatKind::Trivia)
811 && let Some(previous) = output.last_mut()
812 && matches!(previous.kind, FlatKind::Trivia)
813 && previous.depth == token.depth
814 {
815 previous.text.push_str(&token.text);
816 if let Some(span) = previous.span.join(token.span.clone()) {
817 previous.span = span;
818 } else {
819 output.push(token);
820 }
821 } else {
822 output.push(token);
823 }
824}
825
826fn flat_kind(kind: FlatKind) -> &'static str {
827 match kind {
828 FlatKind::Adj => "ADJ",
829 FlatKind::Open => "DELIM->",
830 FlatKind::Close => "<-DELIM",
831 FlatKind::Ident => "IDENT",
832 FlatKind::Literal => "LITERAL",
833 FlatKind::Punct => "PUNCT",
834 FlatKind::Trivia => "TRIVIA",
835 }
836}
837
838fn token_color(kind: FlatKind) -> &'static str {
839 match kind {
840 FlatKind::Adj => BRIGHT_BLACK,
841 FlatKind::Open => BLUE,
842 FlatKind::Close => MAGENTA,
843 FlatKind::Ident => CYAN,
844 FlatKind::Literal => YELLOW,
845 FlatKind::Punct => GREEN,
846 FlatKind::Trivia => RED,
847 }
848}
849
850fn visible_text(text: &str) -> String {
851 let mut visible = String::new();
852 for character in text.chars() {
853 match character {
854 ' ' => visible.push_str("' '"),
855 '\t' => visible.push('⇥'),
856 '\n' => visible.push('↵'),
857 '\r' => visible.push('␍'),
858 character => visible.push(character),
859 }
860 }
861 visible
862}
863
864fn write_tree(output: &mut impl FmtWrite, root: &PrettyNode, options: PrettyOptions) -> FmtResult {
865 write_node(output, root, "", None, true, options)
866}
867
868fn write_node(
869 output: &mut impl FmtWrite,
870 node: &PrettyNode,
871 prefix: &str,
872 label: Option<&str>,
873 last: bool,
874 options: PrettyOptions,
875) -> FmtResult {
876 let connector = if label.is_none() {
877 ""
878 } else if last {
879 "└─ "
880 } else {
881 "├─ "
882 };
883 write!(
884 output,
885 "{}{}",
886 paint(DIM, prefix, options),
887 paint(DIM, connector, options)
888 )?;
889 if let Some(detail) = &node.detail {
890 write!(
891 output,
892 "{} ",
893 paint(
894 detail_color(detail.role),
895 &visible_text(&detail.text),
896 options
897 )
898 )?;
899 }
900 if let Some(label) = label.filter(|label| !label.is_empty()) {
901 write!(
902 output,
903 "{} {} ",
904 paint(BRIGHT_YELLOW, label, options),
905 paint(DIM, "→", options)
906 )?;
907 }
908 write!(
909 output,
910 "{}",
911 paint(node_color(node.role), &node.name, options)
912 )?;
913 if options.spans {
914 write!(
915 output,
916 " {}",
917 paint(
918 DIM,
919 &node.span.as_ref().map_or_else(|| "@?".into(), format_span),
920 options,
921 )
922 )?;
923 }
924 writeln!(output)?;
925
926 let child_prefix = if label.is_none() {
927 String::new()
928 } else {
929 format!("{prefix}{}", if last { " " } else { "│ " })
930 };
931 let child_count = node.children.len();
932 for (index, child) in node.children.iter().enumerate() {
933 write_node(
934 output,
935 &child.node,
936 &child_prefix,
937 Some(&child.label),
938 index + 1 == child_count,
939 options,
940 )?;
941 }
942 Ok(())
943}
944
945fn node_color(role: PrettyNodeRole) -> &'static str {
946 match role {
947 PrettyNodeRole::Syntax => NON_TERMINAL_BLUE,
948 PrettyNodeRole::Token => BOLD_CYAN,
949 PrettyNodeRole::Collection => BRIGHT_YELLOW,
950 PrettyNodeRole::Absent => DIM,
951 }
952}
953
954fn detail_color(role: PrettyDetailRole) -> &'static str {
955 match role {
956 PrettyDetailRole::Text => WHITE,
957 PrettyDetailRole::Glyph => BRIGHT_BLACK,
958 }
959}
960
961fn format_span(span: &Span) -> String {
962 let Some(range) = span.line_column_range() else {
963 return "@unlocated".into();
964 };
965 format!("@{range}")
966}
967
968fn paint(style: &'static str, text: &str, options: PrettyOptions) -> String {
969 if options.color && !text.is_empty() {
970 format!("{style}{text}{RESET}")
971 } else {
972 text.to_owned()
973 }
974}
975
976#[cfg(test)]
977mod tests {
978 use super::*;
979 use crate::lex_str;
980
981 #[test]
982 fn adjacent_trivia_is_hidden_or_rendered_as_one_token() {
983 let source = "-- note";
984 let tokens = lex_str(source).unwrap();
985
986 let hidden = format_tokens(&tokens, PrettyOptions::PLAIN);
987 assert!(!hidden.contains("TRIVIA"));
988 let shown = format_tokens(
989 &tokens,
990 PrettyOptions {
991 trivia: true,
992 ..PrettyOptions::PLAIN
993 },
994 );
995 assert_eq!(shown.matches("TRIVIA").count(), 1);
996 }
997
998 #[test]
999 fn token_bands_use_distinct_class_colors() {
1000 let source = "name = 1\n(next) -- note";
1001 let tokens = lex_str(source).unwrap();
1002 let plain = format_tokens(&tokens, PrettyOptions::PLAIN);
1003
1004 assert!(plain.contains("token │"));
1005
1006 let colored = format_tokens(
1007 &tokens,
1008 PrettyOptions {
1009 trivia: true,
1010 ..PrettyOptions::ANSI
1011 },
1012 );
1013 for color in [RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN] {
1014 assert!(colored.contains(color));
1015 }
1016 }
1017
1018 #[test]
1019 fn raw_tokens_have_no_virtual_cell_boundaries() {
1020 let source = "first\nsecond";
1021 let tokens = lex_str(source).unwrap();
1022 let rendered = format_tokens(&tokens, PrettyOptions::PLAIN);
1023
1024 assert_eq!(rendered.matches("token │").count(), 1);
1025 assert!(!rendered.contains("EOC"));
1026 assert!(rendered.contains("second"));
1027 }
1028
1029 #[test]
1030 fn token_cells_are_centered() {
1031 assert_eq!(centered_cell("x", 5), " x ");
1032 assert_eq!(centered_cell("xy", 5), " xy ");
1033 }
1034
1035 #[test]
1036 fn enum_presentation_is_transparent() {
1037 let span = Span::for_text("x");
1038 let value = PrettyNode::token("Symbol", "x", span.clone());
1039 let rendered = PrettyNode::variant("Expr", "Symbol", Some(span), value);
1040
1041 assert_eq!(rendered.name, "Symbol");
1042 }
1043
1044 #[test]
1045 fn tree_values_lead_their_field_and_distinguish_text_from_glyphs() {
1046 let source = Span::for_text(";");
1047 let mut root = PrettyNode::syntax("Root", Some(source.clone()));
1048 root.push_node(
1049 "semicolon",
1050 PrettyNode::token("Token![;]", ";", source.clone()),
1051 );
1052 root.push_node("operator", PrettyNode::token("Adj", "∅", source.at(1)));
1053
1054 let plain = format_tree(&root, PrettyOptions::PLAIN);
1055 assert!(plain.contains("; semicolon → Token![;]"));
1056 assert!(plain.contains("∅ operator → Adj"));
1057
1058 let colored = format_tree(&root, PrettyOptions::ANSI);
1059 assert!(colored.contains(&format!("{WHITE};{RESET}")));
1060 assert!(colored.contains(&format!("{BRIGHT_BLACK}∅{RESET}")));
1061 }
1062
1063 #[test]
1064 fn delimiter_is_presented_as_a_collection() {
1065 let source = Span::for_text("()");
1066 let delimiter = PrettyNode::delimiter(
1067 DelimiterKind::Parenthesis,
1068 DoubleSpan::new(source.subspan(0, 1), source.subspan(1, 2)),
1069 );
1070
1071 assert_eq!(delimiter.role, PrettyNodeRole::Collection);
1072 assert_eq!(delimiter.name, "(…) · 2 tokens");
1073 assert!(!format_tree(&delimiter, PrettyOptions::PLAIN).contains("Delimiter"));
1074 }
1075
1076 #[test]
1077 fn report_boxes_have_four_fixed_width_walls() {
1078 let mut rendered = String::new();
1079 let long_line = "x".repeat(PrettyOptions::PLAIN.width * 2);
1080 write_box(
1081 &mut rendered,
1082 "TEST",
1083 &format!("one\ntwo\n{long_line}"),
1084 PrettyOptions::PLAIN,
1085 )
1086 .unwrap();
1087
1088 for line in rendered.lines() {
1089 assert_eq!(line.chars().count(), PrettyOptions::PLAIN.width);
1090 }
1091 assert!(rendered.contains('…'));
1092 assert!(rendered.starts_with("╭─ TEST "));
1093 assert!(rendered.lines().next().unwrap().ends_with('╮'));
1094 assert!(rendered.lines().nth(1).unwrap().ends_with('│'));
1095 assert!(rendered.lines().last().unwrap().ends_with('╯'));
1096 }
1097}