Skip to main content

codesnake/
lib.rs

1#![no_std]
2#![forbid(unsafe_code)]
3#![warn(missing_docs)]
4//! Pretty printer for non-overlapping code spans.
5//!
6//! This crate aids you in creating output like the following,
7//! both for the terminal (ANSI) as well as for the web (HTML):
8//!
9//! <!-- colors taken from https://en.wikipedia.org/wiki/Solarized -->
10//! <style>
11//! pre span.red   { color: #dc322f; }
12//! pre span.green { color: #859900; }
13//! pre span.blue  { color: #268bd2; }
14//! pre span.yellow{ color: #b58900; }
15//! </style>
16//! <pre style="background-color:#002b36; color:#93a1a1; line-height:1.0; font-size:large;">
17//!   ╭─<span class=red>[fac.lisp]</span>
18//!   │
19//! 1 │   (<span class=red>defun</span> <span class=green>factorial</span> (n) <span class=blue>(if (zerop n) 1</span>
20//!   ┆          <span class=green>────┬────</span>     <span class=blue>▲</span>
21//!   ┆          <span class=green>    │    </span>     <span class=blue>│</span>
22//!   ┆              <span class=green>╰─────────────────────────</span> this function ...
23//!   ┆ <span class=blue>╭──────────────────────╯</span>
24//! 2 │ <span class=blue>│</span> <span class=blue>        (* n (factorial (1- n))))</span>)<span class=yellow></span>
25//!   ┆ <span class=blue>│</span>                                 <span class=blue>▲</span> <span class=yellow>┬</span>
26//!   ┆ <span class=blue>│</span>                                 <span class=blue>│</span> <span class=yellow>│</span>
27//!   ┆ <span class=blue>╰─────────────────────────────────┴───</span> ... is defined by this
28//!   ┆                                     <span class=yellow>│</span>
29//!   ┆                                     <span class=yellow>╰─</span> (and here is EOF)
30//! ──╯
31//! </pre>
32//!
33//! This example has been created with `cargo run --example example -- --html`.
34//! To see its console output, run `cargo run --example example`.
35//!
36//! # Usage
37//!
38//! Suppose that we have a source file and a list of byte ranges that we want to annotate.
39//! For example:
40//!
41//! ~~~
42//! let src = r#"if true { 42 } else { "42" }"#;
43//! let labels = [
44//!     (8..14, "this is of type Nat"),
45//!     (20..28, "this is of type String"),
46//! ];
47//! ~~~
48//!
49//! First, we have to create a [`LineIndex`].
50//! This splits the source into lines, so that further functions can
51//! quickly find in which line a byte is situated.
52//!
53//! ~~~
54//! use codesnake::LineIndex;
55//! # let src = r#"if true { 42 } else { "42" }"#;
56//! let idx = LineIndex::new(src);
57//! ~~~
58//!
59//! Next, we create a code [`Block`] from our index and the [`Label`]s:
60//!
61//! ~~~
62//! use codesnake::{Block, Label};
63//! # use codesnake::LineIndex;
64//! # let src = r#"if true { 42 } else { "42" }"#;
65//! # let idx = LineIndex::new(src);
66//! # let labels = [(8..14, "this is of type Nat")];
67//! let block = Block::new(&idx, labels.map(|(range, text)| Label::<_, _, ()>::new(range).with_text(text))).unwrap();
68//! ~~~
69//!
70//! This will fail if your labels refer to bytes outside the range of your source.
71//!
72//! Finally, we can print our code block:
73//!
74//! ~~~
75//! use codesnake::CodeWidth;
76//! # use codesnake::{Block, Label};
77//! # use codesnake::LineIndex;
78//! # let src = r#"if true { 42 } else { "42" }"#;
79//! # let idx = LineIndex::new(src);
80//! # let labels = [(8..14, "this is of type Nat")];
81//! # let block = Block::new(&idx, labels.map(|(range, text)| Label::<_, _, ()>::new(range).with_text(text))).unwrap();
82//! let block = block.map_code(|c| CodeWidth::new(c, c.len()));
83//! // yield "  ╭─[main.rs]"
84//! println!("{}{}", block.prologue(), "[main.rs]");
85//! print!("{block}");
86//! // yield "──╯"
87//! println!("{}", block.epilogue());
88//! ~~~
89//!
90//! # Colors
91//!
92//! To color the output on a terminal, you can use a crate like [`yansi`](https://docs.rs/yansi).
93//! This allows you to color labels as follows:
94//!
95//! ~~~
96//! use codesnake::{Block, CodeWidth, Label, LineIndex};
97//! use yansi::{Color, Paint};
98//! # let src = r#"if true { 42 } else { "42" }"#;
99//! # let idx = LineIndex::new(src);
100//! # let range = 8..14;
101//! let label = Label::<_, &str, _>::new(range).with_style(Color::Red);
102//! # let block = Block::new(&idx, [label]).unwrap().map_code(|c| CodeWidth::new(c, c.len()));
103//! let block = block.with_paint(|f, style, code| {
104//!     if *style == Color::default() {
105//!         write!(f, "{code}")
106//!     } else {
107//!         write!(f, "{}", code.fg(*style))
108//!     }
109//! });
110//! assert_eq!(block.to_string(), "1 │ if true \u{1b}[31m{ 42 }\u{1b}[0m else { \"42\" }\n");
111//! ~~~
112//!
113//! For HTML, you can use something like:
114//!
115//! ~~~
116//! use codesnake::{Block, CodeWidth, Label, LineIndex};
117//! # let src = r#"if true { 42 } else { "42" }"#;
118//! # let idx = LineIndex::new(src);
119//! # let range = 8..14;
120//! let label = Label::<_, &str, _>::new(range).with_style("color:red");
121//! # let block = Block::new(&idx, [label]).unwrap().map_code(|c| CodeWidth::new(c, c.len()));
122//! let block = block.with_paint(|f, style, code| {
123//!     if style.is_empty() {
124//!         write!(f, "{code}")
125//!     } else {
126//!         write!(f, "<span style=\"{style}\">{code}</span>")
127//!     }
128//! });
129//! assert_eq!(block.to_string(), "1 │ if true <span style=\"color:red\">{ 42 }</span> else { \"42\" }\n");
130//! ~~~
131
132extern crate alloc;
133
134use alloc::vec::Vec;
135use core::fmt::{self, Display, Formatter};
136use core::ops::Range;
137
138/// Associate byte offsets with line numbers.
139///
140/// If `idx = LineIndex::new(s)` and `idx.0[n] = (offset, line)`, then
141/// the `n`-th line of `s` starts at `offset` in `s` and equals `line`.
142pub struct LineIndex<'a>(Vec<(usize, &'a str)>);
143
144impl<'a> LineIndex<'a> {
145    /// Create a new index.
146    #[must_use]
147    pub fn new(s: &'a str) -> Self {
148        // indices of '\n' characters
149        let newlines: Vec<_> = s
150            .char_indices()
151            .filter_map(|(i, c)| (c == '\n').then_some(i))
152            .collect();
153        // indices of line starts and ends
154        let starts = core::iter::once(0).chain(newlines.iter().map(|i| *i + 1));
155        let ends = newlines.iter().copied().chain(core::iter::once(s.len()));
156
157        let lines = starts.zip(ends).map(|(start, end)| (start, &s[start..end]));
158        Self(lines.collect())
159    }
160
161    fn get(&self, offset: usize) -> Option<IndexEntry<'_>> {
162        use core::cmp::Ordering;
163        let line_no = self.0.binary_search_by(|(line_start, line)| {
164            if *line_start > offset {
165                Ordering::Greater
166            } else if line_start + line.len() < offset {
167                Ordering::Less
168            } else {
169                Ordering::Equal
170            }
171        });
172        let line_no = line_no.ok()?;
173        let (line_start, line) = self.0[line_no];
174        Some(IndexEntry {
175            line_no,
176            line,
177            bytes: offset - line_start,
178        })
179    }
180}
181
182struct IndexEntry<'a> {
183    line: &'a str,
184    line_no: usize,
185    /// offset of position relative to start of line
186    bytes: usize,
187}
188
189/// Functions that determine what to print below labels.
190struct Fns {
191    /// display line for label without text
192    snake: Option<fn(&mut Formatter, usize) -> fmt::Result>,
193    /// display line for label with text
194    text: fn(&mut Formatter, usize, usize) -> fmt::Result,
195}
196
197impl<T> LabelKind<T> {
198    fn has_snake(&self) -> bool {
199        match self {
200            Self::None => false,
201            Self::Snake | Self::Text(_) => true,
202        }
203    }
204
205    fn text(self) -> Option<T> {
206        match self {
207            Self::None | Self::Snake => None,
208            Self::Text(t) => Some(t),
209        }
210    }
211}
212
213/// Code label with text and style.
214pub struct Label<C, T, S = ()> {
215    code: C,
216    kind: LabelKind<T>,
217    style: S,
218}
219
220impl<T, S: Default> Label<Range<usize>, T, S> {
221    /// Create a new label.
222    ///
223    /// If the range start equals the range end,
224    /// an arrow is drawn at the range start.
225    /// This can be useful to indicate errors that occur at the end of the input.
226    #[must_use]
227    pub fn new(code: Range<usize>) -> Self {
228        Self {
229            code,
230            kind: LabelKind::None,
231            style: S::default(),
232        }
233    }
234}
235
236impl<C, T, S> Label<C, T, S> {
237    /// Create a snake label with text.
238    #[must_use]
239    pub fn with_text(self, text: T) -> Self {
240        let kind = LabelKind::Text(text);
241        Self { kind, ..self }
242    }
243
244    /// Create a snake label without text.
245    #[must_use]
246    pub fn with_snake(self) -> Self {
247        let kind = LabelKind::Snake;
248        Self { kind, ..self }
249    }
250
251    /// Use a custom style for drawing the label's snake.
252    #[must_use]
253    pub fn with_style(self, style: S) -> Self {
254        Self { style, ..self }
255    }
256}
257
258/// Piece of code together with its display width.
259pub struct CodeWidth<C> {
260    code: C,
261    width: usize,
262}
263
264impl<C> CodeWidth<C> {
265    /// Create a new piece of code with associated display width.
266    pub fn new(code: C, width: usize) -> Self {
267        CodeWidth { code, width }
268    }
269
270    /// Width to the left and right of the center (excluding the center itself).
271    fn left_right(&self) -> (usize, usize) {
272        let left = self.width / 2;
273        let right = self.width.saturating_sub(left + 1);
274        (left, right)
275    }
276}
277
278impl<C: Display> Display for CodeWidth<C> {
279    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
280        self.code.fmt(f)
281    }
282}
283
284type Paint<S> = fn(&mut Formatter, &S, &dyn Display) -> fmt::Result;
285
286fn styled<'a, S>(paint: Paint<S>, style: &'a S, x: &'a impl Display) -> impl Display + 'a {
287    from_fn(move |f| paint(f, style, x))
288}
289
290struct FromFn<F>(F);
291
292fn from_fn<F: Fn(&mut Formatter) -> fmt::Result>(f: F) -> FromFn<F> {
293    FromFn(f)
294}
295
296impl<F: Fn(&mut Formatter) -> fmt::Result> Display for FromFn<F> {
297    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
298        (self.0)(f)
299    }
300}
301
302enum LabelKind<T> {
303    None,
304    Snake,
305    Text(T),
306}
307
308/// Sequence of lines, containing code `C`, (label) text `T`, and style `S`.
309pub struct Block<C, T, S> {
310    lines: Vec<Option<Line<C, T, S>>>,
311    paint: Paint<S>,
312}
313
314struct Line<C, T, S> {
315    no: usize,
316    parts: LineParts<C, T, S>,
317}
318
319impl<C, T, S> Line<C, T, S> {
320    fn map_code<C1>(self, f: impl FnMut(C) -> C1) -> Line<C1, T, S> {
321        Line {
322            no: self.no,
323            parts: self.parts.map_code(f),
324        }
325    }
326}
327
328/// Line parts, containing code `C`, (label) text `T`, and style `S`.
329struct LineParts<C, T, S> {
330    /// snake that comes from another line, potentially with text
331    incoming: Option<(C, Option<T>, S)>,
332    inside: Vec<(C, LabelKind<T>, S)>,
333    /// snake that starts on current line and extends to another line
334    outgoing: Option<(C, S)>,
335}
336
337impl<C, T, S> LineParts<C, T, S> {
338    fn arrows_below(&self) -> bool {
339        let inside = |(_code, label, _style): &_| LabelKind::has_snake(label);
340        self.incoming.is_some() || self.outgoing.is_some() || self.inside.iter().any(inside)
341    }
342}
343
344impl<C, T, S> Default for LineParts<C, T, S> {
345    fn default() -> Self {
346        Self {
347            incoming: None,
348            inside: Vec::new(),
349            outgoing: None,
350        }
351    }
352}
353
354impl<'a, T, S: Default + Clone> Block<&'a str, T, S> {
355    /// Create a new block.
356    ///
357    /// Given a sequence of labels, find all input lines that are touched by the labels.
358    ///
359    /// The label ranges `r` must fulfill the following conditions:
360    ///
361    /// * `r.start <= r.end`.
362    /// * If the length of the string that was used to construct `idx` is `len`, then
363    ///   `r.start <= len` and `r.end <= len`.
364    /// * For any two subsequent labels with ranges `r1` and `r2`,
365    ///   `r1.start < r2.start` and `r1.end <= r2.start`.
366    ///
367    /// If any of these conditions is not fulfilled, this function returns `None`.
368    pub fn new<I>(idx: &'a LineIndex, labels: I) -> Option<Self>
369    where
370        I: IntoIterator<Item = Label<Range<usize>, T, S>>,
371    {
372        let mut prev_range: Option<Range<_>> = None;
373        let mut lines = Vec::new();
374        for Label { kind, code, style } in labels {
375            if code.start > code.end {
376                return None;
377            }
378            if let Some(prev) = prev_range.replace(code.clone()) {
379                if code.start <= prev.start || code.start < prev.end {
380                    return None;
381                }
382            }
383            let start = idx.get(code.start)?;
384            let end = idx.get(code.end)?;
385            debug_assert!(start.line_no <= end.line_no);
386
387            let mut parts = match lines.pop() {
388                Some(Some((line_no, _line, parts))) if line_no == start.line_no => parts,
389                Some(line) => {
390                    let non_consecutive = line
391                        .as_ref()
392                        .filter(|(no, ..)| start.line_no > no + 1)
393                        .is_some();
394
395                    lines.push(line);
396                    if non_consecutive {
397                        lines.push(None);
398                    }
399                    LineParts::default()
400                }
401                None => LineParts::default(),
402            };
403
404            if start.line_no == end.line_no {
405                parts.inside.push((start.bytes..end.bytes, kind, style));
406                lines.push(Some((start.line_no, start.line, parts)));
407            } else {
408                let range = start.bytes..start.line.len();
409                if kind.has_snake() {
410                    parts.outgoing = Some((range, style.clone()));
411                } else {
412                    parts.inside.push((range, LabelKind::None, style.clone()));
413                }
414                lines.push(Some((start.line_no, start.line, parts)));
415
416                for line_no in start.line_no + 1..end.line_no {
417                    let line = idx.0[line_no].1;
418                    let parts = LineParts {
419                        inside: Vec::from([(0..line.len(), LabelKind::None, style.clone())]),
420                        ..Default::default()
421                    };
422                    lines.push(Some((line_no, line, parts)));
423                }
424
425                let mut parts = LineParts::default();
426                let range = 0..end.bytes;
427                if kind.has_snake() {
428                    parts.incoming = Some((range, kind.text(), style.clone()));
429                } else {
430                    parts.inside.push((range, LabelKind::None, style.clone()));
431                }
432                lines.push(Some((end.line_no, end.line, parts)));
433            }
434        }
435
436        let lines = lines.into_iter().map(|line| {
437            line.map(|(no, line, parts)| Line {
438                no,
439                parts: parts.segment(line),
440            })
441        });
442        Some(Block {
443            lines: lines.collect(),
444            paint: |f, _, s| write!(f, "{s}"),
445        })
446    }
447}
448
449impl<C, T, S> Block<C, T, S> {
450    /// Apply function to code.
451    #[must_use]
452    pub fn map_code<C1>(self, mut f: impl FnMut(C) -> C1) -> Block<C1, T, S> {
453        let f = |line: Option<Line<C, T, S>>| line.map(|line| line.map_code(&mut f));
454        Block {
455            lines: self.lines.into_iter().map(f).collect(),
456            paint: self.paint,
457        }
458    }
459
460    fn some_incoming(&self) -> bool {
461        let mut lines = self.lines.iter().flatten();
462        lines.any(|line| line.parts.incoming.is_some())
463    }
464
465    fn line_no_width(&self) -> usize {
466        let max = self.lines.iter().flatten().next_back().unwrap().no + 1;
467        // number of digits; taken from https://stackoverflow.com/a/69302957
468        core::iter::successors(Some(max), |&n| (n >= 10).then_some(n / 10)).count()
469    }
470
471    /// Line that precedes the block, i.e. " ... ╭─".
472    #[must_use]
473    pub fn prologue(&self) -> impl Display {
474        let space = space(self.line_no_width());
475        from_fn(move |f| write!(f, "{space} {}{}", Snake::UpRight, Snake::Horizontal))
476    }
477
478    /// Line number space followed by a vertical snake, i.e. " ... |".
479    ///
480    /// This is useful after the prologue, to make things less cramped.
481    #[must_use]
482    pub fn space_vert(&self) -> impl Display {
483        let space = space(self.line_no_width());
484        from_fn(move |f| write!(f, "{space} {}", Snake::Vertical))
485    }
486
487    /// Line that succeeds the block, i.e. "─...─╯".
488    #[must_use]
489    pub fn epilogue(&self) -> impl Display {
490        Snake::line_up(self.line_no_width() + 1)
491    }
492}
493
494impl<C, T, S> Block<C, T, S> {
495    /// Format styled code/snakes.
496    ///
497    /// By default, code/snakes are printed without considering their style.
498    #[must_use]
499    pub fn with_paint(self, paint: Paint<S>) -> Self {
500        Self { paint, ..self }
501    }
502}
503
504fn space(width: usize) -> impl Display {
505    from_fn(move |f| write!(f, "{:width$}", ""))
506}
507
508macro_rules! width {
509    ($slice:expr) => {
510        $slice.iter().map(|(code, ..)| code.width).sum::<usize>()
511    };
512}
513
514impl<C: Display, T: Display, S> Display for Block<CodeWidth<C>, T, S> {
515    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
516        let paint = self.paint;
517        let mut incoming_style: Option<&S> = None;
518
519        let line_no_width = self.line_no_width();
520        let line_no_space = space(line_no_width);
521        // " ...  ┆"
522        let dots = from_fn(move |f| write!(f, "{line_no_space} {}", Snake::VerticalDots));
523
524        let incoming_space = if self.some_incoming() { "  " } else { "" };
525
526        for line in &self.lines {
527            let Line { no: line_no, parts } = if let Some(line) = line {
528                line
529            } else {
530                writeln!(f, "{dots}")?;
531                continue;
532            };
533
534            // write line number right-aligned
535            write!(f, "{:>line_no_width$} │", line_no + 1)?;
536
537            if let Some(style) = incoming_style {
538                write!(f, " {}", styled(paint, style, &Snake::Vertical))?;
539            } else {
540                incoming_space.fmt(f)?;
541            }
542
543            write!(f, " ")?;
544            parts.code_parts().try_for_each(|(c, s)| paint(f, s, c))?;
545            writeln!(f)?;
546
547            // print the line just below the code, e.g.
548            // " ...  ┆ │ ... ─┬─ ... ─┬─ ... ▲"
549            if parts.arrows_below() {
550                write!(f, "{dots} ")?;
551                if let Some(style) = incoming_style {
552                    styled(paint, style, &Snake::Vertical).fmt(f)?;
553                    parts.incoming(paint, Snake::ArrowUp).fmt(f)?;
554                } else {
555                    incoming_space.fmt(f)?;
556                }
557                writeln!(f, "{}", parts.arrows(paint))?;
558            }
559
560            if parts.incoming.is_some() {
561                assert!(incoming_style.take().is_some());
562                parts.incoming_text(&dots, paint).fmt(f)?;
563            }
564
565            let incoming_width = width!(parts.incoming);
566            let prefix = from_fn(|f| write!(f, "{dots}{incoming_space} {}", space(incoming_width)));
567            parts.inside_text(&prefix, paint).fmt(f)?;
568
569            // " ...  ┆ ╭─...─╯"
570            if let Some((_, style)) = &parts.outgoing {
571                let snake = Snake::up_line_up(incoming_width + width!(parts.inside) + 1);
572                writeln!(f, "{dots} {}", styled(paint, style, &snake))?;
573
574                incoming_style = Some(style);
575            }
576        }
577        Ok(())
578    }
579}
580
581impl<C: Display, T, S> LineParts<C, T, S> {
582    fn code_parts(&self) -> impl Iterator<Item = (&C, &S)> {
583        let inside = self.inside.iter().map(|(code, _label, styl)| (code, styl));
584        let incoming = self.incoming.iter().map(|(code, _text, styl)| (code, styl));
585        let outgoing = self.outgoing.iter().map(|(code, styl)| (code, styl));
586        incoming.chain(inside).chain(outgoing)
587    }
588}
589
590impl<T, S: Default> LineParts<Range<usize>, T, S> {
591    fn segment(self, line: &str) -> LineParts<&str, T, S> {
592        let len = line.len();
593        let start = self.incoming.as_ref().map_or(0, |(code, ..)| code.end);
594        let end = self.outgoing.as_ref().map_or(len, |(code, _)| code.start);
595        let last = self.inside.last().map_or(start, |(code, ..)| code.end);
596
597        let mut pos = start;
598        let unlabelled =
599            |start, end| (start < end).then(|| (&line[start..end], LabelKind::None, S::default()));
600        let inside = self.inside.into_iter().flat_map(|(code, label, style)| {
601            let unlabelled = unlabelled(pos, code.start);
602            let labelled = (&line[code.start..code.end], label, style);
603            pos = code.end;
604            unlabelled.into_iter().chain([labelled])
605        });
606        LineParts {
607            incoming: self
608                .incoming
609                .map(|(code, text, sty)| (&line[..code.end], text, sty)),
610            inside: inside.chain(unlabelled(last, end)).collect(),
611            outgoing: self.outgoing.map(|(code, sty)| (&line[code.start..], sty)),
612        }
613    }
614}
615
616impl<C, T, S> LineParts<C, T, S> {
617    #[must_use]
618    fn map_code<C1>(self, mut f: impl FnMut(C) -> C1) -> LineParts<C1, T, S> {
619        let inside = self.inside.into_iter();
620        LineParts {
621            incoming: self.incoming.map(|(code, txt, sty)| (f(code), txt, sty)),
622            inside: inside.map(|(code, lbl, sty)| (f(code), lbl, sty)).collect(),
623            outgoing: self.outgoing.map(|(code, style)| (f(code), style)),
624        }
625    }
626}
627
628impl<C, T, S> LineParts<CodeWidth<C>, T, S> {
629    /// Position of the end of the rightmost label.
630    fn width(&self) -> usize {
631        width!(self.inside) + width!(self.incoming) + width!(self.outgoing)
632    }
633
634    fn incoming<'a>(&'a self, paint: Paint<S>, snake: Snake) -> impl Display + 'a {
635        from_fn(move |f| {
636            if let Some((code, _text, style)) = &self.incoming {
637                space(code.width).fmt(f)?;
638                paint(f, style, &snake)?;
639            }
640            Ok(())
641        })
642    }
643
644    fn outgoing<'a>(&'a self, paint: Paint<S>, snake: Snake) -> impl Display + 'a {
645        from_fn(move |f| {
646            if let Some((_code, style)) = &self.outgoing {
647                paint(f, style, &snake)?;
648            }
649            Ok(())
650        })
651    }
652
653    fn inside<'a>(&'a self, paint: Paint<S>, from: usize, fns: &'a Fns) -> impl Display + 'a {
654        from_fn(move |f| {
655            space(width!(self.inside[..from])).fmt(f)?;
656
657            for (code, label, style) in &self.inside[from..] {
658                match label {
659                    LabelKind::Text(_text) => {
660                        let (left, right) = code.left_right();
661                        paint(f, style, &from_fn(|f| (fns.text)(f, left, right)))
662                    }
663                    LabelKind::Snake => match fns.snake {
664                        Some(snake) => paint(f, style, &from_fn(|f| (snake)(f, code.width))),
665                        None => space(code.width).fmt(f),
666                    },
667                    LabelKind::None => space(code.width).fmt(f),
668                }?;
669            }
670            Ok(())
671        })
672    }
673
674    /// Print something like "... ─┬─ ... ─┬─ ... ▲".
675    fn arrows(&self, paint: Paint<S>) -> impl Display + '_ {
676        let fns = Fns {
677            snake: Some(|f, w| Snake::line(w).fmt(f)),
678            text: |f, l, r| Snake::line_down_line(l, r).fmt(f),
679        };
680        let outgoing = self.outgoing(paint, Snake::ArrowUp);
681        from_fn(move |f| write!(f, "{}{outgoing}", self.inside(paint, 0, &fns)))
682    }
683
684    /// Print something like "... │ ...  │"
685    fn inside_vert(&self, paint: Paint<S>, from: usize) -> impl Display + '_ {
686        let fns = Fns {
687            snake: None,
688            text: |f, l, r| write!(f, "{}{}{}", space(l), Snake::Vertical, space(r)),
689        };
690        let outgoing = self.outgoing(paint, Snake::Vertical);
691        from_fn(move |f| write!(f, "{}{outgoing}", self.inside(paint, from, &fns)))
692    }
693}
694
695impl<C, T: Display, S> LineParts<CodeWidth<C>, T, S> {
696    /// Print text snakes below inside parts.
697    ///
698    /// ~~~ text
699    ///   ┆ │   │
700    ///   ┆ ╰────── text1
701    ///   ┆     │
702    ///   ┆     ╰── text2
703    /// ~~~
704    fn inside_text<'a>(&'a self, prefix: &'a impl Display, paint: Paint<S>) -> impl Display + 'a {
705        from_fn(move |f| {
706            let mut before = 0;
707            for (i, (code, label, style)) in self.inside.iter().enumerate() {
708                if let LabelKind::Text(text) = label {
709                    // "... │ ... │"
710                    writeln!(f, "{prefix}{}", self.inside_vert(paint, i))?;
711
712                    // "╰─...─ {text}"
713                    let (left, right) = code.left_right();
714                    let after = width!(self.inside) - before - code.width + width!(self.outgoing);
715                    let space = space(before + left);
716                    let snake = Snake::down_line(right + after + 1);
717                    writeln!(f, "{prefix}{space}{} {text}", styled(paint, style, &snake))?;
718                }
719                before += code.width;
720            }
721            Ok(())
722        })
723    }
724
725    /// Print snakes below incoming parts.
726    ///
727    /// ~~~ text
728    ///   ┆ │   │   │
729    ///   ┆ ╰───┴────── text
730    /// ~~~
731    ///
732    /// Or:
733    ///
734    /// ~~~ text
735    ///   ┆ │   │   │
736    ///   ┆ ╰───╯   |
737    /// ~~~
738    fn incoming_text<'a>(&'a self, dots: &'a impl Display, paint: Paint<S>) -> impl Display + 'a {
739        from_fn(move |f| {
740            if let Some((code, text, style)) = &self.incoming {
741                let snake = styled(paint, style, &Snake::Vertical);
742                let incoming = self.incoming(paint, Snake::Vertical);
743                writeln!(f, "{dots} {snake}{incoming}{}", self.inside_vert(paint, 0))?;
744
745                if let Some(text) = text {
746                    let snake = Snake::down_line_up_line(code.width, self.width() + 1 - code.width);
747                    let snake = styled(paint, style, &snake);
748                    writeln!(f, "{dots} {snake} {text}")
749                } else {
750                    let snake = Snake::down_line_up(code.width);
751                    let snake = styled(paint, style, &snake);
752                    writeln!(f, "{dots} {snake}{}", self.inside_vert(paint, 0))
753                }?
754            }
755            Ok(())
756        })
757    }
758}
759
760/// Parts used to draw code spans and lines.
761#[derive(Copy, Clone)]
762enum Snake {
763    /// "─"
764    Horizontal,
765    /// "│"
766    Vertical,
767    /// "┆"
768    VerticalDots,
769    /// "╭"
770    UpRight,
771    /// "╯"
772    RightUp,
773    /// "╰"
774    DownRight,
775    /// "▲"
776    ArrowUp,
777    /// "┴"
778    HorizontalUp,
779    /// "┬"
780    HorizontalDown,
781}
782
783impl Snake {
784    /// ─...─
785    fn line(len: usize) -> impl Display {
786        from_fn(move |f| write!(f, "{:─>len$}", ""))
787    }
788
789    /// ╰─...─
790    fn down_line(len: usize) -> impl Display {
791        from_fn(move |f| write!(f, "{}{}", Snake::DownRight, Snake::line(len)))
792    }
793
794    /// ╰─...─┴─...─
795    fn down_line_up_line(l: usize, r: usize) -> impl Display {
796        let l = Self::line(l);
797        let r = Self::line(r);
798        from_fn(move |f| write!(f, "{}{l}{}{r}", Self::DownRight, Self::HorizontalUp,))
799    }
800
801    /// "╰─...─╯"
802    fn down_line_up(len: usize) -> impl Display {
803        from_fn(move |f| write!(f, "{}{}{}", Self::DownRight, Self::line(len), Self::RightUp))
804    }
805
806    /// "╭─...─╯"
807    fn up_line_up(len: usize) -> impl Display {
808        from_fn(move |f| write!(f, "{}{}{}", Self::UpRight, Self::line(len), Self::RightUp))
809    }
810
811    /// "─...─╯"
812    fn line_up(len: usize) -> impl Display {
813        from_fn(move |f| write!(f, "{}{}", Self::line(len), Self::RightUp))
814    }
815
816    /// ─...─┬─...─
817    fn line_down_line(l: usize, r: usize) -> impl Display {
818        let l = Self::line(l);
819        let r = Self::line(r);
820        from_fn(move |f| write!(f, "{l}{}{r}", Self::HorizontalDown,))
821    }
822
823    fn as_str(self) -> &'static str {
824        match self {
825            Self::Horizontal => "─",
826            Self::Vertical => "│",
827            Self::VerticalDots => "┆",
828            Self::UpRight => "╭",
829            Self::RightUp => "╯",
830            Self::DownRight => "╰",
831            Self::ArrowUp => "▲",
832            Self::HorizontalUp => "┴",
833            Self::HorizontalDown => "┬",
834        }
835    }
836}
837
838impl Display for Snake {
839    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
840        self.as_str().fmt(f)
841    }
842}