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