miette/src/printer/default_printer.rs

551 lines
18 KiB
Rust

use std::fmt;
use owo_colors::{OwoColorize, Style};
use crate::chain::Chain;
use crate::printer::theme::*;
use crate::protocol::{Diagnostic, DiagnosticReportPrinter, DiagnosticSnippet, Severity};
use crate::{SourceSpan, SpanContents};
/**
Reference implementation of the [DiagnosticReportPrinter] trait. This is generally
good enough for simple use-cases, and is the default one installed with `miette`,
but you might want to implement your own if you want custom reporting for your
tool or app.
*/
pub struct DefaultReportPrinter {
pub(crate) theme: MietteTheme,
}
impl DefaultReportPrinter {
pub fn new() -> Self {
Self {
theme: MietteTheme::default(),
}
}
pub fn new_themed(theme: MietteTheme) -> Self {
Self { theme }
}
}
impl Default for DefaultReportPrinter {
fn default() -> Self {
Self::new()
}
}
impl DefaultReportPrinter {
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)?;
if let Some(snippets) = diagnostic.snippets() {
for snippet in snippets {
writeln!(f)?;
self.render_snippet(f, &snippet)?;
}
}
self.render_footer(f, diagnostic)?;
Ok(())
}
fn render_header(&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.x),
Some(Severity::Warning) => (self.theme.styles.warning, self.theme.characters.warning),
Some(Severity::Advice) => (self.theme.styles.advice, self.theme.characters.point_right),
};
let code = diagnostic.code();
writeln!(
f,
"{}[{}]{}",
self.theme.characters.hbar.to_string().repeat(4),
code.style(self.theme.styles.code),
self.theme.characters.hbar.to_string().repeat(20),
)?;
writeln!(f)?;
writeln!(
f,
" {} {}",
severity_icon.style(severity_style),
diagnostic.style(severity_style)
)?;
Ok(())
}
fn render_causes(&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,
};
if let Some(cause) = diagnostic.source() {
let mut cause_iter = Chain::new(cause).peekable();
while let Some(error) = cause_iter.next() {
let char = if cause_iter.peek().is_some() {
self.theme.characters.lcross
} else {
self.theme.characters.lbot
};
let msg = format!(
" {}{}{} {}",
char, self.theme.characters.hbar, self.theme.characters.rarrow, error
)
.style(severity_style)
.to_string();
writeln!(f, "{}", msg)?;
}
}
Ok(())
}
fn render_footer(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
if let Some(help) = diagnostic.help() {
let help = help.style(self.theme.styles.help);
writeln!(f)?;
writeln!(f, " {} {}", self.theme.characters.fyi, help)?;
}
Ok(())
}
fn render_snippet(&self, f: &mut impl fmt::Write, snippet: &DiagnosticSnippet) -> fmt::Result {
let (contents, lines) = self.get_lines(snippet)?;
// Highlights are the bits we're going to underline in our overall
// snippet, and we need to do some analysis first to come up with
// gutter size.
let mut highlights = snippet.highlights.clone().unwrap_or_else(Vec::new);
// sorting is your friend.
highlights.sort_unstable_by_key(|h| h.offset());
let highlights = highlights
.into_iter()
.zip(self.theme.styles.highlights.iter().cloned().cycle())
.map(|(hl, st)| FancySpan::new(hl, 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 &highlights {
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()
.expect("get_lines should always return at least one line?")
.line_number
.to_string()
.len();
// Header
if let Some(source_name) = snippet.context.label() {
let source_name = source_name.style(self.theme.styles.filename);
write!(
f,
"{}{}{}[{}:{}:{}]",
" ".repeat(linum_width + 2),
self.theme.characters.ltop,
self.theme.characters.hbar.to_string().repeat(3),
source_name,
contents.line() + 1,
contents.column() + 1
)?;
if let Some(msg) = &snippet.message {
write!(f, " {}:", msg)?;
}
writeln!(f)?;
}
// 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, &highlights)?;
// And _now_ we can print out the line text itself!
writeln!(f, "{}", line.text)?;
// Next, we write all the highlights that apply to this particular line.
let (single_line, multi_line): (Vec<_>, Vec<_>) = highlights
.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, &highlights)?;
self.render_single_line_highlights(
f,
line,
linum_width,
max_gutter,
&single_line,
&highlights,
)?;
}
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, &highlights)?;
self.render_multi_line_end(f, hl)?;
}
}
}
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,
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(())
}
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;
for hl in single_liners {
let local_offset = hl.offset() - line.offset;
let vbar_offset = local_offset + (hl.len() / 2);
let num_left = vbar_offset - local_offset;
let num_right = local_offset + hl.len() - vbar_offset - 1;
let start = std::cmp::max(local_offset, highest);
let end = local_offset + hl.len();
if start < end {
underlines.push_str(
&format!(
"{:width$}{}{}{}",
"",
chars.underline.to_string().repeat(num_left),
if hl.label().is_some() {
chars.underbar
} else {
chars.underline
},
chars.underline.to_string().repeat(num_right),
width = local_offset.saturating_sub(highest),
)
.style(hl.style)
.to_string(),
);
}
highest = std::cmp::max(highest, end);
}
writeln!(f, "{}", underlines)?;
for hl in single_liners {
if let Some(label) = hl.label() {
self.write_no_linum(f, linum_width)?;
self.render_highlight_gutter(f, max_gutter, line, all_highlights)?;
let local_offset = hl.offset() - line.offset;
let vbar_offset = local_offset + (hl.len() / 2);
let num_right = local_offset + hl.len() - vbar_offset - 1;
let lines = format!(
"{:width$}{}{} {}",
" ",
chars.lbot,
chars.hbar.to_string().repeat(num_right + 1),
label,
width = vbar_offset
);
writeln!(f, "{}", lines.style(hl.style))?;
}
}
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,
snippet: &'a DiagnosticSnippet,
) -> Result<(Box<dyn SpanContents + 'a>, Vec<Line>), fmt::Error> {
let context_data = snippet
.source
.read_span(&snippet.context)
.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 = snippet.context.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();
match char {
'\r' => {
if iter.next_if_eq(&'\n').is_some() {
offset += 1;
line += 1;
column = 0;
} else {
line_str.push(char);
column += 1;
}
}
'\n' => {
line += 1;
column = 0;
}
_ => {
line_str.push(char);
column += 1;
}
}
if iter.peek().is_none() {
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 DiagnosticReportPrinter for DefaultReportPrinter {
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
*/
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 {
// 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() + span.len() > self.offset + self.length) //todo
// Span ends on this line
|| (span.offset() + span.len() >= self.offset && span.offset() + span.len() <= 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
}
}
struct FancySpan {
span: SourceSpan,
style: Style,
}
impl FancySpan {
fn new(span: SourceSpan, style: Style) -> Self {
FancySpan { span, style }
}
fn style(&self) -> Style {
self.style
}
fn label(&self) -> Option<String> {
self.span.label().map(|l| l.style(self.style()).to_string())
}
fn offset(&self) -> usize {
self.span.offset()
}
fn len(&self) -> usize {
self.span.len()
}
}