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