codespan_reporting/term/views.rs
1use std::ops::Range;
2
3use crate::diagnostic::{Diagnostic, LabelStyle};
4use crate::files::{Error, Files, Location};
5use crate::term::renderer::{Locus, MultiLabel, Renderer, SingleLabel};
6use crate::term::Config;
7
8/// Count the number of decimal digits in `n`.
9fn count_digits(mut n: usize) -> usize {
10 let mut count = 0;
11 while n != 0 {
12 count += 1;
13 n /= 10; // remove last digit
14 }
15 count
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>,
38 renderer: &mut Renderer<'_, '_>,
39 ) -> Result<(), Error>
40 where
41 FileId: 'files,
42 {
43 use std::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: std::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 = std::cmp::max(outer_padding, count_digits(start_line_number));
98 outer_padding = std::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 if start_line_index == end_line_index {
139 // Single line
140 //
141 // ```text
142 // 2 │ (+ test "")
143 // │ ^^ expected `Int` but found `String`
144 // ```
145 let label_start = label.range.start - start_line_range.start;
146 // Ensure that we print at least one caret, even when we
147 // have a zero-length source range.
148 let label_end =
149 usize::max(label.range.end - start_line_range.start, label_start + 1);
150
151 let line = labeled_file.get_or_insert_line(
152 start_line_index,
153 start_line_range,
154 start_line_number,
155 );
156
157 // Ensure that the single line labels are lexicographically
158 // sorted by the range of source code that they cover.
159 let index = match line.single_labels.binary_search_by(|(_, range, _)| {
160 // `Range<usize>` doesn't implement `Ord`, so convert to `(usize, usize)`
161 // to piggyback off its lexicographic comparison implementation.
162 (range.start, range.end).cmp(&(label_start, label_end))
163 }) {
164 // If the ranges are the same, order the labels in reverse
165 // to how they were originally specified in the diagnostic.
166 // This helps with printing in the renderer.
167 Ok(index) | Err(index) => index,
168 };
169
170 line.single_labels
171 .insert(index, (label.style, label_start..label_end, &label.message));
172
173 // If this line is not rendered, the SingleLabel is not visible.
174 line.must_render = true;
175 } else {
176 // Multiple lines
177 //
178 // ```text
179 // 4 │ fizz₁ num = case (mod num 5) (mod num 3) of
180 // │ ╭─────────────^
181 // 5 │ │ 0 0 => "FizzBuzz"
182 // 6 │ │ 0 _ => "Fizz"
183 // 7 │ │ _ 0 => "Buzz"
184 // 8 │ │ _ _ => num
185 // │ ╰──────────────^ `case` clauses have incompatible types
186 // ```
187
188 let label_index = labeled_file.num_multi_labels;
189 labeled_file.num_multi_labels += 1;
190
191 // First labeled line
192 let label_start = label.range.start - start_line_range.start;
193
194 let start_line = labeled_file.get_or_insert_line(
195 start_line_index,
196 start_line_range.clone(),
197 start_line_number,
198 );
199
200 start_line.multi_labels.push((
201 label_index,
202 label.style,
203 MultiLabel::Top(label_start),
204 ));
205
206 // The first line has to be rendered so the start of the label is visible.
207 start_line.must_render = true;
208
209 // Marked lines
210 //
211 // ```text
212 // 5 │ │ 0 0 => "FizzBuzz"
213 // 6 │ │ 0 _ => "Fizz"
214 // 7 │ │ _ 0 => "Buzz"
215 // ```
216 for line_index in (start_line_index + 1)..end_line_index {
217 let line_range = files.line_range(label.file_id, line_index)?;
218 let line_number = files.line_number(label.file_id, line_index)?;
219
220 outer_padding = std::cmp::max(outer_padding, count_digits(line_number));
221
222 let line = labeled_file.get_or_insert_line(line_index, line_range, line_number);
223
224 line.multi_labels
225 .push((label_index, label.style, MultiLabel::Left));
226
227 // The line should be rendered to match the configuration of how much context to show.
228 line.must_render |=
229 // Is this line part of the context after the start of the label?
230 line_index - start_line_index <= self.config.start_context_lines
231 ||
232 // Is this line part of the context before the end of the label?
233 end_line_index - line_index <= self.config.end_context_lines;
234 }
235
236 // Last labeled line
237 //
238 // ```text
239 // 8 │ │ _ _ => num
240 // │ ╰──────────────^ `case` clauses have incompatible types
241 // ```
242 let label_end = label.range.end - end_line_range.start;
243
244 let end_line = labeled_file.get_or_insert_line(
245 end_line_index,
246 end_line_range,
247 end_line_number,
248 );
249
250 end_line.multi_labels.push((
251 label_index,
252 label.style,
253 MultiLabel::Bottom(label_end, &label.message),
254 ));
255
256 // The last line has to be rendered so the end of the label is visible.
257 end_line.must_render = true;
258 }
259 }
260
261 // Header and message
262 //
263 // ```text
264 // error[E0001]: unexpected type in `+` application
265 // ```
266 renderer.render_header(
267 None,
268 self.diagnostic.severity,
269 self.diagnostic.code.as_deref(),
270 self.diagnostic.message.as_str(),
271 )?;
272
273 // Source snippets
274 //
275 // ```text
276 // ┌─ test:2:9
277 // │
278 // 2 │ (+ test "")
279 // │ ^^ expected `Int` but found `String`
280 // │
281 // ```
282 let mut labeled_files = labeled_files.into_iter().peekable();
283 while let Some(labeled_file) = labeled_files.next() {
284 let source = files.source(labeled_file.file_id)?;
285 let source = source.as_ref();
286
287 // Top left border and locus.
288 //
289 // ```text
290 // ┌─ test:2:9
291 // ```
292 if !labeled_file.lines.is_empty() {
293 renderer.render_snippet_start(
294 outer_padding,
295 &Locus {
296 name: labeled_file.name,
297 location: labeled_file.location,
298 },
299 )?;
300 renderer.render_snippet_empty(
301 outer_padding,
302 self.diagnostic.severity,
303 labeled_file.num_multi_labels,
304 &[],
305 )?;
306 }
307
308 let mut lines = labeled_file
309 .lines
310 .iter()
311 .filter(|(_, line)| line.must_render)
312 .peekable();
313
314 while let Some((line_index, line)) = lines.next() {
315 renderer.render_snippet_source(
316 outer_padding,
317 line.number,
318 &source[line.range.clone()],
319 self.diagnostic.severity,
320 &line.single_labels,
321 labeled_file.num_multi_labels,
322 &line.multi_labels,
323 )?;
324
325 // Check to see if we need to render any intermediate stuff
326 // before rendering the next line.
327 if let Some((next_line_index, _)) = lines.peek() {
328 match next_line_index.checked_sub(*line_index) {
329 // Consecutive lines
330 Some(1) => {}
331 // One line between the current line and the next line
332 Some(2) => {
333 // Write a source line
334 let file_id = labeled_file.file_id;
335
336 // This line was not intended to be rendered initially.
337 // To render the line right, we have to get back the original labels.
338 let labels = labeled_file
339 .lines
340 .get(&(line_index + 1))
341 .map_or(&[][..], |line| &line.multi_labels[..]);
342
343 renderer.render_snippet_source(
344 outer_padding,
345 files.line_number(file_id, line_index + 1)?,
346 &source[files.line_range(file_id, line_index + 1)?],
347 self.diagnostic.severity,
348 &[],
349 labeled_file.num_multi_labels,
350 labels,
351 )?;
352 }
353 // More than one line between the current line and the next line.
354 Some(_) | None => {
355 // Source break
356 //
357 // ```text
358 // ·
359 // ```
360 renderer.render_snippet_break(
361 outer_padding,
362 self.diagnostic.severity,
363 labeled_file.num_multi_labels,
364 &line.multi_labels,
365 )?;
366 }
367 }
368 }
369 }
370
371 // Check to see if we should render a trailing border after the
372 // final line of the snippet.
373 if labeled_files.peek().is_none() && self.diagnostic.notes.is_empty() {
374 // We don't render a border if we are at the final newline
375 // without trailing notes, because it would end up looking too
376 // spaced-out in combination with the final new line.
377 } else {
378 // Render the trailing snippet border.
379 renderer.render_snippet_empty(
380 outer_padding,
381 self.diagnostic.severity,
382 labeled_file.num_multi_labels,
383 &[],
384 )?;
385 }
386 }
387
388 // Additional notes
389 //
390 // ```text
391 // = expected type `Int`
392 // found type `String`
393 // ```
394 for note in &self.diagnostic.notes {
395 renderer.render_snippet_note(outer_padding, note)?;
396 }
397 renderer.render_empty()
398 }
399}
400
401/// Output a short diagnostic, with a line number, severity, and message.
402pub struct ShortDiagnostic<'diagnostic, FileId> {
403 diagnostic: &'diagnostic Diagnostic<FileId>,
404 show_notes: bool,
405}
406
407impl<'diagnostic, FileId> ShortDiagnostic<'diagnostic, FileId>
408where
409 FileId: Copy + PartialEq,
410{
411 pub fn new(
412 diagnostic: &'diagnostic Diagnostic<FileId>,
413 show_notes: bool,
414 ) -> ShortDiagnostic<'diagnostic, FileId> {
415 ShortDiagnostic {
416 diagnostic,
417 show_notes,
418 }
419 }
420
421 pub fn render<'files>(
422 &self,
423 files: &'files impl Files<'files, FileId = FileId>,
424 renderer: &mut Renderer<'_, '_>,
425 ) -> Result<(), Error>
426 where
427 FileId: 'files,
428 {
429 // Located headers
430 //
431 // ```text
432 // test:2:9: error[E0001]: unexpected type in `+` application
433 // ```
434 let mut primary_labels_encountered = 0;
435 let labels = self.diagnostic.labels.iter();
436 for label in labels.filter(|label| label.style == LabelStyle::Primary) {
437 primary_labels_encountered += 1;
438
439 renderer.render_header(
440 Some(&Locus {
441 name: files.name(label.file_id)?.to_string(),
442 location: files.location(label.file_id, label.range.start)?,
443 }),
444 self.diagnostic.severity,
445 self.diagnostic.code.as_deref(),
446 self.diagnostic.message.as_str(),
447 )?;
448 }
449
450 // Fallback to printing a non-located header if no primary labels were encountered
451 //
452 // ```text
453 // error[E0002]: Bad config found
454 // ```
455 if primary_labels_encountered == 0 {
456 renderer.render_header(
457 None,
458 self.diagnostic.severity,
459 self.diagnostic.code.as_deref(),
460 self.diagnostic.message.as_str(),
461 )?;
462 }
463
464 if self.show_notes {
465 // Additional notes
466 //
467 // ```text
468 // = expected type `Int`
469 // found type `String`
470 // ```
471 for note in &self.diagnostic.notes {
472 renderer.render_snippet_note(0, note)?;
473 }
474 }
475
476 Ok(())
477 }
478}