rustc_errors/markdown/
term.rs

1use std::cell::Cell;
2use std::io::{self, Write};
3
4use anstyle::{AnsiColor, Effects, Style};
5
6use crate::markdown::{MdStream, MdTree};
7
8const DEFAULT_COLUMN_WIDTH: usize = 140;
9
10thread_local! {
11    /// Track the position of viewable characters in our buffer
12    static CURSOR: Cell<usize> = const { Cell::new(0) };
13    /// Width of the terminal
14    static WIDTH: Cell<usize> = const { Cell::new(DEFAULT_COLUMN_WIDTH) };
15}
16
17/// Print to terminal output to a buffer
18pub(crate) fn entrypoint(stream: &MdStream<'_>, buf: &mut Vec<u8>) -> io::Result<()> {
19    #[cfg(not(test))]
20    if let Some((w, _)) = termize::dimensions() {
21        WIDTH.set(std::cmp::min(w, DEFAULT_COLUMN_WIDTH));
22    }
23    write_stream(stream, buf, None, 0)?;
24    buf.write_all(b"\n")
25}
26/// Write the buffer, reset to the default style after each
27fn write_stream(
28    MdStream(stream): &MdStream<'_>,
29    buf: &mut Vec<u8>,
30    default: Option<Style>,
31    indent: usize,
32) -> io::Result<()> {
33    for tt in stream {
34        write_tt(tt, buf, default, indent)?;
35    }
36    reset_opt_style(buf, default)?;
37
38    Ok(())
39}
40
41fn write_tt(
42    tt: &MdTree<'_>,
43    buf: &mut Vec<u8>,
44    default: Option<Style>,
45    indent: usize,
46) -> io::Result<()> {
47    match tt {
48        MdTree::CodeBlock { txt, lang: _ } => {
49            reset_opt_style(buf, default)?;
50            let style = Style::new().effects(Effects::DIMMED);
51            write!(buf, "{style}{txt}{style:#}")?;
52            render_opt_style(buf, default)?;
53        }
54        MdTree::CodeInline(txt) => {
55            reset_opt_style(buf, default)?;
56            write_wrapping(buf, txt, indent, None, Some(Style::new().effects(Effects::DIMMED)))?;
57            render_opt_style(buf, default)?;
58        }
59        MdTree::Strong(txt) => {
60            reset_opt_style(buf, default)?;
61            write_wrapping(buf, txt, indent, None, Some(Style::new().effects(Effects::BOLD)))?;
62            render_opt_style(buf, default)?;
63        }
64        MdTree::Emphasis(txt) => {
65            reset_opt_style(buf, default)?;
66            write_wrapping(buf, txt, indent, None, Some(Style::new().effects(Effects::ITALIC)))?;
67            render_opt_style(buf, default)?;
68        }
69        MdTree::Strikethrough(txt) => {
70            reset_opt_style(buf, default)?;
71            write_wrapping(
72                buf,
73                txt,
74                indent,
75                None,
76                Some(Style::new().effects(Effects::STRIKETHROUGH)),
77            )?;
78            render_opt_style(buf, default)?;
79        }
80        MdTree::PlainText(txt) => {
81            write_wrapping(buf, txt, indent, None, None)?;
82        }
83        MdTree::Link { disp, link } => {
84            write_wrapping(buf, disp, indent, Some(link), None)?;
85        }
86        MdTree::ParagraphBreak => {
87            buf.write_all(b"\n\n")?;
88            reset_cursor();
89        }
90        MdTree::LineBreak => {
91            buf.write_all(b"\n")?;
92            reset_cursor();
93        }
94        MdTree::HorizontalRule => {
95            (0..WIDTH.get()).for_each(|_| buf.write_all(b"-").unwrap());
96            reset_cursor();
97        }
98        MdTree::Heading(n, stream) => {
99            let cs = match n {
100                1 => AnsiColor::BrightCyan.on_default().effects(Effects::BOLD | Effects::UNDERLINE),
101                2 => AnsiColor::BrightCyan.on_default().effects(Effects::UNDERLINE),
102                3 => AnsiColor::BrightCyan.on_default().effects(Effects::ITALIC),
103                4.. => AnsiColor::Cyan.on_default().effects(Effects::UNDERLINE | Effects::ITALIC),
104                0 => unreachable!(),
105            };
106            reset_opt_style(buf, default)?;
107            write!(buf, "{cs}")?;
108            write_stream(stream, buf, Some(cs), 0)?;
109            write!(buf, "{cs:#}")?;
110            render_opt_style(buf, default)?;
111            buf.write_all(b"\n")?;
112        }
113        MdTree::OrderedListItem(n, stream) => {
114            let base = format!("{n}. ");
115            write_wrapping(buf, &format!("{base:<4}"), indent, None, None)?;
116            write_stream(stream, buf, None, indent + 4)?;
117        }
118        MdTree::UnorderedListItem(stream) => {
119            let base = "* ";
120            write_wrapping(buf, &format!("{base:<4}"), indent, None, None)?;
121            write_stream(stream, buf, None, indent + 4)?;
122        }
123        // Patterns popped in previous step
124        MdTree::Comment(_) | MdTree::LinkDef { .. } | MdTree::RefLink { .. } => unreachable!(),
125    }
126
127    Ok(())
128}
129
130fn render_opt_style(buf: &mut Vec<u8>, style: Option<Style>) -> io::Result<()> {
131    if let Some(style) = &style {
132        write!(buf, "{style}")?;
133    }
134    Ok(())
135}
136
137fn reset_opt_style(buf: &mut Vec<u8>, style: Option<Style>) -> io::Result<()> {
138    if let Some(style) = &style {
139        write!(buf, "{style:#}")?;
140    }
141    Ok(())
142}
143
144/// End of that block, just wrap the line
145fn reset_cursor() {
146    CURSOR.set(0);
147}
148
149/// Change to be generic on Write for testing. If we have a link URL, we don't
150/// count the extra tokens to make it clickable.
151fn write_wrapping(
152    buf: &mut Vec<u8>,
153    text: &str,
154    indent: usize,
155    link_url: Option<&str>,
156    style: Option<Style>,
157) -> io::Result<()> {
158    render_opt_style(buf, style)?;
159
160    let ind_ws = &b"          "[..indent];
161    let mut to_write = text;
162    if let Some(url) = link_url {
163        // This is a nonprinting prefix so we don't increment our cursor
164        write!(buf, "\x1b]8;;{url}\x1b\\")?;
165    }
166    CURSOR.with(|cur| {
167        loop {
168            if cur.get() == 0 {
169                buf.write_all(ind_ws)?;
170                cur.set(indent);
171            }
172            let ch_count = WIDTH.get() - cur.get();
173            let mut iter = to_write.char_indices();
174            let Some((end_idx, _ch)) = iter.nth(ch_count) else {
175                // Write entire line
176                buf.write_all(to_write.as_bytes())?;
177                cur.set(cur.get() + to_write.chars().count());
178                break;
179            };
180
181            if let Some((break_idx, ch)) = to_write[..end_idx]
182                .char_indices()
183                .rev()
184                .find(|(_idx, ch)| ch.is_whitespace() || ['_', '-'].contains(ch))
185            {
186                // Found whitespace to break at
187                if ch.is_whitespace() {
188                    writeln!(buf, "{}", &to_write[..break_idx])?;
189                    to_write = to_write[break_idx..].trim_start();
190                } else {
191                    // Break at a `-` or `_` separator
192                    writeln!(buf, "{}", &to_write.get(..break_idx + 1).unwrap_or(to_write))?;
193                    to_write = to_write.get(break_idx + 1..).unwrap_or_default().trim_start();
194                }
195            } else {
196                // No whitespace, we need to just split
197                let ws_idx =
198                    iter.find(|(_, ch)| ch.is_whitespace()).map_or(to_write.len(), |(idx, _)| idx);
199                writeln!(buf, "{}", &to_write[..ws_idx])?;
200                to_write = to_write.get(ws_idx + 1..).map_or("", str::trim_start);
201            }
202            cur.set(0);
203        }
204        if link_url.is_some() {
205            buf.write_all(b"\x1b]8;;\x1b\\")?;
206        }
207        reset_opt_style(buf, style)?;
208        Ok(())
209    })
210}
211
212#[cfg(test)]
213#[path = "tests/term.rs"]
214mod tests;