rslint_errors/codespan/term/
renderer.rs

1use std::io::{self, Write};
2use std::ops::Range;
3use termcolor::{ColorSpec, WriteColor};
4
5use super::super::diagnostic::{LabelStyle, Note, Severity};
6use super::super::files::{Error, Location};
7use super::super::term::{Chars, Config, Styles};
8
9/// The 'location focus' of a source code snippet.
10pub struct Locus {
11    /// The user-facing name of the file.
12    pub name: String,
13    /// The location.
14    pub location: Location,
15}
16
17/// Single-line label, with an optional message.
18///
19/// ```text
20/// ^^^^^^^^^ blah blah
21/// ```
22pub type SingleLabel<'diagnostic> = (LabelStyle, Range<usize>, &'diagnostic str);
23
24/// A multi-line label to render.
25///
26/// Locations are relative to the start of where the source code is rendered.
27pub enum MultiLabel<'diagnostic> {
28    /// Multi-line label top.
29    /// The contained value indicates where the label starts.
30    ///
31    /// ```text
32    /// ╭────────────^
33    /// ```
34    ///
35    /// Can also be rendered at the beginning of the line
36    /// if there is only whitespace before the label starts.
37    ///
38    /// /// ```text
39    /// ╭
40    /// ```
41    Top(usize),
42    /// Left vertical labels for multi-line labels.
43    ///
44    /// ```text
45    /// │
46    /// ```
47    Left,
48    /// Multi-line label bottom, with an optional message.
49    /// The first value indicates where the label ends.
50    ///
51    /// ```text
52    /// ╰────────────^ blah blah
53    /// ```
54    Bottom(usize, &'diagnostic str),
55}
56
57#[derive(Copy, Clone)]
58enum VerticalBound {
59    Top,
60    Bottom,
61}
62
63type Underline = (LabelStyle, VerticalBound);
64
65/// A renderer of display list entries.
66///
67/// The following diagram gives an overview of each of the parts of the renderer's output:
68///
69/// ```text
70///                     ┌ outer gutter
71///                     │ ┌ left border
72///                     │ │ ┌ inner gutter
73///                     │ │ │   ┌─────────────────────────── source ─────────────────────────────┐
74///                     │ │ │   │                                                                │
75///                  ┌────────────────────────────────────────────────────────────────────────────
76///        header ── │ error[0001]: oh noes, a cupcake has occurred!
77/// snippet start ── │    ┌─ test:9:0
78/// snippet empty ── │    │
79///  snippet line ── │  9 │   ╭ Cupcake ipsum dolor. Sit amet marshmallow topping cheesecake
80///  snippet line ── │ 10 │   │ muffin. Halvah croissant candy canes bonbon candy. Apple pie jelly
81///                  │    │ ╭─│─────────^
82/// snippet break ── │    · │ │
83///  snippet line ── │ 33 │ │ │ Muffin danish chocolate soufflé pastry icing bonbon oat cake.
84///  snippet line ── │ 34 │ │ │ Powder cake jujubes oat cake. Lemon drops tootsie roll marshmallow
85///                  │    │ │ ╰─────────────────────────────^ blah blah
86/// snippet break ── │    · │
87///  snippet line ── │ 38 │ │   Brownie lemon drops chocolate jelly-o candy canes. Danish marzipan
88///  snippet line ── │ 39 │ │   jujubes soufflé carrot cake marshmallow tiramisu caramels candy canes.
89///                  │    │ │           ^^^^^^^^^^^^^^^^^^^ -------------------- blah blah
90///                  │    │ │           │
91///                  │    │ │           blah blah
92///                  │    │ │           note: this is a note
93///  snippet line ── │ 40 │ │   Fruitcake jelly-o danish toffee. Tootsie roll pastry cheesecake
94///  snippet line ── │ 41 │ │   soufflé marzipan. Chocolate bar oat cake jujubes lollipop pastry
95///  snippet line ── │ 42 │ │   cupcake. Candy canes cupcake toffee gingerbread candy canes muffin
96///                  │    │ │                                ^^^^^^^^^^^^^^^^^^ blah blah
97///                  │    │ ╰──────────^ blah blah
98/// snippet break ── │    ·
99///  snippet line ── │ 82 │     gingerbread toffee chupa chups chupa chups jelly-o cotton candy.
100///                  │    │                 ^^^^^^                         ------- blah blah
101/// snippet empty ── │    │
102///  snippet note ── │    = blah blah
103///  snippet note ── │    = blah blah blah
104///                  │      blah blah
105///  snippet note ── │    = blah blah blah
106///                  │      blah blah
107///         empty ── │
108/// ```
109///
110/// Filler text from http://www.cupcakeipsum.com
111pub struct Renderer<'writer, 'config> {
112    writer: &'writer mut dyn WriteColor,
113    config: &'config Config,
114}
115
116impl<'writer, 'config> Renderer<'writer, 'config> {
117    /// Construct a renderer from the given writer and config.
118    pub fn new(
119        writer: &'writer mut dyn WriteColor,
120        config: &'config Config,
121    ) -> Renderer<'writer, 'config> {
122        Renderer { writer, config }
123    }
124
125    fn chars(&self) -> &'config Chars {
126        &self.config.chars
127    }
128
129    fn styles(&self) -> &'config Styles {
130        &self.config.styles
131    }
132
133    /// Diagnostic header, with severity, code, and message.
134    ///
135    /// ```text
136    /// error[E0001]: unexpected type in `+` application
137    /// ```
138    pub fn render_header(
139        &mut self,
140        locus: Option<&Locus>,
141        severity: Severity,
142        code: Option<&str>,
143        message: &str,
144    ) -> Result<(), Error> {
145        // Write locus
146        //
147        // ```text
148        // test:2:9:
149        // ```
150        if let Some(locus) = locus {
151            self.snippet_locus(locus)?;
152            write!(self, ": ")?;
153        }
154
155        // Write severity name
156        //
157        // ```text
158        // error
159        // ```
160        self.set_color(self.styles().header(severity))?;
161        match severity {
162            Severity::Bug => write!(self, "bug")?,
163            Severity::Error => write!(self, "error")?,
164            Severity::Warning => write!(self, "warning")?,
165            Severity::Help => write!(self, "help")?,
166            Severity::Note => write!(self, "note")?,
167        }
168
169        // Write error code
170        //
171        // ```text
172        // [E0001]
173        // ```
174        if let Some(code) = &code.filter(|code| !code.is_empty()) {
175            write!(self, "[{}]", code)?;
176        }
177
178        // Write diagnostic message
179        //
180        // ```text
181        // : unexpected type in `+` application
182        // ```
183        self.set_color(&self.styles().header_message)?;
184        write!(self, ": {}", message)?;
185        self.reset()?;
186
187        writeln!(self)?;
188
189        Ok(())
190    }
191
192    /// Empty line.
193    pub fn render_empty(&mut self) -> Result<(), Error> {
194        writeln!(self)?;
195        Ok(())
196    }
197
198    /// Top left border and locus.
199    ///
200    /// ```text
201    /// ┌─ test:2:9
202    /// ```
203    pub fn render_snippet_start(
204        &mut self,
205        outer_padding: usize,
206        locus: &Locus,
207    ) -> Result<(), Error> {
208        self.outer_gutter(outer_padding)?;
209
210        self.set_color(&self.styles().source_border)?;
211        write!(self, "{}", self.chars().source_border_top_left)?;
212        write!(self, "{0}", self.chars().source_border_top)?;
213        self.reset()?;
214
215        write!(self, " ")?;
216        self.snippet_locus(&locus)?;
217
218        writeln!(self)?;
219
220        Ok(())
221    }
222
223    /// A line of source code.
224    ///
225    /// ```text
226    /// 10 │   │ muffin. Halvah croissant candy canes bonbon candy. Apple pie jelly
227    ///    │ ╭─│─────────^
228    /// ```
229    pub fn render_snippet_source(
230        &mut self,
231        outer_padding: usize,
232        line_number: usize,
233        source: &str,
234        severity: Severity,
235        single_labels: &[SingleLabel<'_>],
236        num_multi_labels: usize,
237        multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)],
238    ) -> Result<(), Error> {
239        // Trim trailing newlines, linefeeds, and null chars from source, if they exist.
240        // FIXME: Use the number of trimmed placeholders when rendering single line carets
241        let source = source.trim_end_matches(['\n', '\r', '\0'].as_ref());
242
243        // Write source line
244        //
245        // ```text
246        // 10 │   │ muffin. Halvah croissant candy canes bonbon candy. Apple pie jelly
247        // ```
248        {
249            // Write outer gutter (with line number) and border
250            self.outer_gutter_number(line_number, outer_padding)?;
251            self.border_left()?;
252
253            // Write inner gutter (with multi-line continuations on the left if necessary)
254            let mut multi_labels_iter = multi_labels.iter().peekable();
255            for label_column in 0..num_multi_labels {
256                match multi_labels_iter.peek() {
257                    Some((label_index, label_style, label)) if *label_index == label_column => {
258                        match label {
259                            MultiLabel::Top(start)
260                                if *start <= source.len() - source.trim_start().len() =>
261                            {
262                                self.label_multi_top_left(severity, *label_style)?;
263                            }
264                            MultiLabel::Top(..) => self.inner_gutter_space()?,
265                            MultiLabel::Left | MultiLabel::Bottom(..) => {
266                                self.label_multi_left(severity, *label_style, None)?;
267                            }
268                        }
269                        multi_labels_iter.next();
270                    }
271                    Some((_, _, _)) | None => self.inner_gutter_space()?,
272                }
273            }
274
275            // Write source text
276            write!(self, " ")?;
277            let mut in_primary = false;
278            for (metrics, ch) in self.char_metrics(source.char_indices()) {
279                let column_range = metrics.byte_index..(metrics.byte_index + ch.len_utf8());
280
281                // Check if we are overlapping a primary label
282                let is_primary = single_labels.iter().any(|(ls, range, _)| {
283                    *ls == LabelStyle::Primary && is_overlapping(range, &column_range)
284                }) || multi_labels.iter().any(|(_, ls, label)| {
285                    *ls == LabelStyle::Primary
286                        && match label {
287                            MultiLabel::Top(start) => column_range.start >= *start,
288                            MultiLabel::Left => true,
289                            MultiLabel::Bottom(start, _) => column_range.end <= *start,
290                        }
291                });
292
293                // Set the source color if we are in a primary label
294                if is_primary && !in_primary {
295                    self.set_color(self.styles().label(severity, LabelStyle::Primary))?;
296                    in_primary = true;
297                } else if !is_primary && in_primary {
298                    self.reset()?;
299                    in_primary = false;
300                }
301
302                match ch {
303                    '\t' => (0..metrics.unicode_width).try_for_each(|_| write!(self, " "))?,
304                    _ => write!(self, "{}", ch)?,
305                }
306            }
307            if in_primary {
308                self.reset()?;
309            }
310            writeln!(self)?;
311        }
312
313        // Write single labels underneath source
314        //
315        // ```text
316        //   │     - ---- ^^^ second mutable borrow occurs here
317        //   │     │ │
318        //   │     │ first mutable borrow occurs here
319        //   │     first borrow later used by call
320        //   │     help: some help here
321        // ```
322        if !single_labels.is_empty() {
323            // Our plan is as follows:
324            //
325            // 1. Do an initial scan to find:
326            //    - The number of non-empty messages.
327            //    - The right-most start and end positions of labels.
328            //    - A candidate for a trailing label (where the label's message
329            //      is printed to the left of the caret).
330            // 2. Check if the trailing label candidate overlaps another label -
331            //    if so we print it underneath the carets with the other labels.
332            // 3. Print a line of carets, and (possibly) the trailing message
333            //    to the left.
334            // 4. Print vertical lines pointing to the carets, and the messages
335            //    for those carets.
336            //
337            // We try our best avoid introducing new dynamic allocations,
338            // instead preferring to iterate over the labels multiple times. It
339            // is unclear what the performance tradeoffs are however, so further
340            // investigation may be required.
341
342            // The number of non-empty messages to print.
343            let mut num_messages = 0;
344            // The right-most start position, eg:
345            //
346            // ```text
347            // -^^^^---- ^^^^^^^
348            //           │
349            //           right-most start position
350            // ```
351            let mut max_label_start = 0;
352            // The right-most end position, eg:
353            //
354            // ```text
355            // -^^^^---- ^^^^^^^
356            //                 │
357            //                 right-most end position
358            // ```
359            let mut max_label_end = 0;
360            // A trailing message, eg:
361            //
362            // ```text
363            // ^^^ second mutable borrow occurs here
364            // ```
365            let mut trailing_label = None;
366
367            for (label_index, label) in single_labels.iter().enumerate() {
368                let (_, range, message) = label;
369                if !message.is_empty() {
370                    num_messages += 1;
371                }
372                max_label_start = std::cmp::max(max_label_start, range.start);
373                max_label_end = std::cmp::max(max_label_end, range.end);
374                // This is a candidate for the trailing label, so let's record it.
375                if range.end == max_label_end {
376                    if message.is_empty() {
377                        trailing_label = None;
378                    } else {
379                        trailing_label = Some((label_index, label));
380                    }
381                }
382            }
383            if let Some((trailing_label_index, (_, trailing_range, _))) = trailing_label {
384                // Check to see if the trailing label candidate overlaps any of
385                // the other labels on the current line.
386                if single_labels
387                    .iter()
388                    .enumerate()
389                    .filter(|(label_index, _)| *label_index != trailing_label_index)
390                    .any(|(_, (_, range, _))| is_overlapping(trailing_range, range))
391                {
392                    // If it does, we'll instead want to render it below the
393                    // carets along with the other hanging labels.
394                    trailing_label = None;
395                }
396            }
397
398            // Write a line of carets
399            //
400            // ```text
401            //   │ ^^^^^^  -------^^^^^^^^^-------^^^^^----- ^^^^ trailing label message
402            // ```
403            self.outer_gutter(outer_padding)?;
404            self.border_left()?;
405            self.inner_gutter(severity, num_multi_labels, multi_labels)?;
406            write!(self, " ")?;
407
408            let mut previous_label_style = None;
409            let placeholder_metrics = Metrics {
410                byte_index: source.len(),
411                unicode_width: 1,
412            };
413            for (metrics, ch) in self
414                .char_metrics(source.char_indices())
415                // Add a placeholder source column at the end to allow for
416                // printing carets at the end of lines, eg:
417                //
418                // ```text
419                // 1 │ Hello world!
420                //   │             ^
421                // ```
422                .chain(std::iter::once((placeholder_metrics, '\0')))
423            {
424                // Find the current label style at this column
425                let column_range = metrics.byte_index..(metrics.byte_index + ch.len_utf8());
426                let current_label_style = single_labels
427                    .iter()
428                    .filter(|(_, range, _)| is_overlapping(range, &column_range))
429                    .map(|(label_style, _, _)| *label_style)
430                    .max_by_key(label_priority_key);
431
432                // Update writer style if necessary
433                if previous_label_style != current_label_style {
434                    match current_label_style {
435                        None => self.reset()?,
436                        Some(label_style) => {
437                            self.set_color(self.styles().label(severity, label_style))?;
438                        }
439                    }
440                }
441
442                let caret_ch = match current_label_style {
443                    Some(LabelStyle::Primary) => Some(self.chars().single_primary_caret),
444                    Some(LabelStyle::Secondary) => Some(self.chars().single_secondary_caret),
445                    // Only print padding if we are before the end of the last single line caret
446                    None if metrics.byte_index < max_label_end => Some(' '),
447                    None => None,
448                };
449                if let Some(caret_ch) = caret_ch {
450                    // FIXME: improve rendering of carets between character boundaries
451                    (0..metrics.unicode_width).try_for_each(|_| write!(self, "{}", caret_ch))?;
452                }
453
454                previous_label_style = current_label_style;
455            }
456            // Reset style if it was previously set
457            if previous_label_style.is_some() {
458                self.reset()?;
459            }
460            // Write first trailing label message
461            if let Some((_, (label_style, _, message))) = trailing_label {
462                write!(self, " ")?;
463                self.set_color(self.styles().label(severity, *label_style))?;
464                write!(self, "{}", message)?;
465                self.reset()?;
466            }
467            writeln!(self)?;
468
469            // Write hanging labels pointing to carets
470            //
471            // ```text
472            //   │     │ │
473            //   │     │ first mutable borrow occurs here
474            //   │     first borrow later used by call
475            //   │     help: some help here
476            // ```
477            if num_messages > trailing_label.iter().count() {
478                // Write first set of vertical lines before hanging labels
479                //
480                // ```text
481                //   │     │ │
482                // ```
483                self.outer_gutter(outer_padding)?;
484                self.border_left()?;
485                self.inner_gutter(severity, num_multi_labels, multi_labels)?;
486                write!(self, " ")?;
487                self.caret_pointers(
488                    severity,
489                    max_label_start,
490                    single_labels,
491                    trailing_label,
492                    source.char_indices(),
493                )?;
494                writeln!(self)?;
495
496                // Write hanging labels pointing to carets
497                //
498                // ```text
499                //   │     │ first mutable borrow occurs here
500                //   │     first borrow later used by call
501                //   │     help: some help here
502                // ```
503                for (label_style, range, message) in
504                    hanging_labels(single_labels, trailing_label).rev()
505                {
506                    self.outer_gutter(outer_padding)?;
507                    self.border_left()?;
508                    self.inner_gutter(severity, num_multi_labels, multi_labels)?;
509                    write!(self, " ")?;
510                    self.caret_pointers(
511                        severity,
512                        max_label_start,
513                        single_labels,
514                        trailing_label,
515                        source
516                            .char_indices()
517                            .take_while(|(byte_index, _)| *byte_index < range.start),
518                    )?;
519                    self.set_color(self.styles().label(severity, *label_style))?;
520                    write!(self, "{}", message)?;
521                    self.reset()?;
522                    writeln!(self)?;
523                }
524            }
525        }
526
527        // Write top or bottom label carets underneath source
528        //
529        // ```text
530        //     │ ╰───│──────────────────^ woops
531        //     │   ╭─│─────────^
532        // ```
533        for (multi_label_index, (_, label_style, label)) in multi_labels.iter().enumerate() {
534            let (label_style, range, bottom_message) = match label {
535                MultiLabel::Left => continue, // no label caret needed
536                // no label caret needed if this can be started in front of the line
537                MultiLabel::Top(start) if *start <= source.len() - source.trim_start().len() => {
538                    continue
539                }
540                MultiLabel::Top(range) => (*label_style, range, None),
541                MultiLabel::Bottom(range, message) => (*label_style, range, Some(message)),
542            };
543
544            self.outer_gutter(outer_padding)?;
545            self.border_left()?;
546
547            // Write inner gutter.
548            //
549            // ```text
550            //  │ ╭─│───│
551            // ```
552            let mut underline = None;
553            let mut multi_labels_iter = multi_labels.iter().enumerate().peekable();
554            for label_column in 0..num_multi_labels {
555                match multi_labels_iter.peek() {
556                    Some((i, (label_index, ls, label))) if *label_index == label_column => {
557                        match label {
558                            MultiLabel::Left => {
559                                self.label_multi_left(severity, *ls, underline.map(|(s, _)| s))?;
560                            }
561                            MultiLabel::Top(..) if multi_label_index > *i => {
562                                self.label_multi_left(severity, *ls, underline.map(|(s, _)| s))?;
563                            }
564                            MultiLabel::Bottom(..) if multi_label_index < *i => {
565                                self.label_multi_left(severity, *ls, underline.map(|(s, _)| s))?;
566                            }
567                            MultiLabel::Top(..) if multi_label_index == *i => {
568                                underline = Some((*ls, VerticalBound::Top));
569                                self.label_multi_top_left(severity, label_style)?
570                            }
571                            MultiLabel::Bottom(..) if multi_label_index == *i => {
572                                underline = Some((*ls, VerticalBound::Bottom));
573                                self.label_multi_bottom_left(severity, label_style)?;
574                            }
575                            MultiLabel::Top(..) | MultiLabel::Bottom(..) => {
576                                self.inner_gutter_column(severity, underline)?;
577                            }
578                        }
579                        multi_labels_iter.next();
580                    }
581                    Some((_, _)) | None => self.inner_gutter_column(severity, underline)?,
582                }
583            }
584
585            // Finish the top or bottom caret
586            match bottom_message {
587                None => self.label_multi_top_caret(severity, label_style, source, *range)?,
588                Some(message) => {
589                    self.label_multi_bottom_caret(severity, label_style, source, *range, message)?
590                }
591            }
592        }
593
594        Ok(())
595    }
596
597    /// An empty source line, for providing additional whitespace to source snippets.
598    ///
599    /// ```text
600    /// │ │ │
601    /// ```
602    pub fn render_snippet_empty(
603        &mut self,
604        outer_padding: usize,
605        severity: Severity,
606        num_multi_labels: usize,
607        multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)],
608    ) -> Result<(), Error> {
609        self.outer_gutter(outer_padding)?;
610        self.border_left()?;
611        self.inner_gutter(severity, num_multi_labels, multi_labels)?;
612        writeln!(self)?;
613        Ok(())
614    }
615
616    /// A broken source line, for labeling skipped sections of source.
617    ///
618    /// ```text
619    /// · │ │
620    /// ```
621    pub fn render_snippet_break(
622        &mut self,
623        outer_padding: usize,
624        severity: Severity,
625        num_multi_labels: usize,
626        multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)],
627    ) -> Result<(), Error> {
628        self.outer_gutter(outer_padding)?;
629        self.border_left_break()?;
630        self.inner_gutter(severity, num_multi_labels, multi_labels)?;
631        writeln!(self)?;
632        Ok(())
633    }
634
635    /// Additional notes.
636    ///
637    /// ```text
638    /// = expected type `Int`
639    ///      found type `String`
640    /// ```
641    pub fn render_snippet_note(
642        &mut self,
643        outer_padding: usize,
644        note: Note,
645        has_another_note_after: bool,
646    ) -> Result<(), Error> {
647        let mut offset = 0;
648        for (note_line_index, line) in note.message.lines().enumerate() {
649            match note_line_index {
650                0 => {
651                    self.outer_gutter(outer_padding)?;
652                    self.set_color(&self.styles().note_bullet)?;
653                    write!(
654                        self,
655                        "{}",
656                        if !has_another_note_after {
657                            self.chars().note_bullet_end
658                        } else {
659                            self.chars().note_bullet_middle
660                        }
661                    )?;
662                    self.reset()?;
663                    if let Some(severity) = note.severity {
664                        write!(self, " ");
665                        if severity == Severity::Note {
666                            self.set_color(&self.styles().header_help)?;
667                        } else {
668                            self.set_color(&self.styles().header(severity))?;
669                        }
670                        let string = match severity {
671                            Severity::Bug => "bug",
672                            Severity::Error => "error",
673                            Severity::Warning => "warning",
674                            Severity::Help => "help",
675                            Severity::Note => "note",
676                        };
677                        offset += string.len();
678                        write!(self, "{}", string)?;
679                        self.reset()?;
680                        write!(self, ":")?;
681                        offset += 1;
682                    }
683                }
684                _ => {
685                    if has_another_note_after {
686                        // write a gutter between a new line and the next note bullet if there is another note
687                        // so its not disjointed
688                        // ```
689                        // = help: foo bar baz
690                        // |         bar
691                        // = note: foo
692                        ///```
693                        self.outer_gutter(outer_padding)?;
694                        self.border_left()?;
695                    } else {
696                        self.outer_gutter(outer_padding)?;
697                    }
698                    write!(self, " {}", " ".repeat(offset))?
699                }
700            }
701            // Write line of message
702            writeln!(self, " {}", line)?;
703        }
704
705        Ok(())
706    }
707
708    /// Adds tab-stop aware unicode-width computations to an iterator over
709    /// character indices. Assumes that the character indices begin at the start
710    /// of the line.
711    fn char_metrics(
712        &self,
713        char_indices: impl Iterator<Item = (usize, char)>,
714    ) -> impl Iterator<Item = (Metrics, char)> {
715        use unicode_width::UnicodeWidthChar;
716
717        let tab_width = self.config.tab_width;
718        let mut unicode_column = 0;
719
720        char_indices.map(move |(byte_index, ch)| {
721            let metrics = Metrics {
722                byte_index,
723                unicode_width: match (ch, tab_width) {
724                    ('\t', 0) => 0, // Guard divide-by-zero
725                    ('\t', _) => tab_width - (unicode_column % tab_width),
726                    (ch, _) => ch.width().unwrap_or(0),
727                },
728            };
729            unicode_column += metrics.unicode_width;
730
731            (metrics, ch)
732        })
733    }
734
735    /// Location focus.
736    fn snippet_locus(&mut self, locus: &Locus) -> Result<(), Error> {
737        write!(
738            self,
739            "{name}:{line_number}:{column_number}",
740            name = locus.name,
741            line_number = locus.location.line_number,
742            column_number = locus.location.column_number,
743        )?;
744        Ok(())
745    }
746
747    /// The outer gutter of a source line.
748    fn outer_gutter(&mut self, outer_padding: usize) -> Result<(), Error> {
749        write!(self, "{space: >width$} ", space = "", width = outer_padding)?;
750        Ok(())
751    }
752
753    /// The outer gutter of a source line, with line number.
754    fn outer_gutter_number(
755        &mut self,
756        line_number: usize,
757        outer_padding: usize,
758    ) -> Result<(), Error> {
759        self.set_color(&self.styles().line_number)?;
760        write!(
761            self,
762            "{line_number: >width$}",
763            line_number = line_number,
764            width = outer_padding,
765        )?;
766        self.reset()?;
767        write!(self, " ")?;
768        Ok(())
769    }
770
771    /// The left-hand border of a source line.
772    fn border_left(&mut self) -> Result<(), Error> {
773        self.set_color(&self.styles().source_border)?;
774        write!(self, "{}", self.chars().source_border_left)?;
775        self.reset()?;
776        Ok(())
777    }
778
779    /// The broken left-hand border of a source line.
780    fn border_left_break(&mut self) -> Result<(), Error> {
781        self.set_color(&self.styles().source_border)?;
782        write!(self, "{}", self.chars().source_border_left_break)?;
783        self.reset()?;
784        Ok(())
785    }
786
787    /// Write vertical lines pointing to carets.
788    fn caret_pointers(
789        &mut self,
790        severity: Severity,
791        max_label_start: usize,
792        single_labels: &[SingleLabel<'_>],
793        trailing_label: Option<(usize, &SingleLabel<'_>)>,
794        char_indices: impl Iterator<Item = (usize, char)>,
795    ) -> Result<(), Error> {
796        for (metrics, ch) in self.char_metrics(char_indices) {
797            let column_range = metrics.byte_index..(metrics.byte_index + ch.len_utf8());
798            let label_style = hanging_labels(single_labels, trailing_label)
799                .filter(|(_, range, _)| column_range.contains(&range.start))
800                .map(|(label_style, _, _)| *label_style)
801                .max_by_key(label_priority_key);
802
803            let mut spaces = match label_style {
804                None => 0..metrics.unicode_width,
805                Some(label_style) => {
806                    self.set_color(self.styles().label(severity, label_style))?;
807                    write!(self, "{}", self.chars().pointer_left)?;
808                    self.reset()?;
809                    1..metrics.unicode_width
810                }
811            };
812            // Only print padding if we are before the end of the last single line caret
813            if metrics.byte_index <= max_label_start {
814                spaces.try_for_each(|_| write!(self, " "))?;
815            }
816        }
817
818        Ok(())
819    }
820
821    /// The left of a multi-line label.
822    ///
823    /// ```text
824    ///  │
825    /// ```
826    fn label_multi_left(
827        &mut self,
828        severity: Severity,
829        label_style: LabelStyle,
830        underline: Option<LabelStyle>,
831    ) -> Result<(), Error> {
832        match underline {
833            None => write!(self, " ")?,
834            // Continue an underline horizontally
835            Some(label_style) => {
836                self.set_color(self.styles().label(severity, label_style))?;
837                write!(self, "{}", self.chars().multi_top)?;
838                self.reset()?;
839            }
840        }
841        self.set_color(self.styles().label(severity, label_style))?;
842        write!(self, "{}", self.chars().multi_left)?;
843        self.reset()?;
844        Ok(())
845    }
846
847    /// The top-left of a multi-line label.
848    ///
849    /// ```text
850    ///  ╭
851    /// ```
852    fn label_multi_top_left(
853        &mut self,
854        severity: Severity,
855        label_style: LabelStyle,
856    ) -> Result<(), Error> {
857        write!(self, " ")?;
858        self.set_color(self.styles().label(severity, label_style))?;
859        write!(self, "{}", self.chars().multi_top_left)?;
860        self.reset()?;
861        Ok(())
862    }
863
864    /// The bottom left of a multi-line label.
865    ///
866    /// ```text
867    ///  ╰
868    /// ```
869    fn label_multi_bottom_left(
870        &mut self,
871        severity: Severity,
872        label_style: LabelStyle,
873    ) -> Result<(), Error> {
874        write!(self, " ")?;
875        self.set_color(self.styles().label(severity, label_style))?;
876        write!(self, "{}", self.chars().multi_bottom_left)?;
877        self.reset()?;
878        Ok(())
879    }
880
881    /// Multi-line label top.
882    ///
883    /// ```text
884    /// ─────────────^
885    /// ```
886    fn label_multi_top_caret(
887        &mut self,
888        severity: Severity,
889        label_style: LabelStyle,
890        source: &str,
891        start: usize,
892    ) -> Result<(), Error> {
893        self.set_color(self.styles().label(severity, label_style))?;
894
895        for (metrics, _) in self
896            .char_metrics(source.char_indices())
897            .take_while(|(metrics, _)| metrics.byte_index < start + 1)
898        {
899            // FIXME: improve rendering of carets between character boundaries
900            (0..metrics.unicode_width)
901                .try_for_each(|_| write!(self, "{}", self.chars().multi_top))?;
902        }
903
904        let caret_start = match label_style {
905            LabelStyle::Primary => self.config.chars.multi_primary_caret_start,
906            LabelStyle::Secondary => self.config.chars.multi_secondary_caret_start,
907        };
908        write!(self, "{}", caret_start)?;
909        self.reset()?;
910        writeln!(self)?;
911        Ok(())
912    }
913
914    /// Multi-line label bottom, with a message.
915    ///
916    /// ```text
917    /// ─────────────^ expected `Int` but found `String`
918    /// ```
919    fn label_multi_bottom_caret(
920        &mut self,
921        severity: Severity,
922        label_style: LabelStyle,
923        source: &str,
924        start: usize,
925        message: &str,
926    ) -> Result<(), Error> {
927        self.set_color(self.styles().label(severity, label_style))?;
928
929        for (metrics, _) in self
930            .char_metrics(source.char_indices())
931            .take_while(|(metrics, _)| metrics.byte_index < start)
932        {
933            // FIXME: improve rendering of carets between character boundaries
934            (0..metrics.unicode_width)
935                .try_for_each(|_| write!(self, "{}", self.chars().multi_bottom))?;
936        }
937
938        let caret_end = match label_style {
939            LabelStyle::Primary => self.config.chars.multi_primary_caret_start,
940            LabelStyle::Secondary => self.config.chars.multi_secondary_caret_start,
941        };
942        write!(self, "{}", caret_end)?;
943        if !message.is_empty() {
944            write!(self, " {}", message)?;
945        }
946        self.reset()?;
947        writeln!(self)?;
948        Ok(())
949    }
950
951    /// Writes an empty gutter space, or continues an underline horizontally.
952    fn inner_gutter_column(
953        &mut self,
954        severity: Severity,
955        underline: Option<Underline>,
956    ) -> Result<(), Error> {
957        match underline {
958            None => self.inner_gutter_space(),
959            Some((label_style, vertical_bound)) => {
960                self.set_color(self.styles().label(severity, label_style))?;
961                let ch = match vertical_bound {
962                    VerticalBound::Top => self.config.chars.multi_top,
963                    VerticalBound::Bottom => self.config.chars.multi_bottom,
964                };
965                write!(self, "{0}{0}", ch)?;
966                self.reset()?;
967                Ok(())
968            }
969        }
970    }
971
972    /// Writes an empty gutter space.
973    fn inner_gutter_space(&mut self) -> Result<(), Error> {
974        write!(self, "  ")?;
975        Ok(())
976    }
977
978    /// Writes an inner gutter, with the left lines if necessary.
979    fn inner_gutter(
980        &mut self,
981        severity: Severity,
982        num_multi_labels: usize,
983        multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)],
984    ) -> Result<(), Error> {
985        let mut multi_labels_iter = multi_labels.iter().peekable();
986        for label_column in 0..num_multi_labels {
987            match multi_labels_iter.peek() {
988                Some((label_index, ls, label)) if *label_index == label_column => match label {
989                    MultiLabel::Left | MultiLabel::Bottom(..) => {
990                        self.label_multi_left(severity, *ls, None)?;
991                        multi_labels_iter.next();
992                    }
993                    MultiLabel::Top(..) => {
994                        self.inner_gutter_space()?;
995                        multi_labels_iter.next();
996                    }
997                },
998                Some((_, _, _)) | None => self.inner_gutter_space()?,
999            }
1000        }
1001
1002        Ok(())
1003    }
1004}
1005
1006impl<'writer, 'config> Write for Renderer<'writer, 'config> {
1007    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
1008        self.writer.write(buf)
1009    }
1010
1011    fn flush(&mut self) -> io::Result<()> {
1012        self.writer.flush()
1013    }
1014}
1015
1016impl<'writer, 'config> WriteColor for Renderer<'writer, 'config> {
1017    fn supports_color(&self) -> bool {
1018        self.writer.supports_color()
1019    }
1020
1021    fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
1022        self.writer.set_color(spec)
1023    }
1024
1025    fn reset(&mut self) -> io::Result<()> {
1026        self.writer.reset()
1027    }
1028
1029    fn is_synchronous(&self) -> bool {
1030        self.writer.is_synchronous()
1031    }
1032}
1033
1034struct Metrics {
1035    byte_index: usize,
1036    unicode_width: usize,
1037}
1038
1039/// Check if two ranges overlap
1040fn is_overlapping(range0: &Range<usize>, range1: &Range<usize>) -> bool {
1041    let start = std::cmp::max(range0.start, range1.start);
1042    let end = std::cmp::min(range0.end, range1.end);
1043    start < end
1044}
1045
1046/// For prioritizing primary labels over secondary labels when rendering carets.
1047fn label_priority_key(label_style: &LabelStyle) -> u8 {
1048    match label_style {
1049        LabelStyle::Secondary => 0,
1050        LabelStyle::Primary => 1,
1051    }
1052}
1053
1054/// Return an iterator that yields the labels that require hanging messages
1055/// rendered underneath them.
1056fn hanging_labels<'labels, 'diagnostic>(
1057    single_labels: &'labels [SingleLabel<'diagnostic>],
1058    trailing_label: Option<(usize, &'labels SingleLabel<'diagnostic>)>,
1059) -> impl 'labels + DoubleEndedIterator<Item = &'labels SingleLabel<'diagnostic>> {
1060    single_labels
1061        .iter()
1062        .enumerate()
1063        .filter(|(_, (_, _, message))| !message.is_empty())
1064        .filter(move |(i, _)| trailing_label.map_or(true, |(j, _)| *i != j))
1065        .map(|(_, label)| label)
1066}