1 use std::fmt;
2
3 use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
4
5 use crate::diagnostic_chain::DiagnosticChain;
6 use crate::protocol::{Diagnostic, Severity};
7 use crate::{LabeledSpan, MietteError, ReportHandler, SourceCode, SourceSpan, SpanContents};
8
9 /**
10 [`ReportHandler`] that renders plain text and avoids extraneous graphics.
11 It's optimized for screen readers and braille users, but is also used in any
12 non-graphical environments, such as non-TTY output.
13 */
14 #[derive(Debug, Clone)]
15 pub struct NarratableReportHandler {
16 context_lines: usize,
17 with_cause_chain: bool,
18 footer: Option<String>,
19 }
20
21 impl NarratableReportHandler {
22 /// Create a new [`NarratableReportHandler`]. There are no customization
23 /// options.
new() -> Self24 pub const fn new() -> Self {
25 Self {
26 footer: None,
27 context_lines: 1,
28 with_cause_chain: true,
29 }
30 }
31
32 /// Include the cause chain of the top-level error in the report, if
33 /// available.
with_cause_chain(mut self) -> Self34 pub const fn with_cause_chain(mut self) -> Self {
35 self.with_cause_chain = true;
36 self
37 }
38
39 /// Do not include the cause chain of the top-level error in the report.
without_cause_chain(mut self) -> Self40 pub const fn without_cause_chain(mut self) -> Self {
41 self.with_cause_chain = false;
42 self
43 }
44
45 /// Set the footer to be displayed at the end of the report.
with_footer(mut self, footer: String) -> Self46 pub fn with_footer(mut self, footer: String) -> Self {
47 self.footer = Some(footer);
48 self
49 }
50
51 /// Sets the number of lines of context to show around each error.
with_context_lines(mut self, lines: usize) -> Self52 pub const fn with_context_lines(mut self, lines: usize) -> Self {
53 self.context_lines = lines;
54 self
55 }
56 }
57
58 impl Default for NarratableReportHandler {
default() -> Self59 fn default() -> Self {
60 Self::new()
61 }
62 }
63
64 impl NarratableReportHandler {
65 /// Render a [`Diagnostic`]. This function is mostly internal and meant to
66 /// be called by the toplevel [`ReportHandler`] handler, but is
67 /// made public to make it easier (possible) to test in isolation from
68 /// global state.
render_report( &self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic), ) -> fmt::Result69 pub fn render_report(
70 &self,
71 f: &mut impl fmt::Write,
72 diagnostic: &(dyn Diagnostic),
73 ) -> fmt::Result {
74 self.render_header(f, diagnostic)?;
75 if self.with_cause_chain {
76 self.render_causes(f, diagnostic)?;
77 }
78 let src = diagnostic.source_code();
79 self.render_snippets(f, diagnostic, src)?;
80 self.render_footer(f, diagnostic)?;
81 self.render_related(f, diagnostic, src)?;
82 if let Some(footer) = &self.footer {
83 writeln!(f, "{}", footer)?;
84 }
85 Ok(())
86 }
87
render_header(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result88 fn render_header(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
89 writeln!(f, "{}", diagnostic)?;
90 let severity = match diagnostic.severity() {
91 Some(Severity::Error) | None => "error",
92 Some(Severity::Warning) => "warning",
93 Some(Severity::Advice) => "advice",
94 };
95 writeln!(f, " Diagnostic severity: {}", severity)?;
96 Ok(())
97 }
98
render_causes(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result99 fn render_causes(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
100 if let Some(cause_iter) = diagnostic
101 .diagnostic_source()
102 .map(DiagnosticChain::from_diagnostic)
103 .or_else(|| diagnostic.source().map(DiagnosticChain::from_stderror))
104 {
105 for error in cause_iter {
106 writeln!(f, " Caused by: {}", error)?;
107 }
108 }
109
110 Ok(())
111 }
112
render_footer(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result113 fn render_footer(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
114 if let Some(help) = diagnostic.help() {
115 writeln!(f, "diagnostic help: {}", help)?;
116 }
117 if let Some(code) = diagnostic.code() {
118 writeln!(f, "diagnostic code: {}", code)?;
119 }
120 if let Some(url) = diagnostic.url() {
121 writeln!(f, "For more details, see:\n{}", url)?;
122 }
123 Ok(())
124 }
125
render_related( &self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic), parent_src: Option<&dyn SourceCode>, ) -> fmt::Result126 fn render_related(
127 &self,
128 f: &mut impl fmt::Write,
129 diagnostic: &(dyn Diagnostic),
130 parent_src: Option<&dyn SourceCode>,
131 ) -> fmt::Result {
132 if let Some(related) = diagnostic.related() {
133 writeln!(f)?;
134 for rel in related {
135 match rel.severity() {
136 Some(Severity::Error) | None => write!(f, "Error: ")?,
137 Some(Severity::Warning) => write!(f, "Warning: ")?,
138 Some(Severity::Advice) => write!(f, "Advice: ")?,
139 };
140 self.render_header(f, rel)?;
141 writeln!(f)?;
142 self.render_causes(f, rel)?;
143 let src = rel.source_code().or(parent_src);
144 self.render_snippets(f, rel, src)?;
145 self.render_footer(f, rel)?;
146 self.render_related(f, rel, src)?;
147 }
148 }
149 Ok(())
150 }
151
render_snippets( &self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic), source_code: Option<&dyn SourceCode>, ) -> fmt::Result152 fn render_snippets(
153 &self,
154 f: &mut impl fmt::Write,
155 diagnostic: &(dyn Diagnostic),
156 source_code: Option<&dyn SourceCode>,
157 ) -> fmt::Result {
158 if let Some(source) = source_code {
159 if let Some(labels) = diagnostic.labels() {
160 let mut labels = labels.collect::<Vec<_>>();
161 labels.sort_unstable_by_key(|l| l.inner().offset());
162 if !labels.is_empty() {
163 let contents = labels
164 .iter()
165 .map(|label| {
166 source.read_span(label.inner(), self.context_lines, self.context_lines)
167 })
168 .collect::<Result<Vec<Box<dyn SpanContents<'_>>>, MietteError>>()
169 .map_err(|_| fmt::Error)?;
170 let mut contexts = Vec::new();
171 for (right, right_conts) in labels.iter().cloned().zip(contents.iter()) {
172 if contexts.is_empty() {
173 contexts.push((right, right_conts));
174 } else {
175 let (left, left_conts) = contexts.last().unwrap().clone();
176 let left_end = left.offset() + left.len();
177 let right_end = right.offset() + right.len();
178 if left_conts.line() + left_conts.line_count() >= right_conts.line() {
179 // The snippets will overlap, so we create one Big Chunky Boi
180 let new_span = LabeledSpan::new(
181 left.label().map(String::from),
182 left.offset(),
183 if right_end >= left_end {
184 // Right end goes past left end
185 right_end - left.offset()
186 } else {
187 // right is contained inside left
188 left.len()
189 },
190 );
191 if source
192 .read_span(
193 new_span.inner(),
194 self.context_lines,
195 self.context_lines,
196 )
197 .is_ok()
198 {
199 contexts.pop();
200 contexts.push((
201 new_span, // We'll throw this away later
202 left_conts,
203 ));
204 } else {
205 contexts.push((right, right_conts));
206 }
207 } else {
208 contexts.push((right, right_conts));
209 }
210 }
211 }
212 for (ctx, _) in contexts {
213 self.render_context(f, source, &ctx, &labels[..])?;
214 }
215 }
216 }
217 }
218 Ok(())
219 }
220
render_context( &self, f: &mut impl fmt::Write, source: &dyn SourceCode, context: &LabeledSpan, labels: &[LabeledSpan], ) -> fmt::Result221 fn render_context(
222 &self,
223 f: &mut impl fmt::Write,
224 source: &dyn SourceCode,
225 context: &LabeledSpan,
226 labels: &[LabeledSpan],
227 ) -> fmt::Result {
228 let (contents, lines) = self.get_lines(source, context.inner())?;
229 write!(f, "Begin snippet")?;
230 if let Some(filename) = contents.name() {
231 write!(f, " for {}", filename,)?;
232 }
233 writeln!(
234 f,
235 " starting at line {}, column {}",
236 contents.line() + 1,
237 contents.column() + 1
238 )?;
239 writeln!(f)?;
240 for line in &lines {
241 writeln!(f, "snippet line {}: {}", line.line_number, line.text)?;
242 let relevant = labels
243 .iter()
244 .filter_map(|l| line.span_attach(l.inner()).map(|a| (a, l)));
245 for (attach, label) in relevant {
246 match attach {
247 SpanAttach::Contained { col_start, col_end } if col_start == col_end => {
248 write!(
249 f,
250 " label at line {}, column {}",
251 line.line_number, col_start,
252 )?;
253 }
254 SpanAttach::Contained { col_start, col_end } => {
255 write!(
256 f,
257 " label at line {}, columns {} to {}",
258 line.line_number, col_start, col_end,
259 )?;
260 }
261 SpanAttach::Starts { col_start } => {
262 write!(
263 f,
264 " label starting at line {}, column {}",
265 line.line_number, col_start,
266 )?;
267 }
268 SpanAttach::Ends { col_end } => {
269 write!(
270 f,
271 " label ending at line {}, column {}",
272 line.line_number, col_end,
273 )?;
274 }
275 }
276 if let Some(label) = label.label() {
277 write!(f, ": {}", label)?;
278 }
279 writeln!(f)?;
280 }
281 }
282 Ok(())
283 }
284
get_lines<'a>( &'a self, source: &'a dyn SourceCode, context_span: &'a SourceSpan, ) -> Result<(Box<dyn SpanContents<'a> + 'a>, Vec<Line>), fmt::Error>285 fn get_lines<'a>(
286 &'a self,
287 source: &'a dyn SourceCode,
288 context_span: &'a SourceSpan,
289 ) -> Result<(Box<dyn SpanContents<'a> + 'a>, Vec<Line>), fmt::Error> {
290 let context_data = source
291 .read_span(context_span, self.context_lines, self.context_lines)
292 .map_err(|_| fmt::Error)?;
293 let context = std::str::from_utf8(context_data.data()).expect("Bad utf8 detected");
294 let mut line = context_data.line();
295 let mut column = context_data.column();
296 let mut offset = context_data.span().offset();
297 let mut line_offset = offset;
298 let mut iter = context.chars().peekable();
299 let mut line_str = String::new();
300 let mut lines = Vec::new();
301 while let Some(char) = iter.next() {
302 offset += char.len_utf8();
303 let mut at_end_of_file = false;
304 match char {
305 '\r' => {
306 if iter.next_if_eq(&'\n').is_some() {
307 offset += 1;
308 line += 1;
309 column = 0;
310 } else {
311 line_str.push(char);
312 column += 1;
313 }
314 at_end_of_file = iter.peek().is_none();
315 }
316 '\n' => {
317 at_end_of_file = iter.peek().is_none();
318 line += 1;
319 column = 0;
320 }
321 _ => {
322 line_str.push(char);
323 column += 1;
324 }
325 }
326
327 if iter.peek().is_none() && !at_end_of_file {
328 line += 1;
329 }
330
331 if column == 0 || iter.peek().is_none() {
332 lines.push(Line {
333 line_number: line,
334 offset: line_offset,
335 text: line_str.clone(),
336 at_end_of_file,
337 });
338 line_str.clear();
339 line_offset = offset;
340 }
341 }
342 Ok((context_data, lines))
343 }
344 }
345
346 impl ReportHandler for NarratableReportHandler {
debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result347 fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
348 if f.alternate() {
349 return fmt::Debug::fmt(diagnostic, f);
350 }
351
352 self.render_report(f, diagnostic)
353 }
354 }
355
356 /*
357 Support types
358 */
359
360 struct Line {
361 line_number: usize,
362 offset: usize,
363 text: String,
364 at_end_of_file: bool,
365 }
366
367 enum SpanAttach {
368 Contained { col_start: usize, col_end: usize },
369 Starts { col_start: usize },
370 Ends { col_end: usize },
371 }
372
373 /// Returns column at offset, and nearest boundary if offset is in the middle of
374 /// the character
safe_get_column(text: &str, offset: usize, start: bool) -> usize375 fn safe_get_column(text: &str, offset: usize, start: bool) -> usize {
376 let mut column = text.get(0..offset).map(|s| s.width()).unwrap_or_else(|| {
377 let mut column = 0;
378 for (idx, c) in text.char_indices() {
379 if offset <= idx {
380 break;
381 }
382 column += c.width().unwrap_or(0);
383 }
384 column
385 });
386 if start {
387 // Offset are zero-based, so plus one
388 column += 1;
389 } // On the other hand for end span, offset refers for the next column
390 // So we should do -1. column+1-1 == column
391 column
392 }
393
394 impl Line {
span_attach(&self, span: &SourceSpan) -> Option<SpanAttach>395 fn span_attach(&self, span: &SourceSpan) -> Option<SpanAttach> {
396 let span_end = span.offset() + span.len();
397 let line_end = self.offset + self.text.len();
398
399 let start_after = span.offset() >= self.offset;
400 let end_before = self.at_end_of_file || span_end <= line_end;
401
402 if start_after && end_before {
403 let col_start = safe_get_column(&self.text, span.offset() - self.offset, true);
404 let col_end = if span.is_empty() {
405 col_start
406 } else {
407 // span_end refers to the next character after token
408 // while col_end refers to the exact character, so -1
409 safe_get_column(&self.text, span_end - self.offset, false)
410 };
411 return Some(SpanAttach::Contained { col_start, col_end });
412 }
413 if start_after && span.offset() <= line_end {
414 let col_start = safe_get_column(&self.text, span.offset() - self.offset, true);
415 return Some(SpanAttach::Starts { col_start });
416 }
417 if end_before && span_end >= self.offset {
418 let col_end = safe_get_column(&self.text, span_end - self.offset, false);
419 return Some(SpanAttach::Ends { col_end });
420 }
421 None
422 }
423 }
424