codespan_reporting/term/
views.rs

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