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