1use std::fmt;
2
3use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
4
5use crate::diagnostic_chain::DiagnosticChain;
6use crate::protocol::{Diagnostic, Severity};
7use crate::{LabeledSpan, MietteError, ReportHandler, SourceCode, SourceSpan, SpanContents};
8
9#[derive(Debug, Clone)]
15pub struct NarratableReportHandler {
16 context_lines: usize,
17 with_cause_chain: bool,
18 footer: Option<String>,
19}
20
21impl NarratableReportHandler {
22 pub const fn new() -> Self {
25 Self {
26 footer: None,
27 context_lines: 1,
28 with_cause_chain: true,
29 }
30 }
31
32 pub const fn with_cause_chain(mut self) -> Self {
35 self.with_cause_chain = true;
36 self
37 }
38
39 pub const fn without_cause_chain(mut self) -> Self {
41 self.with_cause_chain = false;
42 self
43 }
44
45 pub fn with_footer(mut self, footer: String) -> Self {
47 self.footer = Some(footer);
48 self
49 }
50
51 pub const fn with_context_lines(mut self, lines: usize) -> Self {
53 self.context_lines = lines;
54 self
55 }
56}
57
58impl Default for NarratableReportHandler {
59 fn default() -> Self {
60 Self::new()
61 }
62}
63
64impl NarratableReportHandler {
65 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
88 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
99 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
113 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
126 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
152 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 let new_span = LabeledSpan::new(
181 left.label().map(String::from),
182 left.offset(),
183 if right_end >= left_end {
184 right_end - left.offset()
186 } else {
187 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, 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
221 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
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 line_str = String::with_capacity(context.len());
299 let mut lines = Vec::with_capacity(1);
300 let mut iter = context.chars().peekable();
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
346impl ReportHandler for NarratableReportHandler {
347 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
356struct Line {
361 line_number: usize,
362 offset: usize,
363 text: String,
364 at_end_of_file: bool,
365}
366
367enum SpanAttach {
368 Contained { col_start: usize, col_end: usize },
369 Starts { col_start: usize },
370 Ends { col_end: usize },
371}
372
373fn 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 column += 1;
389 } column
392}
393
394impl Line {
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 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}