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 static CURSOR: Cell<usize> = const { Cell::new(0) };
13 static WIDTH: Cell<usize> = const { Cell::new(DEFAULT_COLUMN_WIDTH) };
15}
16
17pub(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
27fn 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 MdTree::Comment(_) | MdTree::LinkDef { .. } | MdTree::RefLink { .. } => unreachable!(),
115 }
116
117 buf.reset()?;
118
119 Ok(())
120}
121
122fn reset_cursor() {
124 CURSOR.with(|cur| cur.set(0));
125}
126
127fn 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 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 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 if ch.is_whitespace() {
163 writeln!(buf, "{}", &to_write[..break_idx])?;
164 to_write = to_write[break_idx..].trim_start();
165 } else {
166 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 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;