| use std::fmt::{self, Write}; |
| |
| use owo_colors::{OwoColorize, Style}; |
| use unicode_width::UnicodeWidthChar; |
| |
| use crate::diagnostic_chain::{DiagnosticChain, ErrorKind}; |
| use crate::handlers::theme::*; |
| use crate::protocol::{Diagnostic, Severity}; |
| use crate::{LabeledSpan, MietteError, ReportHandler, SourceCode, SourceSpan, SpanContents}; |
| |
| /** |
| A [`ReportHandler`] that displays a given [`Report`](crate::Report) in a |
| quasi-graphical way, using terminal colors, unicode drawing characters, and |
| other such things. |
| |
| This is the default reporter bundled with `miette`. |
| |
| This printer can be customized by using [`new_themed()`](GraphicalReportHandler::new_themed) and handing it a |
| [`GraphicalTheme`] of your own creation (or using one of its own defaults!) |
| |
| See [`set_hook()`](crate::set_hook) for more details on customizing your global |
| printer. |
| */ |
| #[derive(Debug, Clone)] |
| pub struct GraphicalReportHandler { |
| pub(crate) links: LinkStyle, |
| pub(crate) termwidth: usize, |
| pub(crate) theme: GraphicalTheme, |
| pub(crate) footer: Option<String>, |
| pub(crate) context_lines: usize, |
| pub(crate) tab_width: usize, |
| pub(crate) with_cause_chain: bool, |
| } |
| |
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| pub(crate) enum LinkStyle { |
| None, |
| Link, |
| Text, |
| } |
| |
| impl GraphicalReportHandler { |
| /// Create a new `GraphicalReportHandler` with the default |
| /// [`GraphicalTheme`]. This will use both unicode characters and colors. |
| pub fn new() -> Self { |
| Self { |
| links: LinkStyle::Link, |
| termwidth: 200, |
| theme: GraphicalTheme::default(), |
| footer: None, |
| context_lines: 1, |
| tab_width: 4, |
| with_cause_chain: true, |
| } |
| } |
| |
| ///Create a new `GraphicalReportHandler` with a given [`GraphicalTheme`]. |
| pub fn new_themed(theme: GraphicalTheme) -> Self { |
| Self { |
| links: LinkStyle::Link, |
| termwidth: 200, |
| theme, |
| footer: None, |
| context_lines: 1, |
| tab_width: 4, |
| with_cause_chain: true, |
| } |
| } |
| |
| /// Set the displayed tab width in spaces. |
| pub fn tab_width(mut self, width: usize) -> Self { |
| self.tab_width = width; |
| self |
| } |
| |
| /// Whether to enable error code linkification using [`Diagnostic::url()`]. |
| pub fn with_links(mut self, links: bool) -> Self { |
| self.links = if links { |
| LinkStyle::Link |
| } else { |
| LinkStyle::Text |
| }; |
| self |
| } |
| |
| /// Include the cause chain of the top-level error in the graphical output, |
| /// if available. |
| pub fn with_cause_chain(mut self) -> Self { |
| self.with_cause_chain = true; |
| self |
| } |
| |
| /// Do not include the cause chain of the top-level error in the graphical |
| /// output. |
| pub fn without_cause_chain(mut self) -> Self { |
| self.with_cause_chain = false; |
| self |
| } |
| |
| /// Whether to include [`Diagnostic::url()`] in the output. |
| /// |
| /// Disabling this is not recommended, but can be useful for more easily |
| /// reproducible tests, as `url(docsrs)` links are version-dependent. |
| pub fn with_urls(mut self, urls: bool) -> Self { |
| self.links = match (self.links, urls) { |
| (_, false) => LinkStyle::None, |
| (LinkStyle::None, true) => LinkStyle::Link, |
| (links, true) => links, |
| }; |
| self |
| } |
| |
| /// Set a theme for this handler. |
| pub fn with_theme(mut self, theme: GraphicalTheme) -> Self { |
| self.theme = theme; |
| self |
| } |
| |
| /// Sets the width to wrap the report at. |
| pub fn with_width(mut self, width: usize) -> Self { |
| self.termwidth = width; |
| self |
| } |
| |
| /// Sets the 'global' footer for this handler. |
| pub fn with_footer(mut self, footer: String) -> Self { |
| self.footer = Some(footer); |
| self |
| } |
| |
| /// Sets the number of lines of context to show around each error. |
| pub fn with_context_lines(mut self, lines: usize) -> Self { |
| self.context_lines = lines; |
| self |
| } |
| } |
| |
| impl Default for GraphicalReportHandler { |
| fn default() -> Self { |
| Self::new() |
| } |
| } |
| |
| impl GraphicalReportHandler { |
| /// Render a [`Diagnostic`]. This function is mostly internal and meant to |
| /// be called by the toplevel [`ReportHandler`] handler, but is made public |
| /// to make it easier (possible) to test in isolation from global state. |
| pub fn render_report( |
| &self, |
| f: &mut impl fmt::Write, |
| diagnostic: &(dyn Diagnostic), |
| ) -> fmt::Result { |
| self.render_header(f, diagnostic)?; |
| self.render_causes(f, diagnostic)?; |
| let src = diagnostic.source_code(); |
| self.render_snippets(f, diagnostic, src)?; |
| self.render_footer(f, diagnostic)?; |
| self.render_related(f, diagnostic, src)?; |
| if let Some(footer) = &self.footer { |
| writeln!(f)?; |
| let width = self.termwidth.saturating_sub(4); |
| let opts = textwrap::Options::new(width) |
| .initial_indent(" ") |
| .subsequent_indent(" "); |
| writeln!(f, "{}", textwrap::fill(footer, opts))?; |
| } |
| Ok(()) |
| } |
| |
| fn render_header(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result { |
| let severity_style = match diagnostic.severity() { |
| Some(Severity::Error) | None => self.theme.styles.error, |
| Some(Severity::Warning) => self.theme.styles.warning, |
| Some(Severity::Advice) => self.theme.styles.advice, |
| }; |
| let mut header = String::new(); |
| if self.links == LinkStyle::Link && diagnostic.url().is_some() { |
| let url = diagnostic.url().unwrap(); // safe |
| let code = if let Some(code) = diagnostic.code() { |
| format!("{} ", code) |
| } else { |
| "".to_string() |
| }; |
| let link = format!( |
| "\u{1b}]8;;{}\u{1b}\\{}{}\u{1b}]8;;\u{1b}\\", |
| url, |
| code.style(severity_style), |
| "(link)".style(self.theme.styles.link) |
| ); |
| write!(header, "{}", link)?; |
| writeln!(f, "{}", header)?; |
| writeln!(f)?; |
| } else if let Some(code) = diagnostic.code() { |
| write!(header, "{}", code.style(severity_style),)?; |
| if self.links == LinkStyle::Text && diagnostic.url().is_some() { |
| let url = diagnostic.url().unwrap(); // safe |
| write!(header, " ({})", url.style(self.theme.styles.link))?; |
| } |
| writeln!(f, "{}", header)?; |
| writeln!(f)?; |
| } |
| Ok(()) |
| } |
| |
| fn render_causes(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result { |
| let (severity_style, severity_icon) = match diagnostic.severity() { |
| Some(Severity::Error) | None => (self.theme.styles.error, &self.theme.characters.error), |
| Some(Severity::Warning) => (self.theme.styles.warning, &self.theme.characters.warning), |
| Some(Severity::Advice) => (self.theme.styles.advice, &self.theme.characters.advice), |
| }; |
| |
| let initial_indent = format!(" {} ", severity_icon.style(severity_style)); |
| let rest_indent = format!(" {} ", self.theme.characters.vbar.style(severity_style)); |
| let width = self.termwidth.saturating_sub(2); |
| let opts = textwrap::Options::new(width) |
| .initial_indent(&initial_indent) |
| .subsequent_indent(&rest_indent); |
| |
| writeln!(f, "{}", textwrap::fill(&diagnostic.to_string(), opts))?; |
| |
| if !self.with_cause_chain { |
| return Ok(()); |
| } |
| |
| if let Some(mut cause_iter) = diagnostic |
| .diagnostic_source() |
| .map(DiagnosticChain::from_diagnostic) |
| .or_else(|| diagnostic.source().map(DiagnosticChain::from_stderror)) |
| .map(|it| it.peekable()) |
| { |
| while let Some(error) = cause_iter.next() { |
| let is_last = cause_iter.peek().is_none(); |
| let char = if !is_last { |
| self.theme.characters.lcross |
| } else { |
| self.theme.characters.lbot |
| }; |
| let initial_indent = format!( |
| " {}{}{} ", |
| char, self.theme.characters.hbar, self.theme.characters.rarrow |
| ) |
| .style(severity_style) |
| .to_string(); |
| let rest_indent = format!( |
| " {} ", |
| if is_last { |
| ' ' |
| } else { |
| self.theme.characters.vbar |
| } |
| ) |
| .style(severity_style) |
| .to_string(); |
| let opts = textwrap::Options::new(width) |
| .initial_indent(&initial_indent) |
| .subsequent_indent(&rest_indent); |
| match error { |
| ErrorKind::Diagnostic(diag) => { |
| let mut inner = String::new(); |
| |
| // Don't print footer for inner errors |
| let mut inner_renderer = self.clone(); |
| inner_renderer.footer = None; |
| inner_renderer.with_cause_chain = false; |
| inner_renderer.render_report(&mut inner, diag)?; |
| |
| writeln!(f, "{}", textwrap::fill(&inner, opts))?; |
| } |
| ErrorKind::StdError(err) => { |
| writeln!(f, "{}", textwrap::fill(&err.to_string(), opts))?; |
| } |
| } |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| fn render_footer(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result { |
| if let Some(help) = diagnostic.help() { |
| let width = self.termwidth.saturating_sub(4); |
| let initial_indent = " help: ".style(self.theme.styles.help).to_string(); |
| let opts = textwrap::Options::new(width) |
| .initial_indent(&initial_indent) |
| .subsequent_indent(" "); |
| writeln!(f, "{}", textwrap::fill(&help.to_string(), opts))?; |
| } |
| Ok(()) |
| } |
| |
| fn render_related( |
| &self, |
| f: &mut impl fmt::Write, |
| diagnostic: &(dyn Diagnostic), |
| parent_src: Option<&dyn SourceCode>, |
| ) -> fmt::Result { |
| if let Some(related) = diagnostic.related() { |
| writeln!(f)?; |
| for rel in related { |
| match rel.severity() { |
| Some(Severity::Error) | None => write!(f, "Error: ")?, |
| Some(Severity::Warning) => write!(f, "Warning: ")?, |
| Some(Severity::Advice) => write!(f, "Advice: ")?, |
| }; |
| self.render_header(f, rel)?; |
| self.render_causes(f, rel)?; |
| let src = rel.source_code().or(parent_src); |
| self.render_snippets(f, rel, src)?; |
| self.render_footer(f, rel)?; |
| self.render_related(f, rel, src)?; |
| } |
| } |
| Ok(()) |
| } |
| |
| fn render_snippets( |
| &self, |
| f: &mut impl fmt::Write, |
| diagnostic: &(dyn Diagnostic), |
| opt_source: Option<&dyn SourceCode>, |
| ) -> fmt::Result { |
| if let Some(source) = opt_source { |
| if let Some(labels) = diagnostic.labels() { |
| let mut labels = labels.collect::<Vec<_>>(); |
| labels.sort_unstable_by_key(|l| l.inner().offset()); |
| if !labels.is_empty() { |
| let contents = labels |
| .iter() |
| .map(|label| { |
| source.read_span(label.inner(), self.context_lines, self.context_lines) |
| }) |
| .collect::<Result<Vec<Box<dyn SpanContents<'_>>>, MietteError>>() |
| .map_err(|_| fmt::Error)?; |
| let mut contexts = Vec::with_capacity(contents.len()); |
| for (right, right_conts) in labels.iter().cloned().zip(contents.iter()) { |
| if contexts.is_empty() { |
| contexts.push((right, right_conts)); |
| } else { |
| let (left, left_conts) = contexts.last().unwrap().clone(); |
| let left_end = left.offset() + left.len(); |
| let right_end = right.offset() + right.len(); |
| if left_conts.line() + left_conts.line_count() >= right_conts.line() { |
| // The snippets will overlap, so we create one Big Chunky Boi |
| let new_span = LabeledSpan::new( |
| left.label().map(String::from), |
| left.offset(), |
| if right_end >= left_end { |
| // Right end goes past left end |
| right_end - left.offset() |
| } else { |
| // right is contained inside left |
| left.len() |
| }, |
| ); |
| if source |
| .read_span( |
| new_span.inner(), |
| self.context_lines, |
| self.context_lines, |
| ) |
| .is_ok() |
| { |
| contexts.pop(); |
| contexts.push(( |
| // We'll throw this away later |
| new_span, left_conts, |
| )); |
| } else { |
| contexts.push((right, right_conts)); |
| } |
| } else { |
| contexts.push((right, right_conts)); |
| } |
| } |
| } |
| for (ctx, _) in contexts { |
| self.render_context(f, source, &ctx, &labels[..])?; |
| } |
| } |
| } |
| } |
| Ok(()) |
| } |
| |
| fn render_context<'a>( |
| &self, |
| f: &mut impl fmt::Write, |
| source: &'a dyn SourceCode, |
| context: &LabeledSpan, |
| labels: &[LabeledSpan], |
| ) -> fmt::Result { |
| let (contents, lines) = self.get_lines(source, context.inner())?; |
| |
| // sorting is your friend |
| let labels = labels |
| .iter() |
| .zip(self.theme.styles.highlights.iter().cloned().cycle()) |
| .map(|(label, st)| FancySpan::new(label.label().map(String::from), *label.inner(), st)) |
| .collect::<Vec<_>>(); |
| |
| // The max number of gutter-lines that will be active at any given |
| // point. We need this to figure out indentation, so we do one loop |
| // over the lines to see what the damage is gonna be. |
| let mut max_gutter = 0usize; |
| for line in &lines { |
| let mut num_highlights = 0; |
| for hl in &labels { |
| if !line.span_line_only(hl) && line.span_applies(hl) { |
| num_highlights += 1; |
| } |
| } |
| max_gutter = std::cmp::max(max_gutter, num_highlights); |
| } |
| |
| // Oh and one more thing: We need to figure out how much room our line |
| // numbers need! |
| let linum_width = lines[..] |
| .last() |
| .map(|line| line.line_number) |
| // It's possible for the source to be an empty string. |
| .unwrap_or(0) |
| .to_string() |
| .len(); |
| |
| // Header |
| write!( |
| f, |
| "{}{}{}", |
| " ".repeat(linum_width + 2), |
| self.theme.characters.ltop, |
| self.theme.characters.hbar, |
| )?; |
| |
| if let Some(source_name) = contents.name() { |
| let source_name = source_name.style(self.theme.styles.link); |
| writeln!( |
| f, |
| "[{}:{}:{}]", |
| source_name, |
| contents.line() + 1, |
| contents.column() + 1 |
| )?; |
| } else if lines.len() <= 1 { |
| writeln!(f, "{}", self.theme.characters.hbar.to_string().repeat(3))?; |
| } else { |
| writeln!(f, "[{}:{}]", contents.line() + 1, contents.column() + 1)?; |
| } |
| |
| // Now it's time for the fun part--actually rendering everything! |
| for line in &lines { |
| // Line number, appropriately padded. |
| self.write_linum(f, linum_width, line.line_number)?; |
| |
| // Then, we need to print the gutter, along with any fly-bys We |
| // have separate gutters depending on whether we're on the actual |
| // line, or on one of the "highlight lines" below it. |
| self.render_line_gutter(f, max_gutter, line, &labels)?; |
| |
| // And _now_ we can print out the line text itself! |
| self.render_line_text(f, &line.text)?; |
| |
| // Next, we write all the highlights that apply to this particular line. |
| let (single_line, multi_line): (Vec<_>, Vec<_>) = labels |
| .iter() |
| .filter(|hl| line.span_applies(hl)) |
| .partition(|hl| line.span_line_only(hl)); |
| if !single_line.is_empty() { |
| // no line number! |
| self.write_no_linum(f, linum_width)?; |
| // gutter _again_ |
| self.render_highlight_gutter(f, max_gutter, line, &labels)?; |
| self.render_single_line_highlights( |
| f, |
| line, |
| linum_width, |
| max_gutter, |
| &single_line, |
| &labels, |
| )?; |
| } |
| for hl in multi_line { |
| if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) { |
| // no line number! |
| self.write_no_linum(f, linum_width)?; |
| // gutter _again_ |
| self.render_highlight_gutter(f, max_gutter, line, &labels)?; |
| self.render_multi_line_end(f, hl)?; |
| } |
| } |
| } |
| writeln!( |
| f, |
| "{}{}{}", |
| " ".repeat(linum_width + 2), |
| self.theme.characters.lbot, |
| self.theme.characters.hbar.to_string().repeat(4), |
| )?; |
| Ok(()) |
| } |
| |
| fn render_line_gutter( |
| &self, |
| f: &mut impl fmt::Write, |
| max_gutter: usize, |
| line: &Line, |
| highlights: &[FancySpan], |
| ) -> fmt::Result { |
| if max_gutter == 0 { |
| return Ok(()); |
| } |
| let chars = &self.theme.characters; |
| let mut gutter = String::new(); |
| let applicable = highlights.iter().filter(|hl| line.span_applies(hl)); |
| let mut arrow = false; |
| for (i, hl) in applicable.enumerate() { |
| if line.span_starts(hl) { |
| gutter.push_str(&chars.ltop.style(hl.style).to_string()); |
| gutter.push_str( |
| &chars |
| .hbar |
| .to_string() |
| .repeat(max_gutter.saturating_sub(i)) |
| .style(hl.style) |
| .to_string(), |
| ); |
| gutter.push_str(&chars.rarrow.style(hl.style).to_string()); |
| arrow = true; |
| break; |
| } else if line.span_ends(hl) { |
| if hl.label().is_some() { |
| gutter.push_str(&chars.lcross.style(hl.style).to_string()); |
| } else { |
| gutter.push_str(&chars.lbot.style(hl.style).to_string()); |
| } |
| gutter.push_str( |
| &chars |
| .hbar |
| .to_string() |
| .repeat(max_gutter.saturating_sub(i)) |
| .style(hl.style) |
| .to_string(), |
| ); |
| gutter.push_str(&chars.rarrow.style(hl.style).to_string()); |
| arrow = true; |
| break; |
| } else if line.span_flyby(hl) { |
| gutter.push_str(&chars.vbar.style(hl.style).to_string()); |
| } else { |
| gutter.push(' '); |
| } |
| } |
| write!( |
| f, |
| "{}{}", |
| gutter, |
| " ".repeat( |
| if arrow { 1 } else { 3 } + max_gutter.saturating_sub(gutter.chars().count()) |
| ) |
| )?; |
| Ok(()) |
| } |
| |
| fn render_highlight_gutter( |
| &self, |
| f: &mut impl fmt::Write, |
| max_gutter: usize, |
| line: &Line, |
| highlights: &[FancySpan], |
| ) -> fmt::Result { |
| if max_gutter == 0 { |
| return Ok(()); |
| } |
| let chars = &self.theme.characters; |
| let mut gutter = String::new(); |
| let applicable = highlights.iter().filter(|hl| line.span_applies(hl)); |
| for (i, hl) in applicable.enumerate() { |
| if !line.span_line_only(hl) && line.span_ends(hl) { |
| gutter.push_str(&chars.lbot.style(hl.style).to_string()); |
| gutter.push_str( |
| &chars |
| .hbar |
| .to_string() |
| .repeat(max_gutter.saturating_sub(i) + 2) |
| .style(hl.style) |
| .to_string(), |
| ); |
| break; |
| } else { |
| gutter.push_str(&chars.vbar.style(hl.style).to_string()); |
| } |
| } |
| write!(f, "{:width$}", gutter, width = max_gutter + 1)?; |
| Ok(()) |
| } |
| |
| fn write_linum(&self, f: &mut impl fmt::Write, width: usize, linum: usize) -> fmt::Result { |
| write!( |
| f, |
| " {:width$} {} ", |
| linum.style(self.theme.styles.linum), |
| self.theme.characters.vbar, |
| width = width |
| )?; |
| Ok(()) |
| } |
| |
| fn write_no_linum(&self, f: &mut impl fmt::Write, width: usize) -> fmt::Result { |
| write!( |
| f, |
| " {:width$} {} ", |
| "", |
| self.theme.characters.vbar_break, |
| width = width |
| )?; |
| Ok(()) |
| } |
| |
| /// Returns an iterator over the visual width of each character in a line. |
| fn line_visual_char_width<'a>(&self, text: &'a str) -> impl Iterator<Item = usize> + 'a { |
| let mut column = 0; |
| let tab_width = self.tab_width; |
| text.chars().map(move |c| { |
| let width = if c == '\t' { |
| // Round up to the next multiple of tab_width |
| tab_width - column % tab_width |
| } else { |
| c.width().unwrap_or(0) |
| }; |
| column += width; |
| width |
| }) |
| } |
| |
| /// Returns the visual column position of a byte offset on a specific line. |
| fn visual_offset(&self, line: &Line, offset: usize) -> usize { |
| let line_range = line.offset..=(line.offset + line.length); |
| assert!(line_range.contains(&offset)); |
| |
| let text_index = offset - line.offset; |
| let text = &line.text[..text_index.min(line.text.len())]; |
| let text_width = self.line_visual_char_width(text).sum(); |
| if text_index > line.text.len() { |
| // Spans extending past the end of the line are always rendered as |
| // one column past the end of the visible line. |
| // |
| // This doesn't necessarily correspond to a specific byte-offset, |
| // since a span extending past the end of the line could contain: |
| // - an actual \n character (1 byte) |
| // - a CRLF (2 bytes) |
| // - EOF (0 bytes) |
| text_width + 1 |
| } else { |
| text_width |
| } |
| } |
| |
| /// Renders a line to the output formatter, replacing tabs with spaces. |
| fn render_line_text(&self, f: &mut impl fmt::Write, text: &str) -> fmt::Result { |
| for (c, width) in text.chars().zip(self.line_visual_char_width(text)) { |
| if c == '\t' { |
| for _ in 0..width { |
| f.write_char(' ')? |
| } |
| } else { |
| f.write_char(c)? |
| } |
| } |
| f.write_char('\n')?; |
| Ok(()) |
| } |
| |
| fn render_single_line_highlights( |
| &self, |
| f: &mut impl fmt::Write, |
| line: &Line, |
| linum_width: usize, |
| max_gutter: usize, |
| single_liners: &[&FancySpan], |
| all_highlights: &[FancySpan], |
| ) -> fmt::Result { |
| let mut underlines = String::new(); |
| let mut highest = 0; |
| |
| let chars = &self.theme.characters; |
| let vbar_offsets: Vec<_> = single_liners |
| .iter() |
| .map(|hl| { |
| let byte_start = hl.offset(); |
| let byte_end = hl.offset() + hl.len(); |
| let start = self.visual_offset(line, byte_start).max(highest); |
| let end = self.visual_offset(line, byte_end).max(start + 1); |
| |
| let vbar_offset = (start + end) / 2; |
| let num_left = vbar_offset - start; |
| let num_right = end - vbar_offset - 1; |
| if start < end { |
| underlines.push_str( |
| &format!( |
| "{:width$}{}{}{}", |
| "", |
| chars.underline.to_string().repeat(num_left), |
| if hl.len() == 0 { |
| chars.uarrow |
| } else if hl.label().is_some() { |
| chars.underbar |
| } else { |
| chars.underline |
| }, |
| chars.underline.to_string().repeat(num_right), |
| width = start.saturating_sub(highest), |
| ) |
| .style(hl.style) |
| .to_string(), |
| ); |
| } |
| highest = std::cmp::max(highest, end); |
| |
| (hl, vbar_offset) |
| }) |
| .collect(); |
| writeln!(f, "{}", underlines)?; |
| |
| for hl in single_liners.iter().rev() { |
| if let Some(label) = hl.label() { |
| self.write_no_linum(f, linum_width)?; |
| self.render_highlight_gutter(f, max_gutter, line, all_highlights)?; |
| let mut curr_offset = 1usize; |
| for (offset_hl, vbar_offset) in &vbar_offsets { |
| while curr_offset < *vbar_offset + 1 { |
| write!(f, " ")?; |
| curr_offset += 1; |
| } |
| if *offset_hl != hl { |
| write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?; |
| curr_offset += 1; |
| } else { |
| let lines = format!( |
| "{}{} {}", |
| chars.lbot, |
| chars.hbar.to_string().repeat(2), |
| label, |
| ); |
| writeln!(f, "{}", lines.style(hl.style))?; |
| break; |
| } |
| } |
| } |
| } |
| Ok(()) |
| } |
| |
| fn render_multi_line_end(&self, f: &mut impl fmt::Write, hl: &FancySpan) -> fmt::Result { |
| writeln!( |
| f, |
| "{} {}", |
| self.theme.characters.hbar.style(hl.style), |
| hl.label().unwrap_or_else(|| "".into()), |
| )?; |
| Ok(()) |
| } |
| |
| fn get_lines<'a>( |
| &'a self, |
| source: &'a dyn SourceCode, |
| context_span: &'a SourceSpan, |
| ) -> Result<(Box<dyn SpanContents<'a> + 'a>, Vec<Line>), fmt::Error> { |
| let context_data = source |
| .read_span(context_span, self.context_lines, self.context_lines) |
| .map_err(|_| fmt::Error)?; |
| let context = std::str::from_utf8(context_data.data()).expect("Bad utf8 detected"); |
| let mut line = context_data.line(); |
| let mut column = context_data.column(); |
| let mut offset = context_data.span().offset(); |
| let mut line_offset = offset; |
| let mut iter = context.chars().peekable(); |
| let mut line_str = String::new(); |
| let mut lines = Vec::new(); |
| while let Some(char) = iter.next() { |
| offset += char.len_utf8(); |
| let mut at_end_of_file = false; |
| match char { |
| '\r' => { |
| if iter.next_if_eq(&'\n').is_some() { |
| offset += 1; |
| line += 1; |
| column = 0; |
| } else { |
| line_str.push(char); |
| column += 1; |
| } |
| at_end_of_file = iter.peek().is_none(); |
| } |
| '\n' => { |
| at_end_of_file = iter.peek().is_none(); |
| line += 1; |
| column = 0; |
| } |
| _ => { |
| line_str.push(char); |
| column += 1; |
| } |
| } |
| |
| if iter.peek().is_none() && !at_end_of_file { |
| line += 1; |
| } |
| |
| if column == 0 || iter.peek().is_none() { |
| lines.push(Line { |
| line_number: line, |
| offset: line_offset, |
| length: offset - line_offset, |
| text: line_str.clone(), |
| }); |
| line_str.clear(); |
| line_offset = offset; |
| } |
| } |
| Ok((context_data, lines)) |
| } |
| } |
| |
| impl ReportHandler for GraphicalReportHandler { |
| fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| if f.alternate() { |
| return fmt::Debug::fmt(diagnostic, f); |
| } |
| |
| self.render_report(f, diagnostic) |
| } |
| } |
| |
| /* |
| Support types |
| */ |
| |
| #[derive(Debug)] |
| struct Line { |
| line_number: usize, |
| offset: usize, |
| length: usize, |
| text: String, |
| } |
| |
| impl Line { |
| fn span_line_only(&self, span: &FancySpan) -> bool { |
| span.offset() >= self.offset && span.offset() + span.len() <= self.offset + self.length |
| } |
| |
| fn span_applies(&self, span: &FancySpan) -> bool { |
| let spanlen = if span.len() == 0 { 1 } else { span.len() }; |
| // Span starts in this line |
| (span.offset() >= self.offset && span.offset() < self.offset + self.length) |
| // Span passes through this line |
| || (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo |
| // Span ends on this line |
| || (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length) |
| } |
| |
| // A 'flyby' is a multi-line span that technically covers this line, but |
| // does not begin or end within the line itself. This method is used to |
| // calculate gutters. |
| fn span_flyby(&self, span: &FancySpan) -> bool { |
| // The span itself starts before this line's starting offset (so, in a |
| // prev line). |
| span.offset() < self.offset |
| // ...and it stops after this line's end. |
| && span.offset() + span.len() > self.offset + self.length |
| } |
| |
| // Does this line contain the *beginning* of this multiline span? |
| // This assumes self.span_applies() is true already. |
| fn span_starts(&self, span: &FancySpan) -> bool { |
| span.offset() >= self.offset |
| } |
| |
| // Does this line contain the *end* of this multiline span? |
| // This assumes self.span_applies() is true already. |
| fn span_ends(&self, span: &FancySpan) -> bool { |
| span.offset() + span.len() >= self.offset |
| && span.offset() + span.len() <= self.offset + self.length |
| } |
| } |
| |
| #[derive(Debug, Clone)] |
| struct FancySpan { |
| label: Option<String>, |
| span: SourceSpan, |
| style: Style, |
| } |
| |
| impl PartialEq for FancySpan { |
| fn eq(&self, other: &Self) -> bool { |
| self.label == other.label && self.span == other.span |
| } |
| } |
| |
| impl FancySpan { |
| fn new(label: Option<String>, span: SourceSpan, style: Style) -> Self { |
| FancySpan { label, span, style } |
| } |
| |
| fn style(&self) -> Style { |
| self.style |
| } |
| |
| fn label(&self) -> Option<String> { |
| self.label |
| .as_ref() |
| .map(|l| l.style(self.style()).to_string()) |
| } |
| |
| fn offset(&self) -> usize { |
| self.span.offset() |
| } |
| |
| fn len(&self) -> usize { |
| self.span.len() |
| } |
| } |