diff options
Diffstat (limited to 'vendor/miette/src/handlers/graphical.rs')
-rw-r--r-- | vendor/miette/src/handlers/graphical.rs | 920 |
1 files changed, 920 insertions, 0 deletions
diff --git a/vendor/miette/src/handlers/graphical.rs b/vendor/miette/src/handlers/graphical.rs new file mode 100644 index 0000000..b5dd754 --- /dev/null +++ b/vendor/miette/src/handlers/graphical.rs @@ -0,0 +1,920 @@ +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() + } +} |