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}