1 use std::fmt::{self, Write}; 2 3 use owo_colors::{OwoColorize, Style, StyledList}; 4 use unicode_width::UnicodeWidthChar; 5 6 use crate::diagnostic_chain::{DiagnosticChain, ErrorKind}; 7 use crate::handlers::theme::*; 8 use crate::highlighters::{Highlighter, MietteHighlighter}; 9 use crate::protocol::{Diagnostic, Severity}; 10 use crate::{LabeledSpan, ReportHandler, SourceCode, SourceSpan, SpanContents}; 11 12 /** 13 A [`ReportHandler`] that displays a given [`Report`](crate::Report) in a 14 quasi-graphical way, using terminal colors, unicode drawing characters, and 15 other such things. 16 17 This is the default reporter bundled with `miette`. 18 19 This printer can be customized by using [`new_themed()`](GraphicalReportHandler::new_themed) and handing it a 20 [`GraphicalTheme`] of your own creation (or using one of its own defaults!) 21 22 See [`set_hook()`](crate::set_hook) for more details on customizing your global 23 printer. 24 */ 25 #[derive(Debug, Clone)] 26 pub struct GraphicalReportHandler { 27 pub(crate) links: LinkStyle, 28 pub(crate) termwidth: usize, 29 pub(crate) theme: GraphicalTheme, 30 pub(crate) footer: Option<String>, 31 pub(crate) context_lines: usize, 32 pub(crate) tab_width: usize, 33 pub(crate) with_cause_chain: bool, 34 pub(crate) wrap_lines: bool, 35 pub(crate) break_words: bool, 36 pub(crate) word_separator: Option<textwrap::WordSeparator>, 37 pub(crate) word_splitter: Option<textwrap::WordSplitter>, 38 pub(crate) highlighter: MietteHighlighter, 39 } 40 41 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 42 pub(crate) enum LinkStyle { 43 None, 44 Link, 45 Text, 46 } 47 48 impl GraphicalReportHandler { 49 /// Create a new `GraphicalReportHandler` with the default 50 /// [`GraphicalTheme`]. This will use both unicode characters and colors. new() -> Self51 pub fn new() -> Self { 52 Self { 53 links: LinkStyle::Link, 54 termwidth: 200, 55 theme: GraphicalTheme::default(), 56 footer: None, 57 context_lines: 1, 58 tab_width: 4, 59 with_cause_chain: true, 60 wrap_lines: true, 61 break_words: true, 62 word_separator: None, 63 word_splitter: None, 64 highlighter: MietteHighlighter::default(), 65 } 66 } 67 68 ///Create a new `GraphicalReportHandler` with a given [`GraphicalTheme`]. new_themed(theme: GraphicalTheme) -> Self69 pub fn new_themed(theme: GraphicalTheme) -> Self { 70 Self { 71 links: LinkStyle::Link, 72 termwidth: 200, 73 theme, 74 footer: None, 75 context_lines: 1, 76 tab_width: 4, 77 wrap_lines: true, 78 with_cause_chain: true, 79 break_words: true, 80 word_separator: None, 81 word_splitter: None, 82 highlighter: MietteHighlighter::default(), 83 } 84 } 85 86 /// Set the displayed tab width in spaces. tab_width(mut self, width: usize) -> Self87 pub fn tab_width(mut self, width: usize) -> Self { 88 self.tab_width = width; 89 self 90 } 91 92 /// Whether to enable error code linkification using [`Diagnostic::url()`]. with_links(mut self, links: bool) -> Self93 pub fn with_links(mut self, links: bool) -> Self { 94 self.links = if links { 95 LinkStyle::Link 96 } else { 97 LinkStyle::Text 98 }; 99 self 100 } 101 102 /// Include the cause chain of the top-level error in the graphical output, 103 /// if available. with_cause_chain(mut self) -> Self104 pub fn with_cause_chain(mut self) -> Self { 105 self.with_cause_chain = true; 106 self 107 } 108 109 /// Do not include the cause chain of the top-level error in the graphical 110 /// output. without_cause_chain(mut self) -> Self111 pub fn without_cause_chain(mut self) -> Self { 112 self.with_cause_chain = false; 113 self 114 } 115 116 /// Whether to include [`Diagnostic::url()`] in the output. 117 /// 118 /// Disabling this is not recommended, but can be useful for more easily 119 /// reproducible tests, as `url(docsrs)` links are version-dependent. with_urls(mut self, urls: bool) -> Self120 pub fn with_urls(mut self, urls: bool) -> Self { 121 self.links = match (self.links, urls) { 122 (_, false) => LinkStyle::None, 123 (LinkStyle::None, true) => LinkStyle::Link, 124 (links, true) => links, 125 }; 126 self 127 } 128 129 /// Set a theme for this handler. with_theme(mut self, theme: GraphicalTheme) -> Self130 pub fn with_theme(mut self, theme: GraphicalTheme) -> Self { 131 self.theme = theme; 132 self 133 } 134 135 /// Sets the width to wrap the report at. with_width(mut self, width: usize) -> Self136 pub fn with_width(mut self, width: usize) -> Self { 137 self.termwidth = width; 138 self 139 } 140 141 /// Enables or disables wrapping of lines to fit the width. with_wrap_lines(mut self, wrap_lines: bool) -> Self142 pub fn with_wrap_lines(mut self, wrap_lines: bool) -> Self { 143 self.wrap_lines = wrap_lines; 144 self 145 } 146 147 /// Enables or disables breaking of words during wrapping. with_break_words(mut self, break_words: bool) -> Self148 pub fn with_break_words(mut self, break_words: bool) -> Self { 149 self.break_words = break_words; 150 self 151 } 152 153 /// Sets the word separator to use when wrapping. with_word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self154 pub fn with_word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self { 155 self.word_separator = Some(word_separator); 156 self 157 } 158 159 /// Sets the word splitter to usewhen wrapping. with_word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self160 pub fn with_word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self { 161 self.word_splitter = Some(word_splitter); 162 self 163 } 164 165 /// Sets the 'global' footer for this handler. with_footer(mut self, footer: String) -> Self166 pub fn with_footer(mut self, footer: String) -> Self { 167 self.footer = Some(footer); 168 self 169 } 170 171 /// Sets the number of lines of context to show around each error. with_context_lines(mut self, lines: usize) -> Self172 pub fn with_context_lines(mut self, lines: usize) -> Self { 173 self.context_lines = lines; 174 self 175 } 176 177 /// Enable syntax highlighting for source code snippets, using the given 178 /// [`Highlighter`]. See the [crate::highlighters] crate for more details. with_syntax_highlighting( mut self, highlighter: impl Highlighter + Send + Sync + 'static, ) -> Self179 pub fn with_syntax_highlighting( 180 mut self, 181 highlighter: impl Highlighter + Send + Sync + 'static, 182 ) -> Self { 183 self.highlighter = MietteHighlighter::from(highlighter); 184 self 185 } 186 187 /// Disable syntax highlighting. This uses the 188 /// [`crate::highlighters::BlankHighlighter`] as a no-op highlighter. without_syntax_highlighting(mut self) -> Self189 pub fn without_syntax_highlighting(mut self) -> Self { 190 self.highlighter = MietteHighlighter::nocolor(); 191 self 192 } 193 } 194 195 impl Default for GraphicalReportHandler { default() -> Self196 fn default() -> Self { 197 Self::new() 198 } 199 } 200 201 impl GraphicalReportHandler { 202 /// Render a [`Diagnostic`]. This function is mostly internal and meant to 203 /// be called by the toplevel [`ReportHandler`] handler, but is made public 204 /// to make it easier (possible) to test in isolation from global state. render_report( &self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic), ) -> fmt::Result205 pub fn render_report( 206 &self, 207 f: &mut impl fmt::Write, 208 diagnostic: &(dyn Diagnostic), 209 ) -> fmt::Result { 210 self.render_header(f, diagnostic)?; 211 self.render_causes(f, diagnostic)?; 212 let src = diagnostic.source_code(); 213 self.render_snippets(f, diagnostic, src)?; 214 self.render_footer(f, diagnostic)?; 215 self.render_related(f, diagnostic, src)?; 216 if let Some(footer) = &self.footer { 217 writeln!(f)?; 218 let width = self.termwidth.saturating_sub(4); 219 let mut opts = textwrap::Options::new(width) 220 .initial_indent(" ") 221 .subsequent_indent(" ") 222 .break_words(self.break_words); 223 if let Some(word_separator) = self.word_separator { 224 opts = opts.word_separator(word_separator); 225 } 226 if let Some(word_splitter) = self.word_splitter.clone() { 227 opts = opts.word_splitter(word_splitter); 228 } 229 230 writeln!(f, "{}", self.wrap(footer, opts))?; 231 } 232 Ok(()) 233 } 234 render_header(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result235 fn render_header(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result { 236 let severity_style = match diagnostic.severity() { 237 Some(Severity::Error) | None => self.theme.styles.error, 238 Some(Severity::Warning) => self.theme.styles.warning, 239 Some(Severity::Advice) => self.theme.styles.advice, 240 }; 241 let mut header = String::new(); 242 if self.links == LinkStyle::Link && diagnostic.url().is_some() { 243 let url = diagnostic.url().unwrap(); // safe 244 let code = if let Some(code) = diagnostic.code() { 245 format!("{} ", code) 246 } else { 247 "".to_string() 248 }; 249 let link = format!( 250 "\u{1b}]8;;{}\u{1b}\\{}{}\u{1b}]8;;\u{1b}\\", 251 url, 252 code.style(severity_style), 253 "(link)".style(self.theme.styles.link) 254 ); 255 write!(header, "{}", link)?; 256 writeln!(f, "{}", header)?; 257 writeln!(f)?; 258 } else if let Some(code) = diagnostic.code() { 259 write!(header, "{}", code.style(severity_style),)?; 260 if self.links == LinkStyle::Text && diagnostic.url().is_some() { 261 let url = diagnostic.url().unwrap(); // safe 262 write!(header, " ({})", url.style(self.theme.styles.link))?; 263 } 264 writeln!(f, "{}", header)?; 265 writeln!(f)?; 266 } 267 Ok(()) 268 } 269 270 fn render_causes(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result { 271 let (severity_style, severity_icon) = match diagnostic.severity() { 272 Some(Severity::Error) | None => (self.theme.styles.error, &self.theme.characters.error), 273 Some(Severity::Warning) => (self.theme.styles.warning, &self.theme.characters.warning), 274 Some(Severity::Advice) => (self.theme.styles.advice, &self.theme.characters.advice), 275 }; 276 277 let initial_indent = format!(" {} ", severity_icon.style(severity_style)); 278 let rest_indent = format!(" {} ", self.theme.characters.vbar.style(severity_style)); 279 let width = self.termwidth.saturating_sub(2); 280 let mut opts = textwrap::Options::new(width) 281 .initial_indent(&initial_indent) 282 .subsequent_indent(&rest_indent) 283 .break_words(self.break_words); 284 if let Some(word_separator) = self.word_separator { 285 opts = opts.word_separator(word_separator); 286 } 287 if let Some(word_splitter) = self.word_splitter.clone() { 288 opts = opts.word_splitter(word_splitter); 289 } 290 291 writeln!(f, "{}", self.wrap(&diagnostic.to_string(), opts))?; 292 293 if !self.with_cause_chain { 294 return Ok(()); 295 } 296 297 if let Some(mut cause_iter) = diagnostic 298 .diagnostic_source() 299 .map(DiagnosticChain::from_diagnostic) 300 .or_else(|| diagnostic.source().map(DiagnosticChain::from_stderror)) 301 .map(|it| it.peekable()) 302 { 303 while let Some(error) = cause_iter.next() { 304 let is_last = cause_iter.peek().is_none(); 305 let char = if !is_last { 306 self.theme.characters.lcross 307 } else { 308 self.theme.characters.lbot 309 }; 310 let initial_indent = format!( 311 " {}{}{} ", 312 char, self.theme.characters.hbar, self.theme.characters.rarrow 313 ) 314 .style(severity_style) 315 .to_string(); 316 let rest_indent = format!( 317 " {} ", 318 if is_last { 319 ' ' 320 } else { 321 self.theme.characters.vbar 322 } 323 ) 324 .style(severity_style) 325 .to_string(); 326 let mut opts = textwrap::Options::new(width) 327 .initial_indent(&initial_indent) 328 .subsequent_indent(&rest_indent) 329 .break_words(self.break_words); 330 if let Some(word_separator) = self.word_separator { 331 opts = opts.word_separator(word_separator); 332 } 333 if let Some(word_splitter) = self.word_splitter.clone() { 334 opts = opts.word_splitter(word_splitter); 335 } 336 337 match error { 338 ErrorKind::Diagnostic(diag) => { 339 let mut inner = String::new(); 340 341 let mut inner_renderer = self.clone(); 342 // Don't print footer for inner errors 343 inner_renderer.footer = None; 344 // Cause chains are already flattened, so don't double-print the nested error 345 inner_renderer.with_cause_chain = false; 346 inner_renderer.render_report(&mut inner, diag)?; 347 348 writeln!(f, "{}", self.wrap(&inner, opts))?; 349 } 350 ErrorKind::StdError(err) => { 351 writeln!(f, "{}", self.wrap(&err.to_string(), opts))?; 352 } 353 } 354 } 355 } 356 357 Ok(()) 358 } 359 360 fn render_footer(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result { 361 if let Some(help) = diagnostic.help() { 362 let width = self.termwidth.saturating_sub(4); 363 let initial_indent = " help: ".style(self.theme.styles.help).to_string(); 364 let mut opts = textwrap::Options::new(width) 365 .initial_indent(&initial_indent) 366 .subsequent_indent(" ") 367 .break_words(self.break_words); 368 if let Some(word_separator) = self.word_separator { 369 opts = opts.word_separator(word_separator); 370 } 371 if let Some(word_splitter) = self.word_splitter.clone() { 372 opts = opts.word_splitter(word_splitter); 373 } 374 375 writeln!(f, "{}", self.wrap(&help.to_string(), opts))?; 376 } 377 Ok(()) 378 } 379 380 fn render_related( 381 &self, 382 f: &mut impl fmt::Write, 383 diagnostic: &(dyn Diagnostic), 384 parent_src: Option<&dyn SourceCode>, 385 ) -> fmt::Result { 386 if let Some(related) = diagnostic.related() { 387 let mut inner_renderer = self.clone(); 388 // Re-enable the printing of nested cause chains for related errors 389 inner_renderer.with_cause_chain = true; 390 writeln!(f)?; 391 for rel in related { 392 match rel.severity() { 393 Some(Severity::Error) | None => write!(f, "Error: ")?, 394 Some(Severity::Warning) => write!(f, "Warning: ")?, 395 Some(Severity::Advice) => write!(f, "Advice: ")?, 396 }; 397 inner_renderer.render_header(f, rel)?; 398 inner_renderer.render_causes(f, rel)?; 399 let src = rel.source_code().or(parent_src); 400 inner_renderer.render_snippets(f, rel, src)?; 401 inner_renderer.render_footer(f, rel)?; 402 inner_renderer.render_related(f, rel, src)?; 403 } 404 } 405 Ok(()) 406 } 407 408 fn render_snippets( 409 &self, 410 f: &mut impl fmt::Write, 411 diagnostic: &(dyn Diagnostic), 412 opt_source: Option<&dyn SourceCode>, 413 ) -> fmt::Result { 414 let source = match opt_source { 415 Some(source) => source, 416 None => return Ok(()), 417 }; 418 let labels = match diagnostic.labels() { 419 Some(labels) => labels, 420 None => return Ok(()), 421 }; 422 423 let mut labels = labels.collect::<Vec<_>>(); 424 labels.sort_unstable_by_key(|l| l.inner().offset()); 425 426 let mut contexts = Vec::with_capacity(labels.len()); 427 for right in labels.iter().cloned() { 428 let right_conts = source 429 .read_span(right.inner(), self.context_lines, self.context_lines) 430 .map_err(|_| fmt::Error)?; 431 432 if contexts.is_empty() { 433 contexts.push((right, right_conts)); 434 continue; 435 } 436 437 let (left, left_conts) = contexts.last().unwrap(); 438 if left_conts.line() + left_conts.line_count() >= right_conts.line() { 439 // The snippets will overlap, so we create one Big Chunky Boi 440 let left_end = left.offset() + left.len(); 441 let right_end = right.offset() + right.len(); 442 let new_end = std::cmp::max(left_end, right_end); 443 444 let new_span = LabeledSpan::new( 445 left.label().map(String::from), 446 left.offset(), 447 new_end - left.offset(), 448 ); 449 // Check that the two contexts can be combined 450 if let Ok(new_conts) = 451 source.read_span(new_span.inner(), self.context_lines, self.context_lines) 452 { 453 contexts.pop(); 454 // We'll throw the contents away later 455 contexts.push((new_span, new_conts)); 456 continue; 457 } 458 } 459 460 contexts.push((right, right_conts)); 461 } 462 for (ctx, _) in contexts { 463 self.render_context(f, source, &ctx, &labels[..])?; 464 } 465 466 Ok(()) 467 } 468 469 fn render_context( 470 &self, 471 f: &mut impl fmt::Write, 472 source: &dyn SourceCode, 473 context: &LabeledSpan, 474 labels: &[LabeledSpan], 475 ) -> fmt::Result { 476 let (contents, lines) = self.get_lines(source, context.inner())?; 477 478 // only consider labels from the context as primary label 479 let ctx_labels = labels.iter().filter(|l| { 480 context.inner().offset() <= l.inner().offset() 481 && l.inner().offset() + l.inner().len() 482 <= context.inner().offset() + context.inner().len() 483 }); 484 let primary_label = ctx_labels 485 .clone() 486 .find(|label| label.primary()) 487 .or_else(|| ctx_labels.clone().next()); 488 489 // sorting is your friend 490 let labels = labels 491 .iter() 492 .zip(self.theme.styles.highlights.iter().cloned().cycle()) 493 .map(|(label, st)| FancySpan::new(label.label().map(String::from), *label.inner(), st)) 494 .collect::<Vec<_>>(); 495 496 let mut highlighter_state = self.highlighter.start_highlighter_state(&*contents); 497 498 // The max number of gutter-lines that will be active at any given 499 // point. We need this to figure out indentation, so we do one loop 500 // over the lines to see what the damage is gonna be. 501 let mut max_gutter = 0usize; 502 for line in &lines { 503 let mut num_highlights = 0; 504 for hl in &labels { 505 if !line.span_line_only(hl) && line.span_applies_gutter(hl) { 506 num_highlights += 1; 507 } 508 } 509 max_gutter = std::cmp::max(max_gutter, num_highlights); 510 } 511 512 // Oh and one more thing: We need to figure out how much room our line 513 // numbers need! 514 let linum_width = lines[..] 515 .last() 516 .map(|line| line.line_number) 517 // It's possible for the source to be an empty string. 518 .unwrap_or(0) 519 .to_string() 520 .len(); 521 522 // Header 523 write!( 524 f, 525 "{}{}{}", 526 " ".repeat(linum_width + 2), 527 self.theme.characters.ltop, 528 self.theme.characters.hbar, 529 )?; 530 531 // If there is a primary label, then use its span 532 // as the reference point for line/column information. 533 let primary_contents = match primary_label { 534 Some(label) => source 535 .read_span(label.inner(), 0, 0) 536 .map_err(|_| fmt::Error)?, 537 None => contents, 538 }; 539 540 if let Some(source_name) = primary_contents.name() { 541 let source_name = source_name.style(self.theme.styles.link); 542 writeln!( 543 f, 544 "[{}:{}:{}]", 545 source_name, 546 primary_contents.line() + 1, 547 primary_contents.column() + 1 548 )?; 549 } else if lines.len() <= 1 { 550 writeln!(f, "{}", self.theme.characters.hbar.to_string().repeat(3))?; 551 } else { 552 writeln!( 553 f, 554 "[{}:{}]", 555 primary_contents.line() + 1, 556 primary_contents.column() + 1 557 )?; 558 } 559 560 // Now it's time for the fun part--actually rendering everything! 561 for line in &lines { 562 // Line number, appropriately padded. 563 self.write_linum(f, linum_width, line.line_number)?; 564 565 // Then, we need to print the gutter, along with any fly-bys We 566 // have separate gutters depending on whether we're on the actual 567 // line, or on one of the "highlight lines" below it. 568 self.render_line_gutter(f, max_gutter, line, &labels)?; 569 570 // And _now_ we can print out the line text itself! 571 let styled_text = 572 StyledList::from(highlighter_state.highlight_line(&line.text)).to_string(); 573 self.render_line_text(f, &styled_text)?; 574 575 // Next, we write all the highlights that apply to this particular line. 576 let (single_line, multi_line): (Vec<_>, Vec<_>) = labels 577 .iter() 578 .filter(|hl| line.span_applies(hl)) 579 .partition(|hl| line.span_line_only(hl)); 580 if !single_line.is_empty() { 581 // no line number! 582 self.write_no_linum(f, linum_width)?; 583 // gutter _again_ 584 self.render_highlight_gutter( 585 f, 586 max_gutter, 587 line, 588 &labels, 589 LabelRenderMode::SingleLine, 590 )?; 591 self.render_single_line_highlights( 592 f, 593 line, 594 linum_width, 595 max_gutter, 596 &single_line, 597 &labels, 598 )?; 599 } 600 for hl in multi_line { 601 if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) { 602 self.render_multi_line_end(f, &labels, max_gutter, linum_width, line, hl)?; 603 } 604 } 605 } 606 writeln!( 607 f, 608 "{}{}{}", 609 " ".repeat(linum_width + 2), 610 self.theme.characters.lbot, 611 self.theme.characters.hbar.to_string().repeat(4), 612 )?; 613 Ok(()) 614 } 615 616 fn render_multi_line_end( 617 &self, 618 f: &mut impl fmt::Write, 619 labels: &[FancySpan], 620 max_gutter: usize, 621 linum_width: usize, 622 line: &Line, 623 label: &FancySpan, 624 ) -> fmt::Result { 625 // no line number! 626 self.write_no_linum(f, linum_width)?; 627 628 if let Some(label_parts) = label.label_parts() { 629 // if it has a label, how long is it? 630 let (first, rest) = label_parts 631 .split_first() 632 .expect("cannot crash because rest would have been None, see docs on the `label` field of FancySpan"); 633 634 if rest.is_empty() { 635 // gutter _again_ 636 self.render_highlight_gutter( 637 f, 638 max_gutter, 639 line, 640 &labels, 641 LabelRenderMode::SingleLine, 642 )?; 643 644 self.render_multi_line_end_single( 645 f, 646 first, 647 label.style, 648 LabelRenderMode::SingleLine, 649 )?; 650 } else { 651 // gutter _again_ 652 self.render_highlight_gutter( 653 f, 654 max_gutter, 655 line, 656 &labels, 657 LabelRenderMode::MultiLineFirst, 658 )?; 659 660 self.render_multi_line_end_single( 661 f, 662 first, 663 label.style, 664 LabelRenderMode::MultiLineFirst, 665 )?; 666 for label_line in rest { 667 // no line number! 668 self.write_no_linum(f, linum_width)?; 669 // gutter _again_ 670 self.render_highlight_gutter( 671 f, 672 max_gutter, 673 line, 674 &labels, 675 LabelRenderMode::MultiLineRest, 676 )?; 677 self.render_multi_line_end_single( 678 f, 679 label_line, 680 label.style, 681 LabelRenderMode::MultiLineRest, 682 )?; 683 } 684 } 685 } else { 686 // gutter _again_ 687 self.render_highlight_gutter( 688 f, 689 max_gutter, 690 line, 691 &labels, 692 LabelRenderMode::SingleLine, 693 )?; 694 // has no label 695 writeln!(f, "{}", self.theme.characters.hbar.style(label.style))?; 696 } 697 698 Ok(()) 699 } 700 701 fn render_line_gutter( 702 &self, 703 f: &mut impl fmt::Write, 704 max_gutter: usize, 705 line: &Line, 706 highlights: &[FancySpan], 707 ) -> fmt::Result { 708 if max_gutter == 0 { 709 return Ok(()); 710 } 711 let chars = &self.theme.characters; 712 let mut gutter = String::new(); 713 let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl)); 714 let mut arrow = false; 715 for (i, hl) in applicable.enumerate() { 716 if line.span_starts(hl) { 717 gutter.push_str(&chars.ltop.style(hl.style).to_string()); 718 gutter.push_str( 719 &chars 720 .hbar 721 .to_string() 722 .repeat(max_gutter.saturating_sub(i)) 723 .style(hl.style) 724 .to_string(), 725 ); 726 gutter.push_str(&chars.rarrow.style(hl.style).to_string()); 727 arrow = true; 728 break; 729 } else if line.span_ends(hl) { 730 if hl.label().is_some() { 731 gutter.push_str(&chars.lcross.style(hl.style).to_string()); 732 } else { 733 gutter.push_str(&chars.lbot.style(hl.style).to_string()); 734 } 735 gutter.push_str( 736 &chars 737 .hbar 738 .to_string() 739 .repeat(max_gutter.saturating_sub(i)) 740 .style(hl.style) 741 .to_string(), 742 ); 743 gutter.push_str(&chars.rarrow.style(hl.style).to_string()); 744 arrow = true; 745 break; 746 } else if line.span_flyby(hl) { 747 gutter.push_str(&chars.vbar.style(hl.style).to_string()); 748 } else { 749 gutter.push(' '); 750 } 751 } 752 write!( 753 f, 754 "{}{}", 755 gutter, 756 " ".repeat( 757 if arrow { 1 } else { 3 } + max_gutter.saturating_sub(gutter.chars().count()) 758 ) 759 )?; 760 Ok(()) 761 } 762 763 fn render_highlight_gutter( 764 &self, 765 f: &mut impl fmt::Write, 766 max_gutter: usize, 767 line: &Line, 768 highlights: &[FancySpan], 769 render_mode: LabelRenderMode, 770 ) -> fmt::Result { 771 if max_gutter == 0 { 772 return Ok(()); 773 } 774 775 // keeps track of how many colums wide the gutter is 776 // important for ansi since simply measuring the size of the final string 777 // gives the wrong result when the string contains ansi codes. 778 let mut gutter_cols = 0; 779 780 let chars = &self.theme.characters; 781 let mut gutter = String::new(); 782 let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl)); 783 for (i, hl) in applicable.enumerate() { 784 if !line.span_line_only(hl) && line.span_ends(hl) { 785 if render_mode == LabelRenderMode::MultiLineRest { 786 // this is to make multiline labels work. We want to make the right amount 787 // of horizontal space for them, but not actually draw the lines 788 let horizontal_space = max_gutter.saturating_sub(i) + 2; 789 for _ in 0..horizontal_space { 790 gutter.push(' '); 791 } 792 // account for one more horizontal space, since in multiline mode 793 // we also add in the vertical line before the label like this: 794 // 2 │ ╭─▶ text 795 // 3 │ ├─▶ here 796 // · ╰──┤ these two lines 797 // · │ are the problem 798 // ^this 799 gutter_cols += horizontal_space + 1; 800 } else { 801 let num_repeat = max_gutter.saturating_sub(i) + 2; 802 803 gutter.push_str(&chars.lbot.style(hl.style).to_string()); 804 805 gutter.push_str( 806 &chars 807 .hbar 808 .to_string() 809 .repeat( 810 num_repeat 811 // if we are rendering a multiline label, then leave a bit of space for the 812 // rcross character 813 - if render_mode == LabelRenderMode::MultiLineFirst { 814 1 815 } else { 816 0 817 }, 818 ) 819 .style(hl.style) 820 .to_string(), 821 ); 822 823 // we count 1 for the lbot char, and then a few more, the same number 824 // as we just repeated for. For each repeat we only add 1, even though 825 // due to ansi escape codes the number of bytes in the string could grow 826 // a lot each time. 827 gutter_cols += num_repeat + 1; 828 } 829 break; 830 } else { 831 gutter.push_str(&chars.vbar.style(hl.style).to_string()); 832 833 // we may push many bytes for the ansi escape codes style adds, 834 // but we still only add a single character-width to the string in a terminal 835 gutter_cols += 1; 836 } 837 } 838 839 // now calculate how many spaces to add based on how many columns we just created. 840 // it's the max width of the gutter, minus how many character-widths we just generated 841 // capped at 0 (though this should never go below in reality), and then we add 3 to 842 // account for arrowheads when a gutter line ends 843 let num_spaces = (max_gutter + 3).saturating_sub(gutter_cols); 844 // we then write the gutter and as many spaces as we need 845 write!(f, "{}{:width$}", gutter, "", width = num_spaces)?; 846 Ok(()) 847 } 848 849 fn wrap(&self, text: &str, opts: textwrap::Options<'_>) -> String { 850 if self.wrap_lines { 851 textwrap::fill(text, opts) 852 } else { 853 // Format without wrapping, but retain the indentation options 854 // Implementation based on `textwrap::indent` 855 let mut result = String::with_capacity(2 * text.len()); 856 let trimmed_indent = opts.subsequent_indent.trim_end(); 857 for (idx, line) in text.split_terminator('\n').enumerate() { 858 if idx > 0 { 859 result.push('\n'); 860 } 861 if idx == 0 { 862 if line.trim().is_empty() { 863 result.push_str(opts.initial_indent.trim_end()); 864 } else { 865 result.push_str(opts.initial_indent); 866 } 867 } else { 868 if line.trim().is_empty() { 869 result.push_str(trimmed_indent); 870 } else { 871 result.push_str(opts.subsequent_indent); 872 } 873 } 874 result.push_str(line); 875 } 876 if text.ends_with('\n') { 877 // split_terminator will have eaten the final '\n'. 878 result.push('\n'); 879 } 880 result 881 } 882 } 883 884 fn write_linum(&self, f: &mut impl fmt::Write, width: usize, linum: usize) -> fmt::Result { 885 write!( 886 f, 887 " {:width$} {} ", 888 linum.style(self.theme.styles.linum), 889 self.theme.characters.vbar, 890 width = width 891 )?; 892 Ok(()) 893 } 894 895 fn write_no_linum(&self, f: &mut impl fmt::Write, width: usize) -> fmt::Result { 896 write!( 897 f, 898 " {:width$} {} ", 899 "", 900 self.theme.characters.vbar_break, 901 width = width 902 )?; 903 Ok(()) 904 } 905 906 /// Returns an iterator over the visual width of each character in a line. 907 fn line_visual_char_width<'a>(&self, text: &'a str) -> impl Iterator<Item = usize> + 'a { 908 let mut column = 0; 909 let mut escaped = false; 910 let tab_width = self.tab_width; 911 text.chars().map(move |c| { 912 let width = match (escaped, c) { 913 // Round up to the next multiple of tab_width 914 (false, '\t') => tab_width - column % tab_width, 915 // start of ANSI escape 916 (false, '\x1b') => { 917 escaped = true; 918 0 919 } 920 // use Unicode width for all other characters 921 (false, c) => c.width().unwrap_or(0), 922 // end of ANSI escape 923 (true, 'm') => { 924 escaped = false; 925 0 926 } 927 // characters are zero width within escape sequence 928 (true, _) => 0, 929 }; 930 column += width; 931 width 932 }) 933 } 934 935 /// Returns the visual column position of a byte offset on a specific line. 936 /// 937 /// If the offset occurs in the middle of a character, the returned column 938 /// corresponds to that character's first column in `start` is true, or its 939 /// last column if `start` is false. 940 fn visual_offset(&self, line: &Line, offset: usize, start: bool) -> usize { 941 let line_range = line.offset..=(line.offset + line.length); 942 assert!(line_range.contains(&offset)); 943 944 let mut text_index = offset - line.offset; 945 while text_index <= line.text.len() && !line.text.is_char_boundary(text_index) { 946 if start { 947 text_index -= 1; 948 } else { 949 text_index += 1; 950 } 951 } 952 let text = &line.text[..text_index.min(line.text.len())]; 953 let text_width = self.line_visual_char_width(text).sum(); 954 if text_index > line.text.len() { 955 // Spans extending past the end of the line are always rendered as 956 // one column past the end of the visible line. 957 // 958 // This doesn't necessarily correspond to a specific byte-offset, 959 // since a span extending past the end of the line could contain: 960 // - an actual \n character (1 byte) 961 // - a CRLF (2 bytes) 962 // - EOF (0 bytes) 963 text_width + 1 964 } else { 965 text_width 966 } 967 } 968 969 /// Renders a line to the output formatter, replacing tabs with spaces. 970 fn render_line_text(&self, f: &mut impl fmt::Write, text: &str) -> fmt::Result { 971 for (c, width) in text.chars().zip(self.line_visual_char_width(text)) { 972 if c == '\t' { 973 for _ in 0..width { 974 f.write_char(' ')?; 975 } 976 } else { 977 f.write_char(c)?; 978 } 979 } 980 f.write_char('\n')?; 981 Ok(()) 982 } 983 984 fn render_single_line_highlights( 985 &self, 986 f: &mut impl fmt::Write, 987 line: &Line, 988 linum_width: usize, 989 max_gutter: usize, 990 single_liners: &[&FancySpan], 991 all_highlights: &[FancySpan], 992 ) -> fmt::Result { 993 let mut underlines = String::new(); 994 let mut highest = 0; 995 996 let chars = &self.theme.characters; 997 let vbar_offsets: Vec<_> = single_liners 998 .iter() 999 .map(|hl| { 1000 let byte_start = hl.offset(); 1001 let byte_end = hl.offset() + hl.len(); 1002 let start = self.visual_offset(line, byte_start, true).max(highest); 1003 let end = if hl.len() == 0 { 1004 start + 1 1005 } else { 1006 self.visual_offset(line, byte_end, false).max(start + 1) 1007 }; 1008 1009 let vbar_offset = (start + end) / 2; 1010 let num_left = vbar_offset - start; 1011 let num_right = end - vbar_offset - 1; 1012 underlines.push_str( 1013 &format!( 1014 "{:width$}{}{}{}", 1015 "", 1016 chars.underline.to_string().repeat(num_left), 1017 if hl.len() == 0 { 1018 chars.uarrow 1019 } else if hl.label().is_some() { 1020 chars.underbar 1021 } else { 1022 chars.underline 1023 }, 1024 chars.underline.to_string().repeat(num_right), 1025 width = start.saturating_sub(highest), 1026 ) 1027 .style(hl.style) 1028 .to_string(), 1029 ); 1030 highest = std::cmp::max(highest, end); 1031 1032 (hl, vbar_offset) 1033 }) 1034 .collect(); 1035 writeln!(f, "{}", underlines)?; 1036 1037 for hl in single_liners.iter().rev() { 1038 if let Some(label) = hl.label_parts() { 1039 if label.len() == 1 { 1040 self.write_label_text( 1041 f, 1042 line, 1043 linum_width, 1044 max_gutter, 1045 all_highlights, 1046 chars, 1047 &vbar_offsets, 1048 hl, 1049 &label[0], 1050 LabelRenderMode::SingleLine, 1051 )?; 1052 } else { 1053 let mut first = true; 1054 for label_line in &label { 1055 self.write_label_text( 1056 f, 1057 line, 1058 linum_width, 1059 max_gutter, 1060 all_highlights, 1061 chars, 1062 &vbar_offsets, 1063 hl, 1064 label_line, 1065 if first { 1066 LabelRenderMode::MultiLineFirst 1067 } else { 1068 LabelRenderMode::MultiLineRest 1069 }, 1070 )?; 1071 first = false; 1072 } 1073 } 1074 } 1075 } 1076 Ok(()) 1077 } 1078 1079 // I know it's not good practice, but making this a function makes a lot of sense 1080 // and making a struct for this does not... 1081 #[allow(clippy::too_many_arguments)] 1082 fn write_label_text( 1083 &self, 1084 f: &mut impl fmt::Write, 1085 line: &Line, 1086 linum_width: usize, 1087 max_gutter: usize, 1088 all_highlights: &[FancySpan], 1089 chars: &ThemeCharacters, 1090 vbar_offsets: &[(&&FancySpan, usize)], 1091 hl: &&FancySpan, 1092 label: &str, 1093 render_mode: LabelRenderMode, 1094 ) -> fmt::Result { 1095 self.write_no_linum(f, linum_width)?; 1096 self.render_highlight_gutter( 1097 f, 1098 max_gutter, 1099 line, 1100 all_highlights, 1101 LabelRenderMode::SingleLine, 1102 )?; 1103 let mut curr_offset = 1usize; 1104 for (offset_hl, vbar_offset) in vbar_offsets { 1105 while curr_offset < *vbar_offset + 1 { 1106 write!(f, " ")?; 1107 curr_offset += 1; 1108 } 1109 if *offset_hl != hl { 1110 write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?; 1111 curr_offset += 1; 1112 } else { 1113 let lines = match render_mode { 1114 LabelRenderMode::SingleLine => format!( 1115 "{}{} {}", 1116 chars.lbot, 1117 chars.hbar.to_string().repeat(2), 1118 label, 1119 ), 1120 LabelRenderMode::MultiLineFirst => { 1121 format!("{}{}{} {}", chars.lbot, chars.hbar, chars.rcross, label,) 1122 } 1123 LabelRenderMode::MultiLineRest => { 1124 format!(" {} {}", chars.vbar, label,) 1125 } 1126 }; 1127 writeln!(f, "{}", lines.style(hl.style))?; 1128 break; 1129 } 1130 } 1131 Ok(()) 1132 } 1133 1134 fn render_multi_line_end_single( 1135 &self, 1136 f: &mut impl fmt::Write, 1137 label: &str, 1138 style: Style, 1139 render_mode: LabelRenderMode, 1140 ) -> fmt::Result { 1141 match render_mode { 1142 LabelRenderMode::SingleLine => { 1143 writeln!(f, "{} {}", self.theme.characters.hbar.style(style), label)?; 1144 } 1145 LabelRenderMode::MultiLineFirst => { 1146 writeln!(f, "{} {}", self.theme.characters.rcross.style(style), label)?; 1147 } 1148 LabelRenderMode::MultiLineRest => { 1149 writeln!(f, "{} {}", self.theme.characters.vbar.style(style), label)?; 1150 } 1151 } 1152 1153 Ok(()) 1154 } 1155 1156 fn get_lines<'a>( 1157 &'a self, 1158 source: &'a dyn SourceCode, 1159 context_span: &'a SourceSpan, 1160 ) -> Result<(Box<dyn SpanContents<'a> + 'a>, Vec<Line>), fmt::Error> { 1161 let context_data = source 1162 .read_span(context_span, self.context_lines, self.context_lines) 1163 .map_err(|_| fmt::Error)?; 1164 let context = std::str::from_utf8(context_data.data()).expect("Bad utf8 detected"); 1165 let mut line = context_data.line(); 1166 let mut column = context_data.column(); 1167 let mut offset = context_data.span().offset(); 1168 let mut line_offset = offset; 1169 let mut iter = context.chars().peekable(); 1170 let mut line_str = String::new(); 1171 let mut lines = Vec::new(); 1172 while let Some(char) = iter.next() { 1173 offset += char.len_utf8(); 1174 let mut at_end_of_file = false; 1175 match char { 1176 '\r' => { 1177 if iter.next_if_eq(&'\n').is_some() { 1178 offset += 1; 1179 line += 1; 1180 column = 0; 1181 } else { 1182 line_str.push(char); 1183 column += 1; 1184 } 1185 at_end_of_file = iter.peek().is_none(); 1186 } 1187 '\n' => { 1188 at_end_of_file = iter.peek().is_none(); 1189 line += 1; 1190 column = 0; 1191 } 1192 _ => { 1193 line_str.push(char); 1194 column += 1; 1195 } 1196 } 1197 1198 if iter.peek().is_none() && !at_end_of_file { 1199 line += 1; 1200 } 1201 1202 if column == 0 || iter.peek().is_none() { 1203 lines.push(Line { 1204 line_number: line, 1205 offset: line_offset, 1206 length: offset - line_offset, 1207 text: line_str.clone(), 1208 }); 1209 line_str.clear(); 1210 line_offset = offset; 1211 } 1212 } 1213 Ok((context_data, lines)) 1214 } 1215 } 1216 1217 impl ReportHandler for GraphicalReportHandler { 1218 fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result { 1219 if f.alternate() { 1220 return fmt::Debug::fmt(diagnostic, f); 1221 } 1222 1223 self.render_report(f, diagnostic) 1224 } 1225 } 1226 1227 /* 1228 Support types 1229 */ 1230 1231 #[derive(PartialEq, Debug)] 1232 enum LabelRenderMode { 1233 /// we're rendering a single line label (or not rendering in any special way) 1234 SingleLine, 1235 /// we're rendering a multiline label 1236 MultiLineFirst, 1237 /// we're rendering the rest of a multiline label 1238 MultiLineRest, 1239 } 1240 1241 #[derive(Debug)] 1242 struct Line { 1243 line_number: usize, 1244 offset: usize, 1245 length: usize, 1246 text: String, 1247 } 1248 1249 impl Line { 1250 fn span_line_only(&self, span: &FancySpan) -> bool { 1251 span.offset() >= self.offset && span.offset() + span.len() <= self.offset + self.length 1252 } 1253 1254 /// Returns whether `span` should be visible on this line, either in the gutter or under the 1255 /// text on this line 1256 fn span_applies(&self, span: &FancySpan) -> bool { 1257 let spanlen = if span.len() == 0 { 1 } else { span.len() }; 1258 // Span starts in this line 1259 1260 (span.offset() >= self.offset && span.offset() < self.offset + self.length) 1261 // Span passes through this line 1262 || (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo 1263 // Span ends on this line 1264 || (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length) 1265 } 1266 1267 /// Returns whether `span` should be visible on this line in the gutter (so this excludes spans 1268 /// that are only visible on this line and do not span multiple lines) 1269 fn span_applies_gutter(&self, span: &FancySpan) -> bool { 1270 let spanlen = if span.len() == 0 { 1 } else { span.len() }; 1271 // Span starts in this line 1272 self.span_applies(span) 1273 && !( 1274 // as long as it doesn't start *and* end on this line 1275 (span.offset() >= self.offset && span.offset() < self.offset + self.length) 1276 && (span.offset() + spanlen > self.offset 1277 && span.offset() + spanlen <= self.offset + self.length) 1278 ) 1279 } 1280 1281 // A 'flyby' is a multi-line span that technically covers this line, but 1282 // does not begin or end within the line itself. This method is used to 1283 // calculate gutters. 1284 fn span_flyby(&self, span: &FancySpan) -> bool { 1285 // The span itself starts before this line's starting offset (so, in a 1286 // prev line). 1287 span.offset() < self.offset 1288 // ...and it stops after this line's end. 1289 && span.offset() + span.len() > self.offset + self.length 1290 } 1291 1292 // Does this line contain the *beginning* of this multiline span? 1293 // This assumes self.span_applies() is true already. 1294 fn span_starts(&self, span: &FancySpan) -> bool { 1295 span.offset() >= self.offset 1296 } 1297 1298 // Does this line contain the *end* of this multiline span? 1299 // This assumes self.span_applies() is true already. 1300 fn span_ends(&self, span: &FancySpan) -> bool { 1301 span.offset() + span.len() >= self.offset 1302 && span.offset() + span.len() <= self.offset + self.length 1303 } 1304 } 1305 1306 #[derive(Debug, Clone)] 1307 struct FancySpan { 1308 /// this is deliberately an option of a vec because I wanted to be very explicit 1309 /// that there can also be *no* label. If there is a label, it can have multiple 1310 /// lines which is what the vec is for. 1311 label: Option<Vec<String>>, 1312 span: SourceSpan, 1313 style: Style, 1314 } 1315 1316 impl PartialEq for FancySpan { 1317 fn eq(&self, other: &Self) -> bool { 1318 self.label == other.label && self.span == other.span 1319 } 1320 } 1321 1322 fn split_label(v: String) -> Vec<String> { 1323 v.split('\n').map(|i| i.to_string()).collect() 1324 } 1325 1326 impl FancySpan { 1327 fn new(label: Option<String>, span: SourceSpan, style: Style) -> Self { 1328 FancySpan { 1329 label: label.map(split_label), 1330 span, 1331 style, 1332 } 1333 } 1334 1335 fn style(&self) -> Style { 1336 self.style 1337 } 1338 1339 fn label(&self) -> Option<String> { 1340 self.label 1341 .as_ref() 1342 .map(|l| l.join("\n").style(self.style()).to_string()) 1343 } 1344 1345 fn label_parts(&self) -> Option<Vec<String>> { 1346 self.label.as_ref().map(|l| { 1347 l.iter() 1348 .map(|i| i.style(self.style()).to_string()) 1349 .collect() 1350 }) 1351 } 1352 1353 fn offset(&self) -> usize { 1354 self.span.offset() 1355 } 1356 1357 fn len(&self) -> usize { 1358 self.span.len() 1359 } 1360 } 1361