1use std::io::{self, Write};
2use std::ops::Range;
3use termcolor::{ColorSpec, WriteColor};
4
5use crate::diagnostic::{LabelStyle, Severity};
6use crate::files::{Error, Location};
7use crate::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        message: &str,
645    ) -> Result<(), Error> {
646        for (note_line_index, line) in message.lines().enumerate() {
647            self.outer_gutter(outer_padding)?;
648            match note_line_index {
649                0 => {
650                    self.set_color(&self.styles().note_bullet)?;
651                    write!(self, "{}", self.chars().note_bullet)?;
652                    self.reset()?;
653                }
654                _ => write!(self, " ")?,
655            }
656            // Write line of message
657            writeln!(self, " {}", line)?;
658        }
659
660        Ok(())
661    }
662
663    /// Adds tab-stop aware unicode-width computations to an iterator over
664    /// character indices. Assumes that the character indices begin at the start
665    /// of the line.
666    fn char_metrics(
667        &self,
668        char_indices: impl Iterator<Item = (usize, char)>,
669    ) -> impl Iterator<Item = (Metrics, char)> {
670        use unicode_width::UnicodeWidthChar;
671
672        let tab_width = self.config.tab_width;
673        let mut unicode_column = 0;
674
675        char_indices.map(move |(byte_index, ch)| {
676            let metrics = Metrics {
677                byte_index,
678                unicode_width: match (ch, tab_width) {
679                    ('\t', 0) => 0, // Guard divide-by-zero
680                    ('\t', _) => tab_width - (unicode_column % tab_width),
681                    (ch, _) => ch.width().unwrap_or(0),
682                },
683            };
684            unicode_column += metrics.unicode_width;
685
686            (metrics, ch)
687        })
688    }
689
690    /// Location focus.
691    fn snippet_locus(&mut self, locus: &Locus) -> Result<(), Error> {
692        write!(
693            self,
694            "{name}:{line_number}:{column_number}",
695            name = locus.name,
696            line_number = locus.location.line_number,
697            column_number = locus.location.column_number,
698        )?;
699        Ok(())
700    }
701
702    /// The outer gutter of a source line.
703    fn outer_gutter(&mut self, outer_padding: usize) -> Result<(), Error> {
704        write!(self, "{space: >width$} ", space = "", width = outer_padding)?;
705        Ok(())
706    }
707
708    /// The outer gutter of a source line, with line number.
709    fn outer_gutter_number(
710        &mut self,
711        line_number: usize,
712        outer_padding: usize,
713    ) -> Result<(), Error> {
714        self.set_color(&self.styles().line_number)?;
715        write!(
716            self,
717            "{line_number: >width$}",
718            line_number = line_number,
719            width = outer_padding,
720        )?;
721        self.reset()?;
722        write!(self, " ")?;
723        Ok(())
724    }
725
726    /// The left-hand border of a source line.
727    fn border_left(&mut self) -> Result<(), Error> {
728        self.set_color(&self.styles().source_border)?;
729        write!(self, "{}", self.chars().source_border_left)?;
730        self.reset()?;
731        Ok(())
732    }
733
734    /// The broken left-hand border of a source line.
735    fn border_left_break(&mut self) -> Result<(), Error> {
736        self.set_color(&self.styles().source_border)?;
737        write!(self, "{}", self.chars().source_border_left_break)?;
738        self.reset()?;
739        Ok(())
740    }
741
742    /// Write vertical lines pointing to carets.
743    fn caret_pointers(
744        &mut self,
745        severity: Severity,
746        max_label_start: usize,
747        single_labels: &[SingleLabel<'_>],
748        trailing_label: Option<(usize, &SingleLabel<'_>)>,
749        char_indices: impl Iterator<Item = (usize, char)>,
750    ) -> Result<(), Error> {
751        for (metrics, ch) in self.char_metrics(char_indices) {
752            let column_range = metrics.byte_index..(metrics.byte_index + ch.len_utf8());
753            let label_style = hanging_labels(single_labels, trailing_label)
754                .filter(|(_, range, _)| column_range.contains(&range.start))
755                .map(|(label_style, _, _)| *label_style)
756                .max_by_key(label_priority_key);
757
758            let mut spaces = match label_style {
759                None => 0..metrics.unicode_width,
760                Some(label_style) => {
761                    self.set_color(self.styles().label(severity, label_style))?;
762                    write!(self, "{}", self.chars().pointer_left)?;
763                    self.reset()?;
764                    1..metrics.unicode_width
765                }
766            };
767            // Only print padding if we are before the end of the last single line caret
768            if metrics.byte_index <= max_label_start {
769                spaces.try_for_each(|_| write!(self, " "))?;
770            }
771        }
772
773        Ok(())
774    }
775
776    /// The left of a multi-line label.
777    ///
778    /// ```text
779    ///  │
780    /// ```
781    fn label_multi_left(
782        &mut self,
783        severity: Severity,
784        label_style: LabelStyle,
785        underline: Option<LabelStyle>,
786    ) -> Result<(), Error> {
787        match underline {
788            None => write!(self, " ")?,
789            // Continue an underline horizontally
790            Some(label_style) => {
791                self.set_color(self.styles().label(severity, label_style))?;
792                write!(self, "{}", self.chars().multi_top)?;
793                self.reset()?;
794            }
795        }
796        self.set_color(self.styles().label(severity, label_style))?;
797        write!(self, "{}", self.chars().multi_left)?;
798        self.reset()?;
799        Ok(())
800    }
801
802    /// The top-left of a multi-line label.
803    ///
804    /// ```text
805    ///  ╭
806    /// ```
807    fn label_multi_top_left(
808        &mut self,
809        severity: Severity,
810        label_style: LabelStyle,
811    ) -> Result<(), Error> {
812        write!(self, " ")?;
813        self.set_color(self.styles().label(severity, label_style))?;
814        write!(self, "{}", self.chars().multi_top_left)?;
815        self.reset()?;
816        Ok(())
817    }
818
819    /// The bottom left of a multi-line label.
820    ///
821    /// ```text
822    ///  ╰
823    /// ```
824    fn label_multi_bottom_left(
825        &mut self,
826        severity: Severity,
827        label_style: LabelStyle,
828    ) -> Result<(), Error> {
829        write!(self, " ")?;
830        self.set_color(self.styles().label(severity, label_style))?;
831        write!(self, "{}", self.chars().multi_bottom_left)?;
832        self.reset()?;
833        Ok(())
834    }
835
836    /// Multi-line label top.
837    ///
838    /// ```text
839    /// ─────────────^
840    /// ```
841    fn label_multi_top_caret(
842        &mut self,
843        severity: Severity,
844        label_style: LabelStyle,
845        source: &str,
846        start: usize,
847    ) -> Result<(), Error> {
848        self.set_color(self.styles().label(severity, label_style))?;
849
850        for (metrics, _) in self
851            .char_metrics(source.char_indices())
852            .take_while(|(metrics, _)| metrics.byte_index < start + 1)
853        {
854            // FIXME: improve rendering of carets between character boundaries
855            (0..metrics.unicode_width)
856                .try_for_each(|_| write!(self, "{}", self.chars().multi_top))?;
857        }
858
859        let caret_start = match label_style {
860            LabelStyle::Primary => self.config.chars.multi_primary_caret_start,
861            LabelStyle::Secondary => self.config.chars.multi_secondary_caret_start,
862        };
863        write!(self, "{}", caret_start)?;
864        self.reset()?;
865        writeln!(self)?;
866        Ok(())
867    }
868
869    /// Multi-line label bottom, with a message.
870    ///
871    /// ```text
872    /// ─────────────^ expected `Int` but found `String`
873    /// ```
874    fn label_multi_bottom_caret(
875        &mut self,
876        severity: Severity,
877        label_style: LabelStyle,
878        source: &str,
879        start: usize,
880        message: &str,
881    ) -> Result<(), Error> {
882        self.set_color(self.styles().label(severity, label_style))?;
883
884        for (metrics, _) in self
885            .char_metrics(source.char_indices())
886            .take_while(|(metrics, _)| metrics.byte_index < start)
887        {
888            // FIXME: improve rendering of carets between character boundaries
889            (0..metrics.unicode_width)
890                .try_for_each(|_| write!(self, "{}", self.chars().multi_bottom))?;
891        }
892
893        let caret_end = match label_style {
894            LabelStyle::Primary => self.config.chars.multi_primary_caret_start,
895            LabelStyle::Secondary => self.config.chars.multi_secondary_caret_start,
896        };
897        write!(self, "{}", caret_end)?;
898        if !message.is_empty() {
899            write!(self, " {}", message)?;
900        }
901        self.reset()?;
902        writeln!(self)?;
903        Ok(())
904    }
905
906    /// Writes an empty gutter space, or continues an underline horizontally.
907    fn inner_gutter_column(
908        &mut self,
909        severity: Severity,
910        underline: Option<Underline>,
911    ) -> Result<(), Error> {
912        match underline {
913            None => self.inner_gutter_space(),
914            Some((label_style, vertical_bound)) => {
915                self.set_color(self.styles().label(severity, label_style))?;
916                let ch = match vertical_bound {
917                    VerticalBound::Top => self.config.chars.multi_top,
918                    VerticalBound::Bottom => self.config.chars.multi_bottom,
919                };
920                write!(self, "{0}{0}", ch)?;
921                self.reset()?;
922                Ok(())
923            }
924        }
925    }
926
927    /// Writes an empty gutter space.
928    fn inner_gutter_space(&mut self) -> Result<(), Error> {
929        write!(self, "  ")?;
930        Ok(())
931    }
932
933    /// Writes an inner gutter, with the left lines if necessary.
934    fn inner_gutter(
935        &mut self,
936        severity: Severity,
937        num_multi_labels: usize,
938        multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)],
939    ) -> Result<(), Error> {
940        let mut multi_labels_iter = multi_labels.iter().peekable();
941        for label_column in 0..num_multi_labels {
942            match multi_labels_iter.peek() {
943                Some((label_index, ls, label)) if *label_index == label_column => match label {
944                    MultiLabel::Left | MultiLabel::Bottom(..) => {
945                        self.label_multi_left(severity, *ls, None)?;
946                        multi_labels_iter.next();
947                    }
948                    MultiLabel::Top(..) => {
949                        self.inner_gutter_space()?;
950                        multi_labels_iter.next();
951                    }
952                },
953                Some((_, _, _)) | None => self.inner_gutter_space()?,
954            }
955        }
956
957        Ok(())
958    }
959}
960
961impl<'writer, 'config> Write for Renderer<'writer, 'config> {
962    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
963        self.writer.write(buf)
964    }
965
966    fn flush(&mut self) -> io::Result<()> {
967        self.writer.flush()
968    }
969}
970
971impl<'writer, 'config> WriteColor for Renderer<'writer, 'config> {
972    fn supports_color(&self) -> bool {
973        self.writer.supports_color()
974    }
975
976    fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
977        self.writer.set_color(spec)
978    }
979
980    fn reset(&mut self) -> io::Result<()> {
981        self.writer.reset()
982    }
983
984    fn is_synchronous(&self) -> bool {
985        self.writer.is_synchronous()
986    }
987}
988
989struct Metrics {
990    byte_index: usize,
991    unicode_width: usize,
992}
993
994/// Check if two ranges overlap
995fn is_overlapping(range0: &Range<usize>, range1: &Range<usize>) -> bool {
996    let start = std::cmp::max(range0.start, range1.start);
997    let end = std::cmp::min(range0.end, range1.end);
998    start < end
999}
1000
1001/// For prioritizing primary labels over secondary labels when rendering carets.
1002fn label_priority_key(label_style: &LabelStyle) -> u8 {
1003    match label_style {
1004        LabelStyle::Secondary => 0,
1005        LabelStyle::Primary => 1,
1006    }
1007}
1008
1009/// Return an iterator that yields the labels that require hanging messages
1010/// rendered underneath them.
1011fn hanging_labels<'labels, 'diagnostic>(
1012    single_labels: &'labels [SingleLabel<'diagnostic>],
1013    trailing_label: Option<(usize, &'labels SingleLabel<'diagnostic>)>,
1014) -> impl 'labels + DoubleEndedIterator<Item = &'labels SingleLabel<'diagnostic>> {
1015    single_labels
1016        .iter()
1017        .enumerate()
1018        .filter(|(_, (_, _, message))| !message.is_empty())
1019        .filter(move |(i, _)| trailing_label.map_or(true, |(j, _)| *i != j))
1020        .map(|(_, label)| label)
1021}
1022