miette/src/handlers/graphical.rs

1513 lines
54 KiB
Rust

extern crate alloc;
use alloc::boxed::Box;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt::{self, Write};
use owo_colors::{OwoColorize, Style, StyledList};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use crate::diagnostic_chain::{DiagnosticChain, ErrorKind};
use crate::handlers::theme::*;
use crate::highlighters::{Highlighter, MietteHighlighter};
use crate::protocol::{Diagnostic, Severity};
use crate::{LabeledSpan, 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,
pub(crate) wrap_lines: bool,
pub(crate) break_words: bool,
pub(crate) with_primary_span_start: bool,
pub(crate) word_separator: Option<textwrap::WordSeparator>,
pub(crate) word_splitter: Option<textwrap::WordSplitter>,
pub(crate) highlighter: MietteHighlighter,
pub(crate) link_display_text: Option<String>,
pub(crate) show_related_as_nested: 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,
wrap_lines: true,
break_words: true,
with_primary_span_start: true,
word_separator: None,
word_splitter: None,
highlighter: MietteHighlighter::default(),
link_display_text: None,
show_related_as_nested: false,
}
}
///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,
wrap_lines: true,
with_cause_chain: true,
with_primary_span_start: true,
break_words: true,
word_separator: None,
word_splitter: None,
highlighter: MietteHighlighter::default(),
link_display_text: None,
show_related_as_nested: false,
}
}
/// 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
}
/// Include the line and column for the the start of the primary span when the
/// snippet extends multiple lines
pub fn with_primary_span_start(mut self) -> Self {
self.with_primary_span_start = true;
self
}
/// Do not include the line and column for the the start of the primary span
/// when the snippet extends multiple lines
pub fn without_primary_span_start(mut self) -> Self {
self.with_primary_span_start = 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
}
/// Enables or disables wrapping of lines to fit the width.
pub fn with_wrap_lines(mut self, wrap_lines: bool) -> Self {
self.wrap_lines = wrap_lines;
self
}
/// Enables or disables breaking of words during wrapping.
pub fn with_break_words(mut self, break_words: bool) -> Self {
self.break_words = break_words;
self
}
/// Sets the word separator to use when wrapping.
pub fn with_word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
self.word_separator = Some(word_separator);
self
}
/// Sets the word splitter to use when wrapping.
pub fn with_word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self {
self.word_splitter = Some(word_splitter);
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
}
/// Sets whether to render related errors as nested errors.
pub fn with_show_related_as_nested(mut self, show_related_as_nested: bool) -> Self {
self.show_related_as_nested = show_related_as_nested;
self
}
/// Enable syntax highlighting for source code snippets, using the given
/// [`Highlighter`]. See the [highlighters](crate::highlighters) crate
/// for more details.
pub fn with_syntax_highlighting(
mut self,
highlighter: impl Highlighter + Send + Sync + 'static,
) -> Self {
self.highlighter = MietteHighlighter::from(highlighter);
self
}
/// Disable syntax highlighting. This uses the
/// [`crate::highlighters::BlankHighlighter`] as a no-op highlighter.
pub fn without_syntax_highlighting(mut self) -> Self {
self.highlighter = MietteHighlighter::nocolor();
self
}
/// Sets the display text for links.
/// Miette displays `(link)` if this option is not set.
pub fn with_link_display_text(mut self, text: impl Into<String>) -> Self {
self.link_display_text = Some(text.into());
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_report_inner(f, diagnostic, diagnostic.source_code())
}
fn render_report_inner(
&self,
f: &mut impl fmt::Write,
diagnostic: &dyn Diagnostic,
parent_src: Option<&dyn SourceCode>,
) -> fmt::Result {
let src = diagnostic.source_code().or(parent_src);
self.render_header(f, diagnostic, false)?;
self.render_causes(f, diagnostic, src)?;
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(2);
let mut opts = textwrap::Options::new(width)
.initial_indent(" ")
.subsequent_indent(" ")
.break_words(self.break_words);
if let Some(word_separator) = self.word_separator {
opts = opts.word_separator(word_separator);
}
if let Some(word_splitter) = self.word_splitter.clone() {
opts = opts.word_splitter(word_splitter);
}
writeln!(f, "{}", self.wrap(footer, opts))?;
}
Ok(())
}
fn render_header(
&self,
f: &mut impl fmt::Write,
diagnostic: &dyn Diagnostic,
is_nested: bool,
) -> 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();
let mut need_newline = is_nested;
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 display_text = self.link_display_text.as_deref().unwrap_or("(link)");
let link = format!(
"\u{1b}]8;;{}\u{1b}\\{}{}\u{1b}]8;;\u{1b}\\",
url,
code.style(severity_style),
display_text.style(self.theme.styles.link)
);
write!(header, "{}", link)?;
writeln!(f, "{}", header)?;
need_newline = true;
} 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)?;
need_newline = true;
}
if need_newline {
writeln!(f)?;
}
Ok(())
}
fn render_causes(
&self,
f: &mut impl fmt::Write,
diagnostic: &dyn Diagnostic,
parent_src: Option<&dyn SourceCode>,
) -> fmt::Result {
let src = diagnostic.source_code().or(parent_src);
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 mut opts = textwrap::Options::new(width)
.initial_indent(&initial_indent)
.subsequent_indent(&rest_indent)
.break_words(self.break_words);
if let Some(word_separator) = self.word_separator {
opts = opts.word_separator(word_separator);
}
if let Some(word_splitter) = self.word_splitter.clone() {
opts = opts.word_splitter(word_splitter);
}
writeln!(f, "{}", self.wrap(&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 mut opts = textwrap::Options::new(width)
.initial_indent(&initial_indent)
.subsequent_indent(&rest_indent)
.break_words(self.break_words);
if let Some(word_separator) = self.word_separator {
opts = opts.word_separator(word_separator);
}
if let Some(word_splitter) = self.word_splitter.clone() {
opts = opts.word_splitter(word_splitter);
}
match error {
ErrorKind::Diagnostic(diag) => {
let mut inner = String::new();
let mut inner_renderer = self.clone();
// Don't print footer for inner errors
inner_renderer.footer = None;
// Cause chains are already flattened, so don't double-print the nested error
inner_renderer.with_cause_chain = false;
// Since everything from here on is indented, shrink the virtual terminal
inner_renderer.termwidth -= rest_indent.width();
inner_renderer.render_report_inner(&mut inner, diag, src)?;
// If there was no header, remove the leading newline
let inner = inner.trim_start_matches('\n');
writeln!(f, "{}", self.wrap(inner, opts))?;
}
ErrorKind::StdError(err) => {
writeln!(f, "{}", self.wrap(&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(2);
let initial_indent = " help: ".style(self.theme.styles.help).to_string();
let mut opts = textwrap::Options::new(width)
.initial_indent(&initial_indent)
.subsequent_indent(" ")
.break_words(self.break_words);
if let Some(word_separator) = self.word_separator {
opts = opts.word_separator(word_separator);
}
if let Some(word_splitter) = self.word_splitter.clone() {
opts = opts.word_splitter(word_splitter);
}
writeln!(f, "{}", self.wrap(&help.to_string(), opts))?;
}
Ok(())
}
fn render_related(
&self,
f: &mut impl fmt::Write,
diagnostic: &dyn Diagnostic,
parent_src: Option<&dyn SourceCode>,
) -> fmt::Result {
let src = diagnostic.source_code().or(parent_src);
if let Some(related) = diagnostic.related() {
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 inner_renderer = self.clone();
// Re-enable the printing of nested cause chains for related errors
inner_renderer.with_cause_chain = true;
if self.show_related_as_nested {
let width = self.termwidth.saturating_sub(2);
let mut related = related.peekable();
while let Some(rel) = related.next() {
let is_last = related.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 mut opts = textwrap::Options::new(width)
.initial_indent(&initial_indent)
.subsequent_indent(&rest_indent)
.break_words(self.break_words);
if let Some(word_separator) = self.word_separator {
opts = opts.word_separator(word_separator);
}
if let Some(word_splitter) = self.word_splitter.clone() {
opts = opts.word_splitter(word_splitter);
}
let mut inner = String::new();
let mut inner_renderer = self.clone();
inner_renderer.footer = None;
inner_renderer.with_cause_chain = false;
inner_renderer.termwidth -= rest_indent.width();
inner_renderer.render_report_inner(&mut inner, rel, src)?;
// If there was no header, remove the leading newline
let inner = inner.trim_matches('\n');
writeln!(f, "{}", self.wrap(inner, opts))?;
}
} else {
for rel in related {
writeln!(f)?;
match rel.severity() {
Some(Severity::Error) | None => write!(f, "Error: ")?,
Some(Severity::Warning) => write!(f, "Warning: ")?,
Some(Severity::Advice) => write!(f, "Advice: ")?,
};
inner_renderer.render_header(f, rel, true)?;
let src = rel.source_code().or(parent_src);
inner_renderer.render_causes(f, rel, src)?;
inner_renderer.render_snippets(f, rel, src)?;
inner_renderer.render_footer(f, rel)?;
inner_renderer.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 {
let source = match opt_source {
Some(source) => source,
None => return Ok(()),
};
let labels = match diagnostic.labels() {
Some(labels) => labels,
None => return Ok(()),
};
let mut labels = labels.collect::<Vec<_>>();
labels.sort_unstable_by_key(|l| l.inner().offset());
let mut contexts = Vec::with_capacity(labels.len());
for right in labels.iter().cloned() {
let right_conts =
match source.read_span(right.inner(), self.context_lines, self.context_lines) {
Ok(cont) => cont,
Err(err) => {
writeln!(
f,
" [{} `{}` (offset: {}, length: {}): {:?}]",
"Failed to read contents for label".style(self.theme.styles.error),
right
.label()
.unwrap_or("<none>")
.style(self.theme.styles.link),
right.offset().style(self.theme.styles.link),
right.len().style(self.theme.styles.link),
err.style(self.theme.styles.warning)
)?;
return Ok(());
}
};
if contexts.is_empty() {
contexts.push((right, right_conts));
continue;
}
let (left, left_conts) = contexts.last().unwrap();
if left_conts.line() + left_conts.line_count() >= right_conts.line() {
// The snippets will overlap, so we create one Big Chunky Boi
let left_end = left.offset() + left.len();
let right_end = right.offset() + right.len();
let new_end = core::cmp::max(left_end, right_end);
let new_span = LabeledSpan::new(
left.label().map(String::from),
left.offset(),
new_end - left.offset(),
);
// Check that the two contexts can be combined
if let Ok(new_conts) =
source.read_span(new_span.inner(), self.context_lines, self.context_lines)
{
contexts.pop();
// We'll throw the contents away later
contexts.push((new_span, new_conts));
continue;
}
}
contexts.push((right, right_conts));
}
for (ctx, _) in contexts {
self.render_context(f, source, &ctx, &labels[..])?;
}
Ok(())
}
fn render_context(
&self,
f: &mut impl fmt::Write,
source: &dyn SourceCode,
context: &LabeledSpan,
labels: &[LabeledSpan],
) -> fmt::Result {
let (contents, lines) = self.get_lines(source, context.inner())?;
// only consider labels from the context as primary label
let ctx_labels = labels.iter().filter(|l| {
context.inner().offset() <= l.inner().offset()
&& l.inner().offset() + l.inner().len()
<= context.inner().offset() + context.inner().len()
});
let primary_label = ctx_labels
.clone()
.find(|label| label.primary())
.or_else(|| ctx_labels.clone().next());
// 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<_>>();
let mut highlighter_state = self.highlighter.start_highlighter_state(&*contents);
// 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_gutter(hl) {
num_highlights += 1;
}
}
max_gutter = core::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 there is a primary label, then use its span
// as the reference point for line/column information.
let primary_contents = match primary_label {
Some(label) => source
.read_span(label.inner(), 0, 0)
.map_err(|_| fmt::Error)?,
None => contents,
};
if let Some(source_name) = primary_contents.name() {
if self.with_primary_span_start {
writeln!(
f,
"[{}]",
format_args!(
"{}:{}:{}",
source_name,
primary_contents.line() + 1,
primary_contents.column() + 1
)
.style(self.theme.styles.link)
)?;
} else {
writeln!(
f,
"[{}]",
format_args!("{}", source_name,).style(self.theme.styles.link)
)?;
}
} else if self.with_primary_span_start && lines.len() > 1 {
writeln!(
f,
"[{}:{}]",
primary_contents.line() + 1,
primary_contents.column() + 1
)?;
} else {
writeln!(f, "{}", self.theme.characters.hbar.to_string().repeat(3))?;
}
// 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!
let styled_text =
StyledList::from(highlighter_state.highlight_line(&line.text)).to_string();
self.render_line_text(f, &styled_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,
LabelRenderMode::SingleLine,
)?;
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) {
self.render_multi_line_end(f, &labels, max_gutter, linum_width, line, hl)?;
}
}
}
writeln!(
f,
"{}{}{}",
" ".repeat(linum_width + 2),
self.theme.characters.lbot,
self.theme.characters.hbar.to_string().repeat(4),
)?;
Ok(())
}
fn render_multi_line_end(
&self,
f: &mut impl fmt::Write,
labels: &[FancySpan],
max_gutter: usize,
linum_width: usize,
line: &Line,
label: &FancySpan,
) -> fmt::Result {
// no line number!
self.write_no_linum(f, linum_width)?;
if let Some(label_parts) = label.label_parts() {
// if it has a label, how long is it?
let (first, rest) = label_parts
.split_first()
.expect("cannot crash because rest would have been None, see docs on the `label` field of FancySpan");
if rest.is_empty() {
// gutter _again_
self.render_highlight_gutter(
f,
max_gutter,
line,
labels,
LabelRenderMode::SingleLine,
)?;
self.render_multi_line_end_single(
f,
first,
label.style,
LabelRenderMode::SingleLine,
)?;
} else {
// gutter _again_
self.render_highlight_gutter(
f,
max_gutter,
line,
labels,
LabelRenderMode::MultiLineFirst,
)?;
self.render_multi_line_end_single(
f,
first,
label.style,
LabelRenderMode::MultiLineFirst,
)?;
for label_line in rest {
// no line number!
self.write_no_linum(f, linum_width)?;
// gutter _again_
self.render_highlight_gutter(
f,
max_gutter,
line,
labels,
LabelRenderMode::MultiLineRest,
)?;
self.render_multi_line_end_single(
f,
label_line,
label.style,
LabelRenderMode::MultiLineRest,
)?;
}
}
} else {
// gutter _again_
self.render_highlight_gutter(f, max_gutter, line, labels, LabelRenderMode::SingleLine)?;
// has no label
writeln!(f, "{}", self.theme.characters.hbar.style(label.style))?;
}
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_gutter(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],
render_mode: LabelRenderMode,
) -> fmt::Result {
if max_gutter == 0 {
return Ok(());
}
// keeps track of how many columns wide the gutter is
// important for ansi since simply measuring the size of the final string
// gives the wrong result when the string contains ansi codes.
let mut gutter_cols = 0;
let chars = &self.theme.characters;
let mut gutter = String::new();
let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl));
for (i, hl) in applicable.enumerate() {
if !line.span_line_only(hl) && line.span_ends(hl) {
if render_mode == LabelRenderMode::MultiLineRest {
// this is to make multiline labels work. We want to make the right amount
// of horizontal space for them, but not actually draw the lines
let horizontal_space = max_gutter.saturating_sub(i) + 2;
for _ in 0..horizontal_space {
gutter.push(' ');
}
// account for one more horizontal space, since in multiline mode
// we also add in the vertical line before the label like this:
// 2 │ ╭─▶ text
// 3 │ ├─▶ here
// · ╰──┤ these two lines
// · │ are the problem
// ^this
gutter_cols += horizontal_space + 1;
} else {
let num_repeat = max_gutter.saturating_sub(i) + 2;
gutter.push_str(&chars.lbot.style(hl.style).to_string());
gutter.push_str(
&chars
.hbar
.to_string()
.repeat(
num_repeat
// if we are rendering a multiline label, then leave a bit of space for the
// rcross character
- if render_mode == LabelRenderMode::MultiLineFirst {
1
} else {
0
},
)
.style(hl.style)
.to_string(),
);
// we count 1 for the lbot char, and then a few more, the same number
// as we just repeated for. For each repeat we only add 1, even though
// due to ansi escape codes the number of bytes in the string could grow
// a lot each time.
gutter_cols += num_repeat + 1;
}
break;
} else {
gutter.push_str(&chars.vbar.style(hl.style).to_string());
// we may push many bytes for the ansi escape codes style adds,
// but we still only add a single character-width to the string in a terminal
gutter_cols += 1;
}
}
// now calculate how many spaces to add based on how many columns we just created.
// it's the max width of the gutter, minus how many character-widths we just generated
// capped at 0 (though this should never go below in reality), and then we add 3 to
// account for arrowheads when a gutter line ends
let num_spaces = (max_gutter + 3).saturating_sub(gutter_cols);
// we then write the gutter and as many spaces as we need
write!(f, "{}{:width$}", gutter, "", width = num_spaces)?;
Ok(())
}
fn wrap(&self, text: &str, opts: textwrap::Options<'_>) -> String {
if self.wrap_lines {
textwrap::fill(text, opts)
} else {
// Format without wrapping, but retain the indentation options
// Implementation based on `textwrap::indent`
let mut result = String::with_capacity(2 * text.len());
let trimmed_indent = opts.subsequent_indent.trim_end();
for (idx, line) in text.split_terminator('\n').enumerate() {
if idx > 0 {
result.push('\n');
}
if idx == 0 {
if line.trim().is_empty() {
result.push_str(opts.initial_indent.trim_end());
} else {
result.push_str(opts.initial_indent);
}
} else if line.trim().is_empty() {
result.push_str(trimmed_indent);
} else {
result.push_str(opts.subsequent_indent);
}
result.push_str(line);
}
if text.ends_with('\n') {
// split_terminator will have eaten the final '\n'.
result.push('\n');
}
result
}
}
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 mut escaped = false;
let tab_width = self.tab_width;
text.chars().map(move |c| {
let width = match (escaped, c) {
// Round up to the next multiple of tab_width
(false, '\t') => tab_width - column % tab_width,
// start of ANSI escape
(false, '\x1b') => {
escaped = true;
0
}
// use Unicode width for all other characters
(false, c) => c.width().unwrap_or(0),
// end of ANSI escape
(true, 'm') => {
escaped = false;
0
}
// characters are zero width within escape sequence
(true, _) => 0,
};
column += width;
width
})
}
/// Returns the visual column position of a byte offset on a specific line.
///
/// If the offset occurs in the middle of a character, the returned column
/// corresponds to that character's first column in `start` is true, or its
/// last column if `start` is false.
fn visual_offset(&self, line: &Line, offset: usize, start: bool) -> usize {
let line_range = line.offset..=(line.offset + line.length);
assert!(line_range.contains(&offset));
let mut text_index = offset - line.offset;
while text_index <= line.text.len() && !line.text.is_char_boundary(text_index) {
if start {
text_index -= 1;
} else {
text_index += 1;
}
}
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, true).max(highest);
let end = if hl.len() == 0 {
start + 1
} else {
self.visual_offset(line, byte_end, false).max(start + 1)
};
let vbar_offset = (start + end) / 2;
let num_left = vbar_offset - start;
let num_right = end - vbar_offset - 1;
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 = core::cmp::max(highest, end);
(hl, vbar_offset)
})
.collect();
writeln!(f, "{}", underlines)?;
for hl in single_liners.iter().rev() {
if let Some(label) = hl.label_parts() {
if label.len() == 1 {
self.write_label_text(
f,
line,
linum_width,
max_gutter,
all_highlights,
chars,
&vbar_offsets,
hl,
&label[0],
LabelRenderMode::SingleLine,
)?;
} else {
let mut first = true;
for label_line in &label {
self.write_label_text(
f,
line,
linum_width,
max_gutter,
all_highlights,
chars,
&vbar_offsets,
hl,
label_line,
if first {
LabelRenderMode::MultiLineFirst
} else {
LabelRenderMode::MultiLineRest
},
)?;
first = false;
}
}
}
}
Ok(())
}
// I know it's not good practice, but making this a function makes a lot of sense
// and making a struct for this does not...
#[allow(clippy::too_many_arguments)]
fn write_label_text(
&self,
f: &mut impl fmt::Write,
line: &Line,
linum_width: usize,
max_gutter: usize,
all_highlights: &[FancySpan],
chars: &ThemeCharacters,
vbar_offsets: &[(&&FancySpan, usize)],
hl: &&FancySpan,
label: &str,
render_mode: LabelRenderMode,
) -> fmt::Result {
self.write_no_linum(f, linum_width)?;
self.render_highlight_gutter(
f,
max_gutter,
line,
all_highlights,
LabelRenderMode::SingleLine,
)?;
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 = match render_mode {
LabelRenderMode::SingleLine => format!(
"{}{} {}",
chars.lbot,
chars.hbar.to_string().repeat(2),
label,
),
LabelRenderMode::MultiLineFirst => {
format!("{}{}{} {}", chars.lbot, chars.hbar, chars.rcross, label,)
}
LabelRenderMode::MultiLineRest => {
format!(" {} {}", chars.vbar, label,)
}
};
writeln!(f, "{}", lines.style(hl.style))?;
break;
}
}
Ok(())
}
fn render_multi_line_end_single(
&self,
f: &mut impl fmt::Write,
label: &str,
style: Style,
render_mode: LabelRenderMode,
) -> fmt::Result {
match render_mode {
LabelRenderMode::SingleLine => {
writeln!(f, "{} {}", self.theme.characters.hbar.style(style), label)?;
}
LabelRenderMode::MultiLineFirst => {
writeln!(f, "{} {}", self.theme.characters.rcross.style(style), label)?;
}
LabelRenderMode::MultiLineRest => {
writeln!(f, "{} {}", self.theme.characters.vbar.style(style), label)?;
}
}
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 = String::from_utf8_lossy(context_data.data());
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 line_str = String::with_capacity(context.len());
let mut lines = Vec::with_capacity(1);
let mut iter = context.chars().peekable();
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(PartialEq, Debug)]
enum LabelRenderMode {
/// we're rendering a single line label (or not rendering in any special way)
SingleLine,
/// we're rendering a multiline label
MultiLineFirst,
/// we're rendering the rest of a multiline label
MultiLineRest,
}
#[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
}
/// Returns whether `span` should be visible on this line, either in the gutter or under the
/// text on this line
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)
}
/// Returns whether `span` should be visible on this line in the gutter (so this excludes spans
/// that are only visible on this line and do not span multiple lines)
fn span_applies_gutter(&self, span: &FancySpan) -> bool {
let spanlen = if span.len() == 0 { 1 } else { span.len() };
// Span starts in this line
self.span_applies(span)
&& !(
// as long as it doesn't start *and* end on this line
(span.offset() >= self.offset && span.offset() < self.offset + self.length)
&& (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 {
/// this is deliberately an option of a vec because I wanted to be very explicit
/// that there can also be *no* label. If there is a label, it can have multiple
/// lines which is what the vec is for.
label: Option<Vec<String>>,
span: SourceSpan,
style: Style,
}
impl PartialEq for FancySpan {
fn eq(&self, other: &Self) -> bool {
self.label == other.label && self.span == other.span
}
}
fn split_label(v: String) -> Vec<String> {
v.split('\n').map(|i| i.to_string()).collect()
}
impl FancySpan {
fn new(label: Option<String>, span: SourceSpan, style: Style) -> Self {
FancySpan {
label: label.map(split_label),
span,
style,
}
}
fn style(&self) -> Style {
self.style
}
fn label(&self) -> Option<String> {
self.label
.as_ref()
.map(|l| l.join("\n").style(self.style()).to_string())
}
fn label_parts(&self) -> Option<Vec<String>> {
self.label.as_ref().map(|l| {
l.iter()
.map(|i| i.style(self.style()).to_string())
.collect()
})
}
fn offset(&self) -> usize {
self.span.offset()
}
fn len(&self) -> usize {
self.span.len()
}
}