rustc_errors/markdown/
term.rs

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