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