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}