1 use std::fmt::{self, Write}; 2 3 use owo_colors::{OwoColorize, Style}; 4 use unicode_width::UnicodeWidthChar; 5 6 use crate::diagnostic_chain::{DiagnosticChain, ErrorKind}; 7 use crate::handlers::theme::*; 8 use crate::protocol::{Diagnostic, Severity}; 9 use crate::{LabeledSpan, MietteError, ReportHandler, SourceCode, SourceSpan, SpanContents}; 10 11 /** 12 A [`ReportHandler`] that displays a given [`Report`](crate::Report) in a 13 quasi-graphical way, using terminal colors, unicode drawing characters, and 14 other such things. 15 16 This is the default reporter bundled with `miette`. 17 18 This printer can be customized by using [`new_themed()`](GraphicalReportHandler::new_themed) and handing it a 19 [`GraphicalTheme`] of your own creation (or using one of its own defaults!) 20 21 See [`set_hook()`](crate::set_hook) for more details on customizing your global 22 printer. 23 */ 24 #[derive(Debug, Clone)] 25 pub struct GraphicalReportHandler { 26 pub(crate) links: LinkStyle, 27 pub(crate) termwidth: usize, 28 pub(crate) theme: GraphicalTheme, 29 pub(crate) footer: Option<String>, 30 pub(crate) context_lines: usize, 31 pub(crate) tab_width: usize, 32 pub(crate) with_cause_chain: bool, 33 } 34 35 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 36 pub(crate) enum LinkStyle { 37 None, 38 Link, 39 Text, 40 } 41 42 impl GraphicalReportHandler { 43 /// Create a new `GraphicalReportHandler` with the default 44 /// [`GraphicalTheme`]. This will use both unicode characters and colors. new() -> Self45 pub fn new() -> Self { 46 Self { 47 links: LinkStyle::Link, 48 termwidth: 200, 49 theme: GraphicalTheme::default(), 50 footer: None, 51 context_lines: 1, 52 tab_width: 4, 53 with_cause_chain: true, 54 } 55 } 56 57 ///Create a new `GraphicalReportHandler` with a given [`GraphicalTheme`]. new_themed(theme: GraphicalTheme) -> Self58 pub fn new_themed(theme: GraphicalTheme) -> Self { 59 Self { 60 links: LinkStyle::Link, 61 termwidth: 200, 62 theme, 63 footer: None, 64 context_lines: 1, 65 tab_width: 4, 66 with_cause_chain: true, 67 } 68 } 69 70 /// Set the displayed tab width in spaces. tab_width(mut self, width: usize) -> Self71 pub fn tab_width(mut self, width: usize) -> Self { 72 self.tab_width = width; 73 self 74 } 75 76 /// Whether to enable error code linkification using [`Diagnostic::url()`]. with_links(mut self, links: bool) -> Self77 pub fn with_links(mut self, links: bool) -> Self { 78 self.links = if links { 79 LinkStyle::Link 80 } else { 81 LinkStyle::Text 82 }; 83 self 84 } 85 86 /// Include the cause chain of the top-level error in the graphical output, 87 /// if available. with_cause_chain(mut self) -> Self88 pub fn with_cause_chain(mut self) -> Self { 89 self.with_cause_chain = true; 90 self 91 } 92 93 /// Do not include the cause chain of the top-level error in the graphical 94 /// output. without_cause_chain(mut self) -> Self95 pub fn without_cause_chain(mut self) -> Self { 96 self.with_cause_chain = false; 97 self 98 } 99 100 /// Whether to include [`Diagnostic::url()`] in the output. 101 /// 102 /// Disabling this is not recommended, but can be useful for more easily 103 /// reproducible tests, as `url(docsrs)` links are version-dependent. with_urls(mut self, urls: bool) -> Self104 pub fn with_urls(mut self, urls: bool) -> Self { 105 self.links = match (self.links, urls) { 106 (_, false) => LinkStyle::None, 107 (LinkStyle::None, true) => LinkStyle::Link, 108 (links, true) => links, 109 }; 110 self 111 } 112 113 /// Set a theme for this handler. with_theme(mut self, theme: GraphicalTheme) -> Self114 pub fn with_theme(mut self, theme: GraphicalTheme) -> Self { 115 self.theme = theme; 116 self 117 } 118 119 /// Sets the width to wrap the report at. with_width(mut self, width: usize) -> Self120 pub fn with_width(mut self, width: usize) -> Self { 121 self.termwidth = width; 122 self 123 } 124 125 /// Sets the 'global' footer for this handler. with_footer(mut self, footer: String) -> Self126 pub fn with_footer(mut self, footer: String) -> Self { 127 self.footer = Some(footer); 128 self 129 } 130 131 /// Sets the number of lines of context to show around each error. with_context_lines(mut self, lines: usize) -> Self132 pub fn with_context_lines(mut self, lines: usize) -> Self { 133 self.context_lines = lines; 134 self 135 } 136 } 137 138 impl Default for GraphicalReportHandler { default() -> Self139 fn default() -> Self { 140 Self::new() 141 } 142 } 143 144 impl GraphicalReportHandler { 145 /// Render a [`Diagnostic`]. This function is mostly internal and meant to 146 /// be called by the toplevel [`ReportHandler`] handler, but is made public 147 /// to make it easier (possible) to test in isolation from global state. render_report( &self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic), ) -> fmt::Result148 pub fn render_report( 149 &self, 150 f: &mut impl fmt::Write, 151 diagnostic: &(dyn Diagnostic), 152 ) -> fmt::Result { 153 self.render_header(f, diagnostic)?; 154 self.render_causes(f, diagnostic)?; 155 let src = diagnostic.source_code(); 156 self.render_snippets(f, diagnostic, src)?; 157 self.render_footer(f, diagnostic)?; 158 self.render_related(f, diagnostic, src)?; 159 if let Some(footer) = &self.footer { 160 writeln!(f)?; 161 let width = self.termwidth.saturating_sub(4); 162 let opts = textwrap::Options::new(width) 163 .initial_indent(" ") 164 .subsequent_indent(" "); 165 writeln!(f, "{}", textwrap::fill(footer, opts))?; 166 } 167 Ok(()) 168 } 169 render_header(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result170 fn render_header(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result { 171 let severity_style = match diagnostic.severity() { 172 Some(Severity::Error) | None => self.theme.styles.error, 173 Some(Severity::Warning) => self.theme.styles.warning, 174 Some(Severity::Advice) => self.theme.styles.advice, 175 }; 176 let mut header = String::new(); 177 if self.links == LinkStyle::Link && diagnostic.url().is_some() { 178 let url = diagnostic.url().unwrap(); // safe 179 let code = if let Some(code) = diagnostic.code() { 180 format!("{} ", code) 181 } else { 182 "".to_string() 183 }; 184 let link = format!( 185 "\u{1b}]8;;{}\u{1b}\\{}{}\u{1b}]8;;\u{1b}\\", 186 url, 187 code.style(severity_style), 188 "(link)".style(self.theme.styles.link) 189 ); 190 write!(header, "{}", link)?; 191 writeln!(f, "{}", header)?; 192 writeln!(f)?; 193 } else if let Some(code) = diagnostic.code() { 194 write!(header, "{}", code.style(severity_style),)?; 195 if self.links == LinkStyle::Text && diagnostic.url().is_some() { 196 let url = diagnostic.url().unwrap(); // safe 197 write!(header, " ({})", url.style(self.theme.styles.link))?; 198 } 199 writeln!(f, "{}", header)?; 200 writeln!(f)?; 201 } 202 Ok(()) 203 } 204 205 fn render_causes(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result { 206 let (severity_style, severity_icon) = match diagnostic.severity() { 207 Some(Severity::Error) | None => (self.theme.styles.error, &self.theme.characters.error), 208 Some(Severity::Warning) => (self.theme.styles.warning, &self.theme.characters.warning), 209 Some(Severity::Advice) => (self.theme.styles.advice, &self.theme.characters.advice), 210 }; 211 212 let initial_indent = format!(" {} ", severity_icon.style(severity_style)); 213 let rest_indent = format!(" {} ", self.theme.characters.vbar.style(severity_style)); 214 let width = self.termwidth.saturating_sub(2); 215 let opts = textwrap::Options::new(width) 216 .initial_indent(&initial_indent) 217 .subsequent_indent(&rest_indent); 218 219 writeln!(f, "{}", textwrap::fill(&diagnostic.to_string(), opts))?; 220 221 if !self.with_cause_chain { 222 return Ok(()); 223 } 224 225 if let Some(mut cause_iter) = diagnostic 226 .diagnostic_source() 227 .map(DiagnosticChain::from_diagnostic) 228 .or_else(|| diagnostic.source().map(DiagnosticChain::from_stderror)) 229 .map(|it| it.peekable()) 230 { 231 while let Some(error) = cause_iter.next() { 232 let is_last = cause_iter.peek().is_none(); 233 let char = if !is_last { 234 self.theme.characters.lcross 235 } else { 236 self.theme.characters.lbot 237 }; 238 let initial_indent = format!( 239 " {}{}{} ", 240 char, self.theme.characters.hbar, self.theme.characters.rarrow 241 ) 242 .style(severity_style) 243 .to_string(); 244 let rest_indent = format!( 245 " {} ", 246 if is_last { 247 ' ' 248 } else { 249 self.theme.characters.vbar 250 } 251 ) 252 .style(severity_style) 253 .to_string(); 254 let opts = textwrap::Options::new(width) 255 .initial_indent(&initial_indent) 256 .subsequent_indent(&rest_indent); 257 match error { 258 ErrorKind::Diagnostic(diag) => { 259 let mut inner = String::new(); 260 261 // Don't print footer for inner errors 262 let mut inner_renderer = self.clone(); 263 inner_renderer.footer = None; 264 inner_renderer.with_cause_chain = false; 265 inner_renderer.render_report(&mut inner, diag)?; 266 267 writeln!(f, "{}", textwrap::fill(&inner, opts))?; 268 } 269 ErrorKind::StdError(err) => { 270 writeln!(f, "{}", textwrap::fill(&err.to_string(), opts))?; 271 } 272 } 273 } 274 } 275 276 Ok(()) 277 } 278 279 fn render_footer(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result { 280 if let Some(help) = diagnostic.help() { 281 let width = self.termwidth.saturating_sub(4); 282 let initial_indent = " help: ".style(self.theme.styles.help).to_string(); 283 let opts = textwrap::Options::new(width) 284 .initial_indent(&initial_indent) 285 .subsequent_indent(" "); 286 writeln!(f, "{}", textwrap::fill(&help.to_string(), opts))?; 287 } 288 Ok(()) 289 } 290 291 fn render_related( 292 &self, 293 f: &mut impl fmt::Write, 294 diagnostic: &(dyn Diagnostic), 295 parent_src: Option<&dyn SourceCode>, 296 ) -> fmt::Result { 297 if let Some(related) = diagnostic.related() { 298 writeln!(f)?; 299 for rel in related { 300 match rel.severity() { 301 Some(Severity::Error) | None => write!(f, "Error: ")?, 302 Some(Severity::Warning) => write!(f, "Warning: ")?, 303 Some(Severity::Advice) => write!(f, "Advice: ")?, 304 }; 305 self.render_header(f, rel)?; 306 self.render_causes(f, rel)?; 307 let src = rel.source_code().or(parent_src); 308 self.render_snippets(f, rel, src)?; 309 self.render_footer(f, rel)?; 310 self.render_related(f, rel, src)?; 311 } 312 } 313 Ok(()) 314 } 315 316 fn render_snippets( 317 &self, 318 f: &mut impl fmt::Write, 319 diagnostic: &(dyn Diagnostic), 320 opt_source: Option<&dyn SourceCode>, 321 ) -> fmt::Result { 322 if let Some(source) = opt_source { 323 if let Some(labels) = diagnostic.labels() { 324 let mut labels = labels.collect::<Vec<_>>(); 325 labels.sort_unstable_by_key(|l| l.inner().offset()); 326 if !labels.is_empty() { 327 let contents = labels 328 .iter() 329 .map(|label| { 330 source.read_span(label.inner(), self.context_lines, self.context_lines) 331 }) 332 .collect::<Result<Vec<Box<dyn SpanContents<'_>>>, MietteError>>() 333 .map_err(|_| fmt::Error)?; 334 let mut contexts = Vec::with_capacity(contents.len()); 335 for (right, right_conts) in labels.iter().cloned().zip(contents.iter()) { 336 if contexts.is_empty() { 337 contexts.push((right, right_conts)); 338 } else { 339 let (left, left_conts) = contexts.last().unwrap().clone(); 340 let left_end = left.offset() + left.len(); 341 let right_end = right.offset() + right.len(); 342 if left_conts.line() + left_conts.line_count() >= right_conts.line() { 343 // The snippets will overlap, so we create one Big Chunky Boi 344 let new_span = LabeledSpan::new( 345 left.label().map(String::from), 346 left.offset(), 347 if right_end >= left_end { 348 // Right end goes past left end 349 right_end - left.offset() 350 } else { 351 // right is contained inside left 352 left.len() 353 }, 354 ); 355 if source 356 .read_span( 357 new_span.inner(), 358 self.context_lines, 359 self.context_lines, 360 ) 361 .is_ok() 362 { 363 contexts.pop(); 364 contexts.push(( 365 // We'll throw this away later 366 new_span, left_conts, 367 )); 368 } else { 369 contexts.push((right, right_conts)); 370 } 371 } else { 372 contexts.push((right, right_conts)); 373 } 374 } 375 } 376 for (ctx, _) in contexts { 377 self.render_context(f, source, &ctx, &labels[..])?; 378 } 379 } 380 } 381 } 382 Ok(()) 383 } 384 385 fn render_context<'a>( 386 &self, 387 f: &mut impl fmt::Write, 388 source: &'a dyn SourceCode, 389 context: &LabeledSpan, 390 labels: &[LabeledSpan], 391 ) -> fmt::Result { 392 let (contents, lines) = self.get_lines(source, context.inner())?; 393 394 // sorting is your friend 395 let labels = labels 396 .iter() 397 .zip(self.theme.styles.highlights.iter().cloned().cycle()) 398 .map(|(label, st)| FancySpan::new(label.label().map(String::from), *label.inner(), st)) 399 .collect::<Vec<_>>(); 400 401 // The max number of gutter-lines that will be active at any given 402 // point. We need this to figure out indentation, so we do one loop 403 // over the lines to see what the damage is gonna be. 404 let mut max_gutter = 0usize; 405 for line in &lines { 406 let mut num_highlights = 0; 407 for hl in &labels { 408 if !line.span_line_only(hl) && line.span_applies(hl) { 409 num_highlights += 1; 410 } 411 } 412 max_gutter = std::cmp::max(max_gutter, num_highlights); 413 } 414 415 // Oh and one more thing: We need to figure out how much room our line 416 // numbers need! 417 let linum_width = lines[..] 418 .last() 419 .map(|line| line.line_number) 420 // It's possible for the source to be an empty string. 421 .unwrap_or(0) 422 .to_string() 423 .len(); 424 425 // Header 426 write!( 427 f, 428 "{}{}{}", 429 " ".repeat(linum_width + 2), 430 self.theme.characters.ltop, 431 self.theme.characters.hbar, 432 )?; 433 434 if let Some(source_name) = contents.name() { 435 let source_name = source_name.style(self.theme.styles.link); 436 writeln!( 437 f, 438 "[{}:{}:{}]", 439 source_name, 440 contents.line() + 1, 441 contents.column() + 1 442 )?; 443 } else if lines.len() <= 1 { 444 writeln!(f, "{}", self.theme.characters.hbar.to_string().repeat(3))?; 445 } else { 446 writeln!(f, "[{}:{}]", contents.line() + 1, contents.column() + 1)?; 447 } 448 449 // Now it's time for the fun part--actually rendering everything! 450 for line in &lines { 451 // Line number, appropriately padded. 452 self.write_linum(f, linum_width, line.line_number)?; 453 454 // Then, we need to print the gutter, along with any fly-bys We 455 // have separate gutters depending on whether we're on the actual 456 // line, or on one of the "highlight lines" below it. 457 self.render_line_gutter(f, max_gutter, line, &labels)?; 458 459 // And _now_ we can print out the line text itself! 460 self.render_line_text(f, &line.text)?; 461 462 // Next, we write all the highlights that apply to this particular line. 463 let (single_line, multi_line): (Vec<_>, Vec<_>) = labels 464 .iter() 465 .filter(|hl| line.span_applies(hl)) 466 .partition(|hl| line.span_line_only(hl)); 467 if !single_line.is_empty() { 468 // no line number! 469 self.write_no_linum(f, linum_width)?; 470 // gutter _again_ 471 self.render_highlight_gutter(f, max_gutter, line, &labels)?; 472 self.render_single_line_highlights( 473 f, 474 line, 475 linum_width, 476 max_gutter, 477 &single_line, 478 &labels, 479 )?; 480 } 481 for hl in multi_line { 482 if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) { 483 // no line number! 484 self.write_no_linum(f, linum_width)?; 485 // gutter _again_ 486 self.render_highlight_gutter(f, max_gutter, line, &labels)?; 487 self.render_multi_line_end(f, hl)?; 488 } 489 } 490 } 491 writeln!( 492 f, 493 "{}{}{}", 494 " ".repeat(linum_width + 2), 495 self.theme.characters.lbot, 496 self.theme.characters.hbar.to_string().repeat(4), 497 )?; 498 Ok(()) 499 } 500 501 fn render_line_gutter( 502 &self, 503 f: &mut impl fmt::Write, 504 max_gutter: usize, 505 line: &Line, 506 highlights: &[FancySpan], 507 ) -> fmt::Result { 508 if max_gutter == 0 { 509 return Ok(()); 510 } 511 let chars = &self.theme.characters; 512 let mut gutter = String::new(); 513 let applicable = highlights.iter().filter(|hl| line.span_applies(hl)); 514 let mut arrow = false; 515 for (i, hl) in applicable.enumerate() { 516 if line.span_starts(hl) { 517 gutter.push_str(&chars.ltop.style(hl.style).to_string()); 518 gutter.push_str( 519 &chars 520 .hbar 521 .to_string() 522 .repeat(max_gutter.saturating_sub(i)) 523 .style(hl.style) 524 .to_string(), 525 ); 526 gutter.push_str(&chars.rarrow.style(hl.style).to_string()); 527 arrow = true; 528 break; 529 } else if line.span_ends(hl) { 530 if hl.label().is_some() { 531 gutter.push_str(&chars.lcross.style(hl.style).to_string()); 532 } else { 533 gutter.push_str(&chars.lbot.style(hl.style).to_string()); 534 } 535 gutter.push_str( 536 &chars 537 .hbar 538 .to_string() 539 .repeat(max_gutter.saturating_sub(i)) 540 .style(hl.style) 541 .to_string(), 542 ); 543 gutter.push_str(&chars.rarrow.style(hl.style).to_string()); 544 arrow = true; 545 break; 546 } else if line.span_flyby(hl) { 547 gutter.push_str(&chars.vbar.style(hl.style).to_string()); 548 } else { 549 gutter.push(' '); 550 } 551 } 552 write!( 553 f, 554 "{}{}", 555 gutter, 556 " ".repeat( 557 if arrow { 1 } else { 3 } + max_gutter.saturating_sub(gutter.chars().count()) 558 ) 559 )?; 560 Ok(()) 561 } 562 563 fn render_highlight_gutter( 564 &self, 565 f: &mut impl fmt::Write, 566 max_gutter: usize, 567 line: &Line, 568 highlights: &[FancySpan], 569 ) -> fmt::Result { 570 if max_gutter == 0 { 571 return Ok(()); 572 } 573 let chars = &self.theme.characters; 574 let mut gutter = String::new(); 575 let applicable = highlights.iter().filter(|hl| line.span_applies(hl)); 576 for (i, hl) in applicable.enumerate() { 577 if !line.span_line_only(hl) && line.span_ends(hl) { 578 gutter.push_str(&chars.lbot.style(hl.style).to_string()); 579 gutter.push_str( 580 &chars 581 .hbar 582 .to_string() 583 .repeat(max_gutter.saturating_sub(i) + 2) 584 .style(hl.style) 585 .to_string(), 586 ); 587 break; 588 } else { 589 gutter.push_str(&chars.vbar.style(hl.style).to_string()); 590 } 591 } 592 write!(f, "{:width$}", gutter, width = max_gutter + 1)?; 593 Ok(()) 594 } 595 596 fn write_linum(&self, f: &mut impl fmt::Write, width: usize, linum: usize) -> fmt::Result { 597 write!( 598 f, 599 " {:width$} {} ", 600 linum.style(self.theme.styles.linum), 601 self.theme.characters.vbar, 602 width = width 603 )?; 604 Ok(()) 605 } 606 607 fn write_no_linum(&self, f: &mut impl fmt::Write, width: usize) -> fmt::Result { 608 write!( 609 f, 610 " {:width$} {} ", 611 "", 612 self.theme.characters.vbar_break, 613 width = width 614 )?; 615 Ok(()) 616 } 617 618 /// Returns an iterator over the visual width of each character in a line. 619 fn line_visual_char_width<'a>(&self, text: &'a str) -> impl Iterator<Item = usize> + 'a { 620 let mut column = 0; 621 let tab_width = self.tab_width; 622 text.chars().map(move |c| { 623 let width = if c == '\t' { 624 // Round up to the next multiple of tab_width 625 tab_width - column % tab_width 626 } else { 627 c.width().unwrap_or(0) 628 }; 629 column += width; 630 width 631 }) 632 } 633 634 /// Returns the visual column position of a byte offset on a specific line. 635 fn visual_offset(&self, line: &Line, offset: usize) -> usize { 636 let line_range = line.offset..=(line.offset + line.length); 637 assert!(line_range.contains(&offset)); 638 639 let text_index = offset - line.offset; 640 let text = &line.text[..text_index.min(line.text.len())]; 641 let text_width = self.line_visual_char_width(text).sum(); 642 if text_index > line.text.len() { 643 // Spans extending past the end of the line are always rendered as 644 // one column past the end of the visible line. 645 // 646 // This doesn't necessarily correspond to a specific byte-offset, 647 // since a span extending past the end of the line could contain: 648 // - an actual \n character (1 byte) 649 // - a CRLF (2 bytes) 650 // - EOF (0 bytes) 651 text_width + 1 652 } else { 653 text_width 654 } 655 } 656 657 /// Renders a line to the output formatter, replacing tabs with spaces. 658 fn render_line_text(&self, f: &mut impl fmt::Write, text: &str) -> fmt::Result { 659 for (c, width) in text.chars().zip(self.line_visual_char_width(text)) { 660 if c == '\t' { 661 for _ in 0..width { 662 f.write_char(' ')? 663 } 664 } else { 665 f.write_char(c)? 666 } 667 } 668 f.write_char('\n')?; 669 Ok(()) 670 } 671 672 fn render_single_line_highlights( 673 &self, 674 f: &mut impl fmt::Write, 675 line: &Line, 676 linum_width: usize, 677 max_gutter: usize, 678 single_liners: &[&FancySpan], 679 all_highlights: &[FancySpan], 680 ) -> fmt::Result { 681 let mut underlines = String::new(); 682 let mut highest = 0; 683 684 let chars = &self.theme.characters; 685 let vbar_offsets: Vec<_> = single_liners 686 .iter() 687 .map(|hl| { 688 let byte_start = hl.offset(); 689 let byte_end = hl.offset() + hl.len(); 690 let start = self.visual_offset(line, byte_start).max(highest); 691 let end = self.visual_offset(line, byte_end).max(start + 1); 692 693 let vbar_offset = (start + end) / 2; 694 let num_left = vbar_offset - start; 695 let num_right = end - vbar_offset - 1; 696 if start < end { 697 underlines.push_str( 698 &format!( 699 "{:width$}{}{}{}", 700 "", 701 chars.underline.to_string().repeat(num_left), 702 if hl.len() == 0 { 703 chars.uarrow 704 } else if hl.label().is_some() { 705 chars.underbar 706 } else { 707 chars.underline 708 }, 709 chars.underline.to_string().repeat(num_right), 710 width = start.saturating_sub(highest), 711 ) 712 .style(hl.style) 713 .to_string(), 714 ); 715 } 716 highest = std::cmp::max(highest, end); 717 718 (hl, vbar_offset) 719 }) 720 .collect(); 721 writeln!(f, "{}", underlines)?; 722 723 for hl in single_liners.iter().rev() { 724 if let Some(label) = hl.label() { 725 self.write_no_linum(f, linum_width)?; 726 self.render_highlight_gutter(f, max_gutter, line, all_highlights)?; 727 let mut curr_offset = 1usize; 728 for (offset_hl, vbar_offset) in &vbar_offsets { 729 while curr_offset < *vbar_offset + 1 { 730 write!(f, " ")?; 731 curr_offset += 1; 732 } 733 if *offset_hl != hl { 734 write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?; 735 curr_offset += 1; 736 } else { 737 let lines = format!( 738 "{}{} {}", 739 chars.lbot, 740 chars.hbar.to_string().repeat(2), 741 label, 742 ); 743 writeln!(f, "{}", lines.style(hl.style))?; 744 break; 745 } 746 } 747 } 748 } 749 Ok(()) 750 } 751 752 fn render_multi_line_end(&self, f: &mut impl fmt::Write, hl: &FancySpan) -> fmt::Result { 753 writeln!( 754 f, 755 "{} {}", 756 self.theme.characters.hbar.style(hl.style), 757 hl.label().unwrap_or_else(|| "".into()), 758 )?; 759 Ok(()) 760 } 761 762 fn get_lines<'a>( 763 &'a self, 764 source: &'a dyn SourceCode, 765 context_span: &'a SourceSpan, 766 ) -> Result<(Box<dyn SpanContents<'a> + 'a>, Vec<Line>), fmt::Error> { 767 let context_data = source 768 .read_span(context_span, self.context_lines, self.context_lines) 769 .map_err(|_| fmt::Error)?; 770 let context = std::str::from_utf8(context_data.data()).expect("Bad utf8 detected"); 771 let mut line = context_data.line(); 772 let mut column = context_data.column(); 773 let mut offset = context_data.span().offset(); 774 let mut line_offset = offset; 775 let mut iter = context.chars().peekable(); 776 let mut line_str = String::new(); 777 let mut lines = Vec::new(); 778 while let Some(char) = iter.next() { 779 offset += char.len_utf8(); 780 let mut at_end_of_file = false; 781 match char { 782 '\r' => { 783 if iter.next_if_eq(&'\n').is_some() { 784 offset += 1; 785 line += 1; 786 column = 0; 787 } else { 788 line_str.push(char); 789 column += 1; 790 } 791 at_end_of_file = iter.peek().is_none(); 792 } 793 '\n' => { 794 at_end_of_file = iter.peek().is_none(); 795 line += 1; 796 column = 0; 797 } 798 _ => { 799 line_str.push(char); 800 column += 1; 801 } 802 } 803 804 if iter.peek().is_none() && !at_end_of_file { 805 line += 1; 806 } 807 808 if column == 0 || iter.peek().is_none() { 809 lines.push(Line { 810 line_number: line, 811 offset: line_offset, 812 length: offset - line_offset, 813 text: line_str.clone(), 814 }); 815 line_str.clear(); 816 line_offset = offset; 817 } 818 } 819 Ok((context_data, lines)) 820 } 821 } 822 823 impl ReportHandler for GraphicalReportHandler { 824 fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result { 825 if f.alternate() { 826 return fmt::Debug::fmt(diagnostic, f); 827 } 828 829 self.render_report(f, diagnostic) 830 } 831 } 832 833 /* 834 Support types 835 */ 836 837 #[derive(Debug)] 838 struct Line { 839 line_number: usize, 840 offset: usize, 841 length: usize, 842 text: String, 843 } 844 845 impl Line { 846 fn span_line_only(&self, span: &FancySpan) -> bool { 847 span.offset() >= self.offset && span.offset() + span.len() <= self.offset + self.length 848 } 849 850 fn span_applies(&self, span: &FancySpan) -> bool { 851 let spanlen = if span.len() == 0 { 1 } else { span.len() }; 852 // Span starts in this line 853 (span.offset() >= self.offset && span.offset() < self.offset + self.length) 854 // Span passes through this line 855 || (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo 856 // Span ends on this line 857 || (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length) 858 } 859 860 // A 'flyby' is a multi-line span that technically covers this line, but 861 // does not begin or end within the line itself. This method is used to 862 // calculate gutters. 863 fn span_flyby(&self, span: &FancySpan) -> bool { 864 // The span itself starts before this line's starting offset (so, in a 865 // prev line). 866 span.offset() < self.offset 867 // ...and it stops after this line's end. 868 && span.offset() + span.len() > self.offset + self.length 869 } 870 871 // Does this line contain the *beginning* of this multiline span? 872 // This assumes self.span_applies() is true already. 873 fn span_starts(&self, span: &FancySpan) -> bool { 874 span.offset() >= self.offset 875 } 876 877 // Does this line contain the *end* of this multiline span? 878 // This assumes self.span_applies() is true already. 879 fn span_ends(&self, span: &FancySpan) -> bool { 880 span.offset() + span.len() >= self.offset 881 && span.offset() + span.len() <= self.offset + self.length 882 } 883 } 884 885 #[derive(Debug, Clone)] 886 struct FancySpan { 887 label: Option<String>, 888 span: SourceSpan, 889 style: Style, 890 } 891 892 impl PartialEq for FancySpan { 893 fn eq(&self, other: &Self) -> bool { 894 self.label == other.label && self.span == other.span 895 } 896 } 897 898 impl FancySpan { 899 fn new(label: Option<String>, span: SourceSpan, style: Style) -> Self { 900 FancySpan { label, span, style } 901 } 902 903 fn style(&self) -> Style { 904 self.style 905 } 906 907 fn label(&self) -> Option<String> { 908 self.label 909 .as_ref() 910 .map(|l| l.style(self.style()).to_string()) 911 } 912 913 fn offset(&self) -> usize { 914 self.span.offset() 915 } 916 917 fn len(&self) -> usize { 918 self.span.len() 919 } 920 } 921