1 use std::io::{self, Write};
2 use std::ops::Range;
3 use termcolor::{ColorSpec, WriteColor};
4 
5 use crate::diagnostic::{LabelStyle, Severity};
6 use crate::files::{Error, Location};
7 use crate::term::{Chars, Config, Styles};
8 
9 /// The 'location focus' of a source code snippet.
10 pub 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 /// ```
22 pub 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.
27 pub 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)]
58 enum VerticalBound {
59     Top,
60     Bottom,
61 }
62 
63 type 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
111 pub struct Renderer<'writer, 'config> {
112     writer: &'writer mut dyn WriteColor,
113     config: &'config Config,
114 }
115 
116 impl<'writer, 'config> Renderer<'writer, 'config> {
117     /// Construct a renderer from the given writer and config.
new( writer: &'writer mut dyn WriteColor, config: &'config Config, ) -> Renderer<'writer, '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 
chars(&self) -> &'config Chars125     fn chars(&self) -> &'config Chars {
126         &self.config.chars
127     }
128 
styles(&self) -> &'config Styles129     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     /// ```
render_header( &mut self, locus: Option<&Locus>, severity: Severity, code: Option<&str>, message: &str, ) -> Result<(), Error>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.
render_empty(&mut self) -> Result<(), Error>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     /// ```
render_snippet_start( &mut self, outer_padding: usize, locus: &Locus, ) -> Result<(), Error>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().snippet_start)?;
212         self.reset()?;
213 
214         write!(self, " ")?;
215         self.snippet_locus(&locus)?;
216 
217         writeln!(self)?;
218 
219         Ok(())
220     }
221 
222     /// A line of source code.
223     ///
224     /// ```text
225     /// 10 │   │ muffin. Halvah croissant candy canes bonbon candy. Apple pie jelly
226     ///    │ ╭─│─────────^
227     /// ```
render_snippet_source( &mut self, outer_padding: usize, line_number: usize, source: &str, severity: Severity, single_labels: &[SingleLabel<'_>], num_multi_labels: usize, multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)], ) -> Result<(), Error>228     pub fn render_snippet_source(
229         &mut self,
230         outer_padding: usize,
231         line_number: usize,
232         source: &str,
233         severity: Severity,
234         single_labels: &[SingleLabel<'_>],
235         num_multi_labels: usize,
236         multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)],
237     ) -> Result<(), Error> {
238         // Trim trailing newlines, linefeeds, and null chars from source, if they exist.
239         // FIXME: Use the number of trimmed placeholders when rendering single line carets
240         let source = source.trim_end_matches(['\n', '\r', '\0'].as_ref());
241 
242         // Write source line
243         //
244         // ```text
245         // 10 │   │ muffin. Halvah croissant candy canes bonbon candy. Apple pie jelly
246         // ```
247         {
248             // Write outer gutter (with line number) and border
249             self.outer_gutter_number(line_number, outer_padding)?;
250             self.border_left()?;
251 
252             // Write inner gutter (with multi-line continuations on the left if necessary)
253             let mut multi_labels_iter = multi_labels.iter().peekable();
254             for label_column in 0..num_multi_labels {
255                 match multi_labels_iter.peek() {
256                     Some((label_index, label_style, label)) if *label_index == label_column => {
257                         match label {
258                             MultiLabel::Top(start)
259                                 if *start <= source.len() - source.trim_start().len() =>
260                             {
261                                 self.label_multi_top_left(severity, *label_style)?;
262                             }
263                             MultiLabel::Top(..) => self.inner_gutter_space()?,
264                             MultiLabel::Left | MultiLabel::Bottom(..) => {
265                                 self.label_multi_left(severity, *label_style, None)?;
266                             }
267                         }
268                         multi_labels_iter.next();
269                     }
270                     Some((_, _, _)) | None => self.inner_gutter_space()?,
271                 }
272             }
273 
274             // Write source text
275             write!(self, " ")?;
276             let mut in_primary = false;
277             for (metrics, ch) in self.char_metrics(source.char_indices()) {
278                 let column_range = metrics.byte_index..(metrics.byte_index + ch.len_utf8());
279 
280                 // Check if we are overlapping a primary label
281                 let is_primary = single_labels.iter().any(|(ls, range, _)| {
282                     *ls == LabelStyle::Primary && is_overlapping(range, &column_range)
283                 }) || multi_labels.iter().any(|(_, ls, label)| {
284                     *ls == LabelStyle::Primary
285                         && match label {
286                             MultiLabel::Top(start) => column_range.start >= *start,
287                             MultiLabel::Left => true,
288                             MultiLabel::Bottom(start, _) => column_range.end <= *start,
289                         }
290                 });
291 
292                 // Set the source color if we are in a primary label
293                 if is_primary && !in_primary {
294                     self.set_color(self.styles().label(severity, LabelStyle::Primary))?;
295                     in_primary = true;
296                 } else if !is_primary && in_primary {
297                     self.reset()?;
298                     in_primary = false;
299                 }
300 
301                 match ch {
302                     '\t' => (0..metrics.unicode_width).try_for_each(|_| write!(self, " "))?,
303                     _ => write!(self, "{}", ch)?,
304                 }
305             }
306             if in_primary {
307                 self.reset()?;
308             }
309             writeln!(self)?;
310         }
311 
312         // Write single labels underneath source
313         //
314         // ```text
315         //   │     - ---- ^^^ second mutable borrow occurs here
316         //   │     │ │
317         //   │     │ first mutable borrow occurs here
318         //   │     first borrow later used by call
319         //   │     help: some help here
320         // ```
321         if !single_labels.is_empty() {
322             // Our plan is as follows:
323             //
324             // 1. Do an initial scan to find:
325             //    - The number of non-empty messages.
326             //    - The right-most start and end positions of labels.
327             //    - A candidate for a trailing label (where the label's message
328             //      is printed to the left of the caret).
329             // 2. Check if the trailing label candidate overlaps another label -
330             //    if so we print it underneath the carets with the other labels.
331             // 3. Print a line of carets, and (possibly) the trailing message
332             //    to the left.
333             // 4. Print vertical lines pointing to the carets, and the messages
334             //    for those carets.
335             //
336             // We try our best avoid introducing new dynamic allocations,
337             // instead preferring to iterate over the labels multiple times. It
338             // is unclear what the performance tradeoffs are however, so further
339             // investigation may be required.
340 
341             // The number of non-empty messages to print.
342             let mut num_messages = 0;
343             // The right-most start position, eg:
344             //
345             // ```text
346             // -^^^^---- ^^^^^^^
347             //           │
348             //           right-most start position
349             // ```
350             let mut max_label_start = 0;
351             // The right-most end position, eg:
352             //
353             // ```text
354             // -^^^^---- ^^^^^^^
355             //                 │
356             //                 right-most end position
357             // ```
358             let mut max_label_end = 0;
359             // A trailing message, eg:
360             //
361             // ```text
362             // ^^^ second mutable borrow occurs here
363             // ```
364             let mut trailing_label = None;
365 
366             for (label_index, label) in single_labels.iter().enumerate() {
367                 let (_, range, message) = label;
368                 if !message.is_empty() {
369                     num_messages += 1;
370                 }
371                 max_label_start = std::cmp::max(max_label_start, range.start);
372                 max_label_end = std::cmp::max(max_label_end, range.end);
373                 // This is a candidate for the trailing label, so let's record it.
374                 if range.end == max_label_end {
375                     if message.is_empty() {
376                         trailing_label = None;
377                     } else {
378                         trailing_label = Some((label_index, label));
379                     }
380                 }
381             }
382             if let Some((trailing_label_index, (_, trailing_range, _))) = trailing_label {
383                 // Check to see if the trailing label candidate overlaps any of
384                 // the other labels on the current line.
385                 if single_labels
386                     .iter()
387                     .enumerate()
388                     .filter(|(label_index, _)| *label_index != trailing_label_index)
389                     .any(|(_, (_, range, _))| is_overlapping(trailing_range, range))
390                 {
391                     // If it does, we'll instead want to render it below the
392                     // carets along with the other hanging labels.
393                     trailing_label = None;
394                 }
395             }
396 
397             // Write a line of carets
398             //
399             // ```text
400             //   │ ^^^^^^  -------^^^^^^^^^-------^^^^^----- ^^^^ trailing label message
401             // ```
402             self.outer_gutter(outer_padding)?;
403             self.border_left()?;
404             self.inner_gutter(severity, num_multi_labels, multi_labels)?;
405             write!(self, " ")?;
406 
407             let mut previous_label_style = None;
408             let placeholder_metrics = Metrics {
409                 byte_index: source.len(),
410                 unicode_width: 1,
411             };
412             for (metrics, ch) in self
413                 .char_metrics(source.char_indices())
414                 // Add a placeholder source column at the end to allow for
415                 // printing carets at the end of lines, eg:
416                 //
417                 // ```text
418                 // 1 │ Hello world!
419                 //   │             ^
420                 // ```
421                 .chain(std::iter::once((placeholder_metrics, '\0')))
422             {
423                 // Find the current label style at this column
424                 let column_range = metrics.byte_index..(metrics.byte_index + ch.len_utf8());
425                 let current_label_style = single_labels
426                     .iter()
427                     .filter(|(_, range, _)| is_overlapping(range, &column_range))
428                     .map(|(label_style, _, _)| *label_style)
429                     .max_by_key(label_priority_key);
430 
431                 // Update writer style if necessary
432                 if previous_label_style != current_label_style {
433                     match current_label_style {
434                         None => self.reset()?,
435                         Some(label_style) => {
436                             self.set_color(self.styles().label(severity, label_style))?;
437                         }
438                     }
439                 }
440 
441                 let caret_ch = match current_label_style {
442                     Some(LabelStyle::Primary) => Some(self.chars().single_primary_caret),
443                     Some(LabelStyle::Secondary) => Some(self.chars().single_secondary_caret),
444                     // Only print padding if we are before the end of the last single line caret
445                     None if metrics.byte_index < max_label_end => Some(' '),
446                     None => None,
447                 };
448                 if let Some(caret_ch) = caret_ch {
449                     // FIXME: improve rendering of carets between character boundaries
450                     (0..metrics.unicode_width).try_for_each(|_| write!(self, "{}", caret_ch))?;
451                 }
452 
453                 previous_label_style = current_label_style;
454             }
455             // Reset style if it was previously set
456             if previous_label_style.is_some() {
457                 self.reset()?;
458             }
459             // Write first trailing label message
460             if let Some((_, (label_style, _, message))) = trailing_label {
461                 write!(self, " ")?;
462                 self.set_color(self.styles().label(severity, *label_style))?;
463                 write!(self, "{}", message)?;
464                 self.reset()?;
465             }
466             writeln!(self)?;
467 
468             // Write hanging labels pointing to carets
469             //
470             // ```text
471             //   │     │ │
472             //   │     │ first mutable borrow occurs here
473             //   │     first borrow later used by call
474             //   │     help: some help here
475             // ```
476             if num_messages > trailing_label.iter().count() {
477                 // Write first set of vertical lines before hanging labels
478                 //
479                 // ```text
480                 //   │     │ │
481                 // ```
482                 self.outer_gutter(outer_padding)?;
483                 self.border_left()?;
484                 self.inner_gutter(severity, num_multi_labels, multi_labels)?;
485                 write!(self, " ")?;
486                 self.caret_pointers(
487                     severity,
488                     max_label_start,
489                     single_labels,
490                     trailing_label,
491                     source.char_indices(),
492                 )?;
493                 writeln!(self)?;
494 
495                 // Write hanging labels pointing to carets
496                 //
497                 // ```text
498                 //   │     │ first mutable borrow occurs here
499                 //   │     first borrow later used by call
500                 //   │     help: some help here
501                 // ```
502                 for (label_style, range, message) in
503                     hanging_labels(single_labels, trailing_label).rev()
504                 {
505                     self.outer_gutter(outer_padding)?;
506                     self.border_left()?;
507                     self.inner_gutter(severity, num_multi_labels, multi_labels)?;
508                     write!(self, " ")?;
509                     self.caret_pointers(
510                         severity,
511                         max_label_start,
512                         single_labels,
513                         trailing_label,
514                         source
515                             .char_indices()
516                             .take_while(|(byte_index, _)| *byte_index < range.start),
517                     )?;
518                     self.set_color(self.styles().label(severity, *label_style))?;
519                     write!(self, "{}", message)?;
520                     self.reset()?;
521                     writeln!(self)?;
522                 }
523             }
524         }
525 
526         // Write top or bottom label carets underneath source
527         //
528         // ```text
529         //     │ ╰───│──────────────────^ woops
530         //     │   ╭─│─────────^
531         // ```
532         for (multi_label_index, (_, label_style, label)) in multi_labels.iter().enumerate() {
533             let (label_style, range, bottom_message) = match label {
534                 MultiLabel::Left => continue, // no label caret needed
535                 // no label caret needed if this can be started in front of the line
536                 MultiLabel::Top(start) if *start <= source.len() - source.trim_start().len() => {
537                     continue
538                 }
539                 MultiLabel::Top(range) => (*label_style, range, None),
540                 MultiLabel::Bottom(range, message) => (*label_style, range, Some(message)),
541             };
542 
543             self.outer_gutter(outer_padding)?;
544             self.border_left()?;
545 
546             // Write inner gutter.
547             //
548             // ```text
549             //  │ ╭─│───│
550             // ```
551             let mut underline = None;
552             let mut multi_labels_iter = multi_labels.iter().enumerate().peekable();
553             for label_column in 0..num_multi_labels {
554                 match multi_labels_iter.peek() {
555                     Some((i, (label_index, ls, label))) if *label_index == label_column => {
556                         match label {
557                             MultiLabel::Left => {
558                                 self.label_multi_left(severity, *ls, underline.map(|(s, _)| s))?;
559                             }
560                             MultiLabel::Top(..) if multi_label_index > *i => {
561                                 self.label_multi_left(severity, *ls, underline.map(|(s, _)| s))?;
562                             }
563                             MultiLabel::Bottom(..) if multi_label_index < *i => {
564                                 self.label_multi_left(severity, *ls, underline.map(|(s, _)| s))?;
565                             }
566                             MultiLabel::Top(..) if multi_label_index == *i => {
567                                 underline = Some((*ls, VerticalBound::Top));
568                                 self.label_multi_top_left(severity, label_style)?
569                             }
570                             MultiLabel::Bottom(..) if multi_label_index == *i => {
571                                 underline = Some((*ls, VerticalBound::Bottom));
572                                 self.label_multi_bottom_left(severity, label_style)?;
573                             }
574                             MultiLabel::Top(..) | MultiLabel::Bottom(..) => {
575                                 self.inner_gutter_column(severity, underline)?;
576                             }
577                         }
578                         multi_labels_iter.next();
579                     }
580                     Some((_, _)) | None => self.inner_gutter_column(severity, underline)?,
581                 }
582             }
583 
584             // Finish the top or bottom caret
585             match bottom_message {
586                 None => self.label_multi_top_caret(severity, label_style, source, *range)?,
587                 Some(message) => {
588                     self.label_multi_bottom_caret(severity, label_style, source, *range, message)?
589                 }
590             }
591         }
592 
593         Ok(())
594     }
595 
596     /// An empty source line, for providing additional whitespace to source snippets.
597     ///
598     /// ```text
599     /// │ │ │
600     /// ```
render_snippet_empty( &mut self, outer_padding: usize, severity: Severity, num_multi_labels: usize, multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)], ) -> Result<(), Error>601     pub fn render_snippet_empty(
602         &mut self,
603         outer_padding: usize,
604         severity: Severity,
605         num_multi_labels: usize,
606         multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)],
607     ) -> Result<(), Error> {
608         self.outer_gutter(outer_padding)?;
609         self.border_left()?;
610         self.inner_gutter(severity, num_multi_labels, multi_labels)?;
611         writeln!(self)?;
612         Ok(())
613     }
614 
615     /// A broken source line, for labeling skipped sections of source.
616     ///
617     /// ```text
618     /// · │ │
619     /// ```
render_snippet_break( &mut self, outer_padding: usize, severity: Severity, num_multi_labels: usize, multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)], ) -> Result<(), Error>620     pub fn render_snippet_break(
621         &mut self,
622         outer_padding: usize,
623         severity: Severity,
624         num_multi_labels: usize,
625         multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)],
626     ) -> Result<(), Error> {
627         self.outer_gutter(outer_padding)?;
628         self.border_left_break()?;
629         self.inner_gutter(severity, num_multi_labels, multi_labels)?;
630         writeln!(self)?;
631         Ok(())
632     }
633 
634     /// Additional notes.
635     ///
636     /// ```text
637     /// = expected type `Int`
638     ///      found type `String`
639     /// ```
render_snippet_note( &mut self, outer_padding: usize, message: &str, ) -> Result<(), Error>640     pub fn render_snippet_note(
641         &mut self,
642         outer_padding: usize,
643         message: &str,
644     ) -> Result<(), Error> {
645         for (note_line_index, line) in message.lines().enumerate() {
646             self.outer_gutter(outer_padding)?;
647             match note_line_index {
648                 0 => {
649                     self.set_color(&self.styles().note_bullet)?;
650                     write!(self, "{}", self.chars().note_bullet)?;
651                     self.reset()?;
652                 }
653                 _ => write!(self, " ")?,
654             }
655             // Write line of message
656             writeln!(self, " {}", line)?;
657         }
658 
659         Ok(())
660     }
661 
662     /// Adds tab-stop aware unicode-width computations to an iterator over
663     /// character indices. Assumes that the character indices begin at the start
664     /// of the line.
char_metrics( &self, char_indices: impl Iterator<Item = (usize, char)>, ) -> impl Iterator<Item = (Metrics, char)>665     fn char_metrics(
666         &self,
667         char_indices: impl Iterator<Item = (usize, char)>,
668     ) -> impl Iterator<Item = (Metrics, char)> {
669         use unicode_width::UnicodeWidthChar;
670 
671         let tab_width = self.config.tab_width;
672         let mut unicode_column = 0;
673 
674         char_indices.map(move |(byte_index, ch)| {
675             let metrics = Metrics {
676                 byte_index,
677                 unicode_width: match (ch, tab_width) {
678                     ('\t', 0) => 0, // Guard divide-by-zero
679                     ('\t', _) => tab_width - (unicode_column % tab_width),
680                     (ch, _) => ch.width().unwrap_or(0),
681                 },
682             };
683             unicode_column += metrics.unicode_width;
684 
685             (metrics, ch)
686         })
687     }
688 
689     /// Location focus.
snippet_locus(&mut self, locus: &Locus) -> Result<(), Error>690     fn snippet_locus(&mut self, locus: &Locus) -> Result<(), Error> {
691         write!(
692             self,
693             "{name}:{line_number}:{column_number}",
694             name = locus.name,
695             line_number = locus.location.line_number,
696             column_number = locus.location.column_number,
697         )?;
698         Ok(())
699     }
700 
701     /// The outer gutter of a source line.
outer_gutter(&mut self, outer_padding: usize) -> Result<(), Error>702     fn outer_gutter(&mut self, outer_padding: usize) -> Result<(), Error> {
703         write!(self, "{space: >width$} ", space = "", width = outer_padding)?;
704         Ok(())
705     }
706 
707     /// The outer gutter of a source line, with line number.
outer_gutter_number( &mut self, line_number: usize, outer_padding: usize, ) -> Result<(), Error>708     fn outer_gutter_number(
709         &mut self,
710         line_number: usize,
711         outer_padding: usize,
712     ) -> Result<(), Error> {
713         self.set_color(&self.styles().line_number)?;
714         write!(
715             self,
716             "{line_number: >width$}",
717             line_number = line_number,
718             width = outer_padding,
719         )?;
720         self.reset()?;
721         write!(self, " ")?;
722         Ok(())
723     }
724 
725     /// The left-hand border of a source line.
border_left(&mut self) -> Result<(), Error>726     fn border_left(&mut self) -> Result<(), Error> {
727         self.set_color(&self.styles().source_border)?;
728         write!(self, "{}", self.chars().source_border_left)?;
729         self.reset()?;
730         Ok(())
731     }
732 
733     /// The broken left-hand border of a source line.
border_left_break(&mut self) -> Result<(), Error>734     fn border_left_break(&mut self) -> Result<(), Error> {
735         self.set_color(&self.styles().source_border)?;
736         write!(self, "{}", self.chars().source_border_left_break)?;
737         self.reset()?;
738         Ok(())
739     }
740 
741     /// Write vertical lines pointing to carets.
caret_pointers( &mut self, severity: Severity, max_label_start: usize, single_labels: &[SingleLabel<'_>], trailing_label: Option<(usize, &SingleLabel<'_>)>, char_indices: impl Iterator<Item = (usize, char)>, ) -> Result<(), Error>742     fn caret_pointers(
743         &mut self,
744         severity: Severity,
745         max_label_start: usize,
746         single_labels: &[SingleLabel<'_>],
747         trailing_label: Option<(usize, &SingleLabel<'_>)>,
748         char_indices: impl Iterator<Item = (usize, char)>,
749     ) -> Result<(), Error> {
750         for (metrics, ch) in self.char_metrics(char_indices) {
751             let column_range = metrics.byte_index..(metrics.byte_index + ch.len_utf8());
752             let label_style = hanging_labels(single_labels, trailing_label)
753                 .filter(|(_, range, _)| column_range.contains(&range.start))
754                 .map(|(label_style, _, _)| *label_style)
755                 .max_by_key(label_priority_key);
756 
757             let mut spaces = match label_style {
758                 None => 0..metrics.unicode_width,
759                 Some(label_style) => {
760                     self.set_color(self.styles().label(severity, label_style))?;
761                     write!(self, "{}", self.chars().pointer_left)?;
762                     self.reset()?;
763                     1..metrics.unicode_width
764                 }
765             };
766             // Only print padding if we are before the end of the last single line caret
767             if metrics.byte_index <= max_label_start {
768                 spaces.try_for_each(|_| write!(self, " "))?;
769             }
770         }
771 
772         Ok(())
773     }
774 
775     /// The left of a multi-line label.
776     ///
777     /// ```text
778     ///  │
779     /// ```
label_multi_left( &mut self, severity: Severity, label_style: LabelStyle, underline: Option<LabelStyle>, ) -> Result<(), Error>780     fn label_multi_left(
781         &mut self,
782         severity: Severity,
783         label_style: LabelStyle,
784         underline: Option<LabelStyle>,
785     ) -> Result<(), Error> {
786         match underline {
787             None => write!(self, " ")?,
788             // Continue an underline horizontally
789             Some(label_style) => {
790                 self.set_color(self.styles().label(severity, label_style))?;
791                 write!(self, "{}", self.chars().multi_top)?;
792                 self.reset()?;
793             }
794         }
795         self.set_color(self.styles().label(severity, label_style))?;
796         write!(self, "{}", self.chars().multi_left)?;
797         self.reset()?;
798         Ok(())
799     }
800 
801     /// The top-left of a multi-line label.
802     ///
803     /// ```text
804     ///  ╭
805     /// ```
label_multi_top_left( &mut self, severity: Severity, label_style: LabelStyle, ) -> Result<(), Error>806     fn label_multi_top_left(
807         &mut self,
808         severity: Severity,
809         label_style: LabelStyle,
810     ) -> Result<(), Error> {
811         write!(self, " ")?;
812         self.set_color(self.styles().label(severity, label_style))?;
813         write!(self, "{}", self.chars().multi_top_left)?;
814         self.reset()?;
815         Ok(())
816     }
817 
818     /// The bottom left of a multi-line label.
819     ///
820     /// ```text
821     ///  ╰
822     /// ```
label_multi_bottom_left( &mut self, severity: Severity, label_style: LabelStyle, ) -> Result<(), Error>823     fn label_multi_bottom_left(
824         &mut self,
825         severity: Severity,
826         label_style: LabelStyle,
827     ) -> Result<(), Error> {
828         write!(self, " ")?;
829         self.set_color(self.styles().label(severity, label_style))?;
830         write!(self, "{}", self.chars().multi_bottom_left)?;
831         self.reset()?;
832         Ok(())
833     }
834 
835     /// Multi-line label top.
836     ///
837     /// ```text
838     /// ─────────────^
839     /// ```
label_multi_top_caret( &mut self, severity: Severity, label_style: LabelStyle, source: &str, start: usize, ) -> Result<(), Error>840     fn label_multi_top_caret(
841         &mut self,
842         severity: Severity,
843         label_style: LabelStyle,
844         source: &str,
845         start: usize,
846     ) -> Result<(), Error> {
847         self.set_color(self.styles().label(severity, label_style))?;
848 
849         for (metrics, _) in self
850             .char_metrics(source.char_indices())
851             .take_while(|(metrics, _)| metrics.byte_index < start + 1)
852         {
853             // FIXME: improve rendering of carets between character boundaries
854             (0..metrics.unicode_width)
855                 .try_for_each(|_| write!(self, "{}", self.chars().multi_top))?;
856         }
857 
858         let caret_start = match label_style {
859             LabelStyle::Primary => self.config.chars.multi_primary_caret_start,
860             LabelStyle::Secondary => self.config.chars.multi_secondary_caret_start,
861         };
862         write!(self, "{}", caret_start)?;
863         self.reset()?;
864         writeln!(self)?;
865         Ok(())
866     }
867 
868     /// Multi-line label bottom, with a message.
869     ///
870     /// ```text
871     /// ─────────────^ expected `Int` but found `String`
872     /// ```
label_multi_bottom_caret( &mut self, severity: Severity, label_style: LabelStyle, source: &str, start: usize, message: &str, ) -> Result<(), Error>873     fn label_multi_bottom_caret(
874         &mut self,
875         severity: Severity,
876         label_style: LabelStyle,
877         source: &str,
878         start: usize,
879         message: &str,
880     ) -> Result<(), Error> {
881         self.set_color(self.styles().label(severity, label_style))?;
882 
883         for (metrics, _) in self
884             .char_metrics(source.char_indices())
885             .take_while(|(metrics, _)| metrics.byte_index < start)
886         {
887             // FIXME: improve rendering of carets between character boundaries
888             (0..metrics.unicode_width)
889                 .try_for_each(|_| write!(self, "{}", self.chars().multi_bottom))?;
890         }
891 
892         let caret_end = match label_style {
893             LabelStyle::Primary => self.config.chars.multi_primary_caret_start,
894             LabelStyle::Secondary => self.config.chars.multi_secondary_caret_start,
895         };
896         write!(self, "{}", caret_end)?;
897         if !message.is_empty() {
898             write!(self, " {}", message)?;
899         }
900         self.reset()?;
901         writeln!(self)?;
902         Ok(())
903     }
904 
905     /// Writes an empty gutter space, or continues an underline horizontally.
inner_gutter_column( &mut self, severity: Severity, underline: Option<Underline>, ) -> Result<(), Error>906     fn inner_gutter_column(
907         &mut self,
908         severity: Severity,
909         underline: Option<Underline>,
910     ) -> Result<(), Error> {
911         match underline {
912             None => self.inner_gutter_space(),
913             Some((label_style, vertical_bound)) => {
914                 self.set_color(self.styles().label(severity, label_style))?;
915                 let ch = match vertical_bound {
916                     VerticalBound::Top => self.config.chars.multi_top,
917                     VerticalBound::Bottom => self.config.chars.multi_bottom,
918                 };
919                 write!(self, "{0}{0}", ch)?;
920                 self.reset()?;
921                 Ok(())
922             }
923         }
924     }
925 
926     /// Writes an empty gutter space.
inner_gutter_space(&mut self) -> Result<(), Error>927     fn inner_gutter_space(&mut self) -> Result<(), Error> {
928         write!(self, "  ")?;
929         Ok(())
930     }
931 
932     /// Writes an inner gutter, with the left lines if necessary.
inner_gutter( &mut self, severity: Severity, num_multi_labels: usize, multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)], ) -> Result<(), Error>933     fn inner_gutter(
934         &mut self,
935         severity: Severity,
936         num_multi_labels: usize,
937         multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)],
938     ) -> Result<(), Error> {
939         let mut multi_labels_iter = multi_labels.iter().peekable();
940         for label_column in 0..num_multi_labels {
941             match multi_labels_iter.peek() {
942                 Some((label_index, ls, label)) if *label_index == label_column => match label {
943                     MultiLabel::Left | MultiLabel::Bottom(..) => {
944                         self.label_multi_left(severity, *ls, None)?;
945                         multi_labels_iter.next();
946                     }
947                     MultiLabel::Top(..) => {
948                         self.inner_gutter_space()?;
949                         multi_labels_iter.next();
950                     }
951                 },
952                 Some((_, _, _)) | None => self.inner_gutter_space()?,
953             }
954         }
955 
956         Ok(())
957     }
958 }
959 
960 impl<'writer, 'config> Write for Renderer<'writer, 'config> {
write(&mut self, buf: &[u8]) -> io::Result<usize>961     fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
962         self.writer.write(buf)
963     }
964 
flush(&mut self) -> io::Result<()>965     fn flush(&mut self) -> io::Result<()> {
966         self.writer.flush()
967     }
968 }
969 
970 impl<'writer, 'config> WriteColor for Renderer<'writer, 'config> {
supports_color(&self) -> bool971     fn supports_color(&self) -> bool {
972         self.writer.supports_color()
973     }
974 
set_color(&mut self, spec: &ColorSpec) -> io::Result<()>975     fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
976         self.writer.set_color(spec)
977     }
978 
reset(&mut self) -> io::Result<()>979     fn reset(&mut self) -> io::Result<()> {
980         self.writer.reset()
981     }
982 
is_synchronous(&self) -> bool983     fn is_synchronous(&self) -> bool {
984         self.writer.is_synchronous()
985     }
986 }
987 
988 struct Metrics {
989     byte_index: usize,
990     unicode_width: usize,
991 }
992 
993 /// Check if two ranges overlap
is_overlapping(range0: &Range<usize>, range1: &Range<usize>) -> bool994 fn is_overlapping(range0: &Range<usize>, range1: &Range<usize>) -> bool {
995     let start = std::cmp::max(range0.start, range1.start);
996     let end = std::cmp::min(range0.end, range1.end);
997     start < end
998 }
999 
1000 /// For prioritizing primary labels over secondary labels when rendering carets.
label_priority_key(label_style: &LabelStyle) -> u81001 fn label_priority_key(label_style: &LabelStyle) -> u8 {
1002     match label_style {
1003         LabelStyle::Secondary => 0,
1004         LabelStyle::Primary => 1,
1005     }
1006 }
1007 
1008 /// Return an iterator that yields the labels that require hanging messages
1009 /// rendered underneath them.
hanging_labels<'labels, 'diagnostic>( single_labels: &'labels [SingleLabel<'diagnostic>], trailing_label: Option<(usize, &'labels SingleLabel<'diagnostic>)>, ) -> impl 'labels + DoubleEndedIterator<Item = &'labels SingleLabel<'diagnostic>>1010 fn hanging_labels<'labels, 'diagnostic>(
1011     single_labels: &'labels [SingleLabel<'diagnostic>],
1012     trailing_label: Option<(usize, &'labels SingleLabel<'diagnostic>)>,
1013 ) -> impl 'labels + DoubleEndedIterator<Item = &'labels SingleLabel<'diagnostic>> {
1014     single_labels
1015         .iter()
1016         .enumerate()
1017         .filter(|(_, (_, _, message))| !message.is_empty())
1018         .filter(move |(i, _)| trailing_label.map_or(true, |(j, _)| *i != j))
1019         .map(|(_, label)| label)
1020 }
1021