• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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().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     /// ```
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>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     /// ```
render_snippet_empty( &mut self, outer_padding: usize, severity: Severity, num_multi_labels: usize, multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)], ) -> Result<(), Error>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     /// ```
render_snippet_break( &mut self, outer_padding: usize, severity: Severity, num_multi_labels: usize, multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)], ) -> Result<(), Error>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     /// ```
render_snippet_note( &mut self, outer_padding: usize, message: &str, ) -> Result<(), Error>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.
char_metrics( &self, char_indices: impl Iterator<Item = (usize, char)>, ) -> impl Iterator<Item = (Metrics, char)>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.
snippet_locus(&mut self, locus: &Locus) -> Result<(), Error>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.
outer_gutter(&mut self, outer_padding: usize) -> Result<(), Error>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.
outer_gutter_number( &mut self, line_number: usize, outer_padding: usize, ) -> Result<(), Error>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.
border_left(&mut self) -> Result<(), Error>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.
border_left_break(&mut self) -> Result<(), Error>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.
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>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     /// ```
label_multi_left( &mut self, severity: Severity, label_style: LabelStyle, underline: Option<LabelStyle>, ) -> Result<(), Error>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     /// ```
label_multi_top_left( &mut self, severity: Severity, label_style: LabelStyle, ) -> Result<(), Error>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     /// ```
label_multi_bottom_left( &mut self, severity: Severity, label_style: LabelStyle, ) -> Result<(), Error>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     /// ```
label_multi_top_caret( &mut self, severity: Severity, label_style: LabelStyle, source: &str, start: usize, ) -> Result<(), Error>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     /// ```
label_multi_bottom_caret( &mut self, severity: Severity, label_style: LabelStyle, source: &str, start: usize, message: &str, ) -> Result<(), Error>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.
inner_gutter_column( &mut self, severity: Severity, underline: Option<Underline>, ) -> Result<(), Error>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.
inner_gutter_space(&mut self) -> Result<(), Error>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.
inner_gutter( &mut self, severity: Severity, num_multi_labels: usize, multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)], ) -> Result<(), Error>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 
961 impl<'writer, 'config> Write for Renderer<'writer, 'config> {
write(&mut self, buf: &[u8]) -> io::Result<usize>962     fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
963         self.writer.write(buf)
964     }
965 
flush(&mut self) -> io::Result<()>966     fn flush(&mut self) -> io::Result<()> {
967         self.writer.flush()
968     }
969 }
970 
971 impl<'writer, 'config> WriteColor for Renderer<'writer, 'config> {
supports_color(&self) -> bool972     fn supports_color(&self) -> bool {
973         self.writer.supports_color()
974     }
975 
set_color(&mut self, spec: &ColorSpec) -> io::Result<()>976     fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
977         self.writer.set_color(spec)
978     }
979 
reset(&mut self) -> io::Result<()>980     fn reset(&mut self) -> io::Result<()> {
981         self.writer.reset()
982     }
983 
is_synchronous(&self) -> bool984     fn is_synchronous(&self) -> bool {
985         self.writer.is_synchronous()
986     }
987 }
988 
989 struct Metrics {
990     byte_index: usize,
991     unicode_width: usize,
992 }
993 
994 /// Check if two ranges overlap
is_overlapping(range0: &Range<usize>, range1: &Range<usize>) -> bool995 fn 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.
label_priority_key(label_style: &LabelStyle) -> u81002 fn 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.
hanging_labels<'labels, 'diagnostic>( single_labels: &'labels [SingleLabel<'diagnostic>], trailing_label: Option<(usize, &'labels SingleLabel<'diagnostic>)>, ) -> impl 'labels + DoubleEndedIterator<Item = &'labels SingleLabel<'diagnostic>>1011 fn 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