Skip to main content

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