codespan_reporting/term/
renderer.rs

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