blob: b5dd7545a5ac49df7d61b6cc92725f77071ca665 [file] [log] [blame] [edit]
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()
}
}