codespan_reporting/term/views.rs
1use alloc::{
2 string::{String, ToString},
3 vec,
4 vec::Vec,
5};
6use core::ops::Range;
7
8use crate::diagnostic::{Diagnostic, LabelStyle};
9use crate::files::{Error, Files, Location};
10use crate::term::renderer::{Locus, MultiLabel, Renderer, SingleLabel};
11use crate::term::Config;
12
13/// Calculate the number of decimal digits in `n`.
14fn count_digits(n: usize) -> usize {
15 n.ilog10() as usize + 1
16}
17
18/// Output a richly formatted diagnostic, with source code previews.
19pub struct RichDiagnostic<'diagnostic, 'config, FileId> {
20 diagnostic: &'diagnostic Diagnostic<FileId>,
21 config: &'config Config,
22}
23
24impl<'diagnostic, 'config, FileId> RichDiagnostic<'diagnostic, 'config, FileId>
25where
26 FileId: Copy + PartialEq,
27{
28 pub fn new(
29 diagnostic: &'diagnostic Diagnostic<FileId>,
30 config: &'config Config,
31 ) -> RichDiagnostic<'diagnostic, 'config, FileId> {
32 RichDiagnostic { diagnostic, config }
33 }
34
35 pub fn render<'files>(
36 &self,
37 files: &'files (impl Files<'files, FileId = FileId> + ?Sized),
38 renderer: &mut Renderer<'_, '_>,
39 ) -> Result<(), Error>
40 where
41 FileId: 'files,
42 {
43 use alloc::collections::BTreeMap;
44
45 struct LabeledFile<'diagnostic, FileId> {
46 file_id: FileId,
47 start: usize,
48 name: String,
49 location: Location,
50 num_multi_labels: usize,
51 lines: BTreeMap<usize, Line<'diagnostic>>,
52 max_label_style: LabelStyle,
53 }
54
55 impl<'diagnostic, FileId> LabeledFile<'diagnostic, FileId> {
56 fn get_or_insert_line(
57 &mut self,
58 line_index: usize,
59 line_range: Range<usize>,
60 line_number: usize,
61 ) -> &mut Line<'diagnostic> {
62 self.lines.entry(line_index).or_insert_with(|| Line {
63 range: line_range,
64 number: line_number,
65 single_labels: vec![],
66 multi_labels: vec![],
67 // This has to be false by default so we know if it must be rendered by another condition already.
68 must_render: false,
69 })
70 }
71 }
72
73 struct Line<'diagnostic> {
74 number: usize,
75 range: core::ops::Range<usize>,
76 // TODO: How do we reuse these allocations?
77 single_labels: Vec<SingleLabel<'diagnostic>>,
78 multi_labels: Vec<(usize, LabelStyle, MultiLabel<'diagnostic>)>,
79 must_render: bool,
80 }
81
82 // TODO: Make this data structure external, to allow for allocation reuse
83 let mut labeled_files = Vec::<LabeledFile<'_, _>>::new();
84 // Keep track of the outer padding to use when rendering the
85 // snippets of source code.
86 let mut outer_padding = 0;
87
88 // Group labels by file
89 for label in &self.diagnostic.labels {
90 let start_line_index = files.line_index(label.file_id, label.range.start)?;
91 let start_line_number = files.line_number(label.file_id, start_line_index)?;
92 let start_line_range = files.line_range(label.file_id, start_line_index)?;
93 let end_line_index = files.line_index(label.file_id, label.range.end)?;
94 let end_line_number = files.line_number(label.file_id, end_line_index)?;
95 let end_line_range = files.line_range(label.file_id, end_line_index)?;
96
97 outer_padding = core::cmp::max(outer_padding, count_digits(start_line_number));
98 outer_padding = core::cmp::max(outer_padding, count_digits(end_line_number));
99
100 // NOTE: This could be made more efficient by using an associative
101 // data structure like a hashmap or B-tree, but we use a vector to
102 // preserve the order that unique files appear in the list of labels.
103 let labeled_file = match labeled_files
104 .iter_mut()
105 .find(|labeled_file| label.file_id == labeled_file.file_id)
106 {
107 Some(labeled_file) => {
108 // another diagnostic also referenced this file
109 if labeled_file.max_label_style > label.style
110 || (labeled_file.max_label_style == label.style
111 && labeled_file.start > label.range.start)
112 {
113 // this label has a higher style or has the same style but starts earlier
114 labeled_file.start = label.range.start;
115 labeled_file.location = files.location(label.file_id, label.range.start)?;
116 labeled_file.max_label_style = label.style;
117 }
118 labeled_file
119 }
120 None => {
121 // no other diagnostic referenced this file yet
122 labeled_files.push(LabeledFile {
123 file_id: label.file_id,
124 start: label.range.start,
125 name: files.name(label.file_id)?.to_string(),
126 location: files.location(label.file_id, label.range.start)?,
127 num_multi_labels: 0,
128 lines: BTreeMap::new(),
129 max_label_style: label.style,
130 });
131 // this unwrap should never fail because we just pushed an element
132 labeled_files
133 .last_mut()
134 .expect("just pushed an element that disappeared")
135 }
136 };
137
138 // insert context lines before label
139 // start from 1 because 0 would be the start of the label itself
140 for offset in 1..self.config.before_label_lines + 1 {
141 let index = if let Some(index) = start_line_index.checked_sub(offset) {
142 index
143 } else {
144 // we are going from smallest to largest offset, so if
145 // the offset can not be subtracted from the start we
146 // reached the first line
147 break;
148 };
149
150 if let Ok(range) = files.line_range(label.file_id, index) {
151 let line =
152 labeled_file.get_or_insert_line(index, range, start_line_number - offset);
153 line.must_render = true;
154 } else {
155 break;
156 }
157 }
158
159 // insert context lines after label
160 // start from 1 because 0 would be the end of the label itself
161 for offset in 1..self.config.after_label_lines + 1 {
162 let index = end_line_index
163 .checked_add(offset)
164 .expect("line index too big");
165
166 if let Ok(range) = files.line_range(label.file_id, index) {
167 let line =
168 labeled_file.get_or_insert_line(index, range, end_line_number + offset);
169 line.must_render = true;
170 } else {
171 break;
172 }
173 }
174
175 if start_line_index == end_line_index {
176 // Single line
177 //
178 // ```text
179 // 2 │ (+ test "")
180 // │ ^^ expected `Int` but found `String`
181 // ```
182 let label_start = label.range.start - start_line_range.start;
183 // Ensure that we print at least one caret, even when we
184 // have a zero-length source range.
185 let label_end =
186 usize::max(label.range.end - start_line_range.start, label_start + 1);
187
188 let line = labeled_file.get_or_insert_line(
189 start_line_index,
190 start_line_range,
191 start_line_number,
192 );
193
194 // Ensure that the single line labels are lexicographically
195 // sorted by the range of source code that they cover.
196 let index = match line.single_labels.binary_search_by(|(_, range, _)| {
197 // `Range<usize>` doesn't implement `Ord`, so convert to `(usize, usize)`
198 // to piggyback off its lexicographic comparison implementation.
199 (range.start, range.end).cmp(&(label_start, label_end))
200 }) {
201 // If the ranges are the same, order the labels in reverse
202 // to how they were originally specified in the diagnostic.
203 // This helps with printing in the renderer.
204 Ok(index) | Err(index) => index,
205 };
206
207 line.single_labels
208 .insert(index, (label.style, label_start..label_end, &label.message));
209
210 // If this line is not rendered, the SingleLabel is not visible.
211 line.must_render = true;
212 } else {
213 // Multiple lines
214 //
215 // ```text
216 // 4 │ fizz₁ num = case (mod num 5) (mod num 3) of
217 // │ ╭─────────────^
218 // 5 │ │ 0 0 => "FizzBuzz"
219 // 6 │ │ 0 _ => "Fizz"
220 // 7 │ │ _ 0 => "Buzz"
221 // 8 │ │ _ _ => num
222 // │ ╰──────────────^ `case` clauses have incompatible types
223 // ```
224
225 let label_index = labeled_file.num_multi_labels;
226 labeled_file.num_multi_labels += 1;
227
228 // First labeled line
229 let label_start = label.range.start - start_line_range.start;
230
231 let start_line = labeled_file.get_or_insert_line(
232 start_line_index,
233 start_line_range.clone(),
234 start_line_number,
235 );
236
237 start_line.multi_labels.push((
238 label_index,
239 label.style,
240 MultiLabel::Top(label_start),
241 ));
242
243 // The first line has to be rendered so the start of the label is visible.
244 start_line.must_render = true;
245
246 // Marked lines
247 //
248 // ```text
249 // 5 │ │ 0 0 => "FizzBuzz"
250 // 6 │ │ 0 _ => "Fizz"
251 // 7 │ │ _ 0 => "Buzz"
252 // ```
253 for line_index in (start_line_index + 1)..end_line_index {
254 let line_range = files.line_range(label.file_id, line_index)?;
255 let line_number = files.line_number(label.file_id, line_index)?;
256
257 outer_padding = core::cmp::max(outer_padding, count_digits(line_number));
258
259 let line = labeled_file.get_or_insert_line(line_index, line_range, line_number);
260
261 line.multi_labels
262 .push((label_index, label.style, MultiLabel::Left));
263
264 // The line should be rendered to match the configuration of how much context to show.
265 line.must_render |=
266 // Is this line part of the context after the start of the label?
267 line_index - start_line_index <= self.config.start_context_lines
268 ||
269 // Is this line part of the context before the end of the label?
270 end_line_index - line_index <= self.config.end_context_lines;
271 }
272
273 // Last labeled line
274 //
275 // ```text
276 // 8 │ │ _ _ => num
277 // │ ╰──────────────^ `case` clauses have incompatible types
278 // ```
279 let label_end = label.range.end - end_line_range.start;
280
281 let end_line = labeled_file.get_or_insert_line(
282 end_line_index,
283 end_line_range,
284 end_line_number,
285 );
286
287 end_line.multi_labels.push((
288 label_index,
289 label.style,
290 MultiLabel::Bottom(label_end, &label.message),
291 ));
292
293 // The last line has to be rendered so the end of the label is visible.
294 end_line.must_render = true;
295 }
296 }
297
298 // Header and message
299 //
300 // ```text
301 // error[E0001]: unexpected type in `+` application
302 // ```
303 renderer.render_header(
304 None,
305 self.diagnostic.severity,
306 self.diagnostic.code.as_deref(),
307 self.diagnostic.message.as_str(),
308 )?;
309
310 // Source snippets
311 //
312 // ```text
313 // ┌─ test:2:9
314 // │
315 // 2 │ (+ test "")
316 // │ ^^ expected `Int` but found `String`
317 // │
318 // ```
319 let mut labeled_files = labeled_files.into_iter().peekable();
320 while let Some(labeled_file) = labeled_files.next() {
321 let source = files.source(labeled_file.file_id)?;
322 let source = source.as_ref();
323
324 // Top left border and locus.
325 //
326 // ```text
327 // ┌─ test:2:9
328 // ```
329 if !labeled_file.lines.is_empty() {
330 renderer.render_snippet_start(
331 outer_padding,
332 &Locus {
333 name: labeled_file.name,
334 location: labeled_file.location,
335 },
336 )?;
337 renderer.render_snippet_empty(
338 outer_padding,
339 self.diagnostic.severity,
340 labeled_file.num_multi_labels,
341 &[],
342 )?;
343 }
344
345 let mut lines = labeled_file
346 .lines
347 .iter()
348 .filter(|(_, line)| line.must_render)
349 .peekable();
350
351 while let Some((line_index, line)) = lines.next() {
352 renderer.render_snippet_source(
353 outer_padding,
354 line.number,
355 &source[line.range.clone()],
356 self.diagnostic.severity,
357 &line.single_labels,
358 labeled_file.num_multi_labels,
359 &line.multi_labels,
360 )?;
361
362 // Check to see if we need to render any intermediate stuff
363 // before rendering the next line.
364 if let Some((next_line_index, next_line)) = lines.peek() {
365 match next_line_index.checked_sub(*line_index) {
366 // Consecutive lines
367 Some(1) => {}
368 // One line between the current line and the next line
369 Some(2) => {
370 // Write a source line
371 let file_id = labeled_file.file_id;
372
373 // This line was not intended to be rendered initially.
374 // To render the line right, we have to get back the original labels.
375 let labels = labeled_file
376 .lines
377 .get(&(line_index + 1))
378 .map_or(&[][..], |line| &line.multi_labels[..]);
379
380 renderer.render_snippet_source(
381 outer_padding,
382 files.line_number(file_id, line_index + 1)?,
383 &source[files.line_range(file_id, line_index + 1)?],
384 self.diagnostic.severity,
385 &[],
386 labeled_file.num_multi_labels,
387 labels,
388 )?;
389 }
390 // More than one line between the current line and the next line.
391 Some(_) | None => {
392 // Source break
393 //
394 // ```text
395 // ·
396 // ```
397 renderer.render_snippet_break(
398 outer_padding,
399 self.diagnostic.severity,
400 labeled_file.num_multi_labels,
401 &next_line.multi_labels,
402 )?;
403 }
404 }
405 }
406 }
407
408 // Check to see if we should render a trailing border after the
409 // final line of the snippet.
410 if labeled_files.peek().is_none() && self.diagnostic.notes.is_empty() {
411 // We don't render a border if we are at the final newline
412 // without trailing notes, because it would end up looking too
413 // spaced-out in combination with the final new line.
414 } else {
415 // Render the trailing snippet border.
416 renderer.render_snippet_empty(
417 outer_padding,
418 self.diagnostic.severity,
419 labeled_file.num_multi_labels,
420 &[],
421 )?;
422 }
423 }
424
425 // Additional notes
426 //
427 // ```text
428 // = expected type `Int`
429 // found type `String`
430 // ```
431 for note in &self.diagnostic.notes {
432 renderer.render_snippet_note(outer_padding, note)?;
433 }
434 renderer.render_empty()
435 }
436}
437
438/// Output a short diagnostic, with a line number, severity, and message.
439pub struct ShortDiagnostic<'diagnostic, FileId> {
440 diagnostic: &'diagnostic Diagnostic<FileId>,
441 show_notes: bool,
442}
443
444impl<'diagnostic, FileId> ShortDiagnostic<'diagnostic, FileId>
445where
446 FileId: Copy + PartialEq,
447{
448 pub fn new(
449 diagnostic: &'diagnostic Diagnostic<FileId>,
450 show_notes: bool,
451 ) -> ShortDiagnostic<'diagnostic, FileId> {
452 ShortDiagnostic {
453 diagnostic,
454 show_notes,
455 }
456 }
457
458 pub fn render<'files>(
459 &self,
460 files: &'files (impl Files<'files, FileId = FileId> + ?Sized),
461 renderer: &mut Renderer<'_, '_>,
462 ) -> Result<(), Error>
463 where
464 FileId: 'files,
465 {
466 // Located headers
467 //
468 // ```text
469 // test:2:9: error[E0001]: unexpected type in `+` application
470 // ```
471 let mut primary_labels_encountered = 0;
472 let labels = self.diagnostic.labels.iter();
473 for label in labels.filter(|label| label.style == LabelStyle::Primary) {
474 primary_labels_encountered += 1;
475
476 renderer.render_header(
477 Some(&Locus {
478 name: files.name(label.file_id)?.to_string(),
479 location: files.location(label.file_id, label.range.start)?,
480 }),
481 self.diagnostic.severity,
482 self.diagnostic.code.as_deref(),
483 self.diagnostic.message.as_str(),
484 )?;
485 }
486
487 // Fallback to printing a non-located header if no primary labels were encountered
488 //
489 // ```text
490 // error[E0002]: Bad config found
491 // ```
492 if primary_labels_encountered == 0 {
493 renderer.render_header(
494 None,
495 self.diagnostic.severity,
496 self.diagnostic.code.as_deref(),
497 self.diagnostic.message.as_str(),
498 )?;
499 }
500
501 if self.show_notes {
502 // Additional notes
503 //
504 // ```text
505 // = expected type `Int`
506 // found type `String`
507 // ```
508 for note in &self.diagnostic.notes {
509 renderer.render_snippet_note(0, note)?;
510 }
511 }
512
513 Ok(())
514 }
515}