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