mirror of https://github.com/zkat/miette.git
feat(reporter): fancy new reporter with unicode, colors, and multiline (#23)
Fixes: https://github.com/zkat/miette/issues/3 Fixes: https://github.com/zkat/miette/issues/5
This commit is contained in:
parent
8fbad1b1cd
commit
d675334e48
|
|
@ -15,6 +15,8 @@ indenter = "0.3.3"
|
|||
thiserror = "1.0.26"
|
||||
miette-derive = { version = "=0.11.0", path = "miette-derive" }
|
||||
once_cell = "1.8.0"
|
||||
owo-colors = "2.0.0"
|
||||
atty = "0.2.14"
|
||||
|
||||
[dev-dependencies]
|
||||
thiserror = "1.0.26"
|
||||
|
|
|
|||
|
|
@ -131,4 +131,5 @@ Error: Error[oops::my::bad]: oops it broke!
|
|||
|
||||
It also includes some code taken from [`eyre`](https://github.com/yaahc/eyre),
|
||||
and some from [`thiserror`](https://github.com/dtolnay/thiserror), also under
|
||||
the Apache License.
|
||||
the Apache License. Some code is taken from
|
||||
[`ariadne`](https://github.com/zesterer/ariadne), which is MIT licensed.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
|
|||
use quote::{format_ident, quote, quote_spanned, ToTokens};
|
||||
use syn::ext::IdentExt;
|
||||
use syn::parse::{ParseStream, Parser};
|
||||
use syn::{Ident, Index, LitStr, Member, Result, Token, braced, bracketed, parenthesized};
|
||||
use syn::{braced, bracketed, parenthesized, Ident, Index, LitStr, Member, Result, Token};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Display {
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@
|
|||
pub use miette_derive::*;
|
||||
|
||||
pub use error::*;
|
||||
pub use printer::*;
|
||||
pub use protocol::*;
|
||||
pub use reporter::*;
|
||||
pub use utils::*;
|
||||
|
||||
mod chain;
|
||||
mod error;
|
||||
mod printer;
|
||||
mod protocol;
|
||||
mod reporter;
|
||||
mod source_impls;
|
||||
mod utils;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,536 @@
|
|||
use std::fmt;
|
||||
|
||||
use indenter::indented;
|
||||
use owo_colors::{OwoColorize, Style};
|
||||
|
||||
use crate::chain::Chain;
|
||||
use crate::printer::theme::*;
|
||||
use crate::protocol::{Diagnostic, DiagnosticReportPrinter, DiagnosticSnippet, Severity};
|
||||
use crate::SourceSpan;
|
||||
|
||||
/**
|
||||
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() {
|
||||
let mut pre = false;
|
||||
for snippet in snippets {
|
||||
if !pre {
|
||||
writeln!(f)?;
|
||||
pre = true;
|
||||
}
|
||||
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 sev = match diagnostic.severity() {
|
||||
Some(Severity::Error) | None => "Error".style(self.theme.styles.error),
|
||||
Some(Severity::Warning) => "Warning".style(self.theme.styles.warning),
|
||||
Some(Severity::Advice) => "Advice".style(self.theme.styles.advice),
|
||||
}
|
||||
.to_string();
|
||||
let code = diagnostic.code();
|
||||
let msg = diagnostic.to_string();
|
||||
writeln!(
|
||||
f,
|
||||
"{} [{}]: {}",
|
||||
sev,
|
||||
code.style(self.theme.styles.code),
|
||||
msg
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_causes(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
|
||||
use fmt::Write as _;
|
||||
|
||||
if let Some(cause) = diagnostic.source() {
|
||||
writeln!(f)?;
|
||||
write!(f, "Caused by:")?;
|
||||
|
||||
let multiple = cause.source().is_some();
|
||||
|
||||
for (n, error) in Chain::new(cause).enumerate() {
|
||||
let msg = format!("{}", error);
|
||||
writeln!(f)?;
|
||||
if multiple {
|
||||
write!(indented(f).ind(n), "{}", msg)?;
|
||||
} else {
|
||||
write!(indented(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.eq, help)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_snippet(&self, f: &mut impl fmt::Write, snippet: &DiagnosticSnippet) -> fmt::Result {
|
||||
// Boring: The Header
|
||||
if let Some(source_name) = snippet.context.label() {
|
||||
let source_name = source_name.style(self.theme.styles.filename);
|
||||
write!(f, "[{}]", source_name)?;
|
||||
}
|
||||
if let Some(msg) = &snippet.message {
|
||||
write!(f, " {}:", msg)?;
|
||||
}
|
||||
writeln!(f)?;
|
||||
writeln!(f)?;
|
||||
|
||||
// Fun time!
|
||||
|
||||
// Our actual code, line by line! Handy!
|
||||
let 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();
|
||||
|
||||
// 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.to_string().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.to_string().style(hl.style).to_string());
|
||||
arrow = true;
|
||||
break;
|
||||
} else if line.span_ends(hl) {
|
||||
if hl.label().is_some() {
|
||||
gutter.push_str(&chars.lcross.to_string().style(hl.style).to_string());
|
||||
} else {
|
||||
gutter.push_str(&chars.lbot.to_string().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.to_string().style(hl.style).to_string());
|
||||
arrow = true;
|
||||
break;
|
||||
} else if line.span_flyby(hl) {
|
||||
gutter.push_str(&chars.vbar.to_string().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.to_string().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.to_string().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.to_string().style(hl.style),
|
||||
hl.label().unwrap_or_else(|| "".into()),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_lines(&self, snippet: &DiagnosticSnippet) -> Result<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(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()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*!
|
||||
Basic reporter for Diagnostics. Probably good enough for most use-cases,
|
||||
but largely meant to be an example.
|
||||
*/
|
||||
use std::fmt;
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
use crate::protocol::{Diagnostic, DiagnosticReportPrinter, Severity};
|
||||
use crate::MietteError;
|
||||
|
||||
pub use default_reporter::*;
|
||||
pub use theme::*;
|
||||
|
||||
mod default_reporter;
|
||||
mod theme;
|
||||
|
||||
static REPORTER: OnceCell<Box<dyn DiagnosticReportPrinter + Send + Sync + 'static>> =
|
||||
OnceCell::new();
|
||||
|
||||
/// Set the global [DiagnosticReportPrinter] that will be used when you report
|
||||
/// using [DiagnosticReport].
|
||||
pub fn set_reporter(
|
||||
reporter: impl DiagnosticReportPrinter + Send + Sync + 'static,
|
||||
) -> Result<(), MietteError> {
|
||||
REPORTER
|
||||
.set(Box::new(reporter))
|
||||
.map_err(|_| MietteError::ReporterInstallFailed)
|
||||
}
|
||||
|
||||
/// Used by [DiagnosticReport] to fetch the reporter that will be used to
|
||||
/// print stuff out.
|
||||
pub fn get_reporter() -> &'static (dyn DiagnosticReportPrinter + Send + Sync + 'static) {
|
||||
&**REPORTER.get_or_init(|| {
|
||||
Box::new(DefaultReportPrinter {
|
||||
// TODO: color support detection here?
|
||||
theme: MietteTheme::default(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Literally what it says on the tin.
|
||||
pub struct JokeReporter;
|
||||
|
||||
impl DiagnosticReportPrinter for JokeReporter {
|
||||
fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if f.alternate() {
|
||||
return fmt::Debug::fmt(diagnostic, f);
|
||||
}
|
||||
|
||||
let sev = match diagnostic.severity() {
|
||||
Some(Severity::Error) | None => "error",
|
||||
Some(Severity::Warning) => "warning",
|
||||
Some(Severity::Advice) => "advice",
|
||||
};
|
||||
writeln!(
|
||||
f,
|
||||
"me, with {} {}: {}",
|
||||
sev,
|
||||
diagnostic,
|
||||
diagnostic
|
||||
.help()
|
||||
.unwrap_or_else(|| Box::new(&"have you tried not failing?"))
|
||||
)?;
|
||||
writeln!(
|
||||
f,
|
||||
"miette, her eyes enormous: you {} miette? you {}? oh! oh! jail for mother! jail for mother for One Thousand Years!!!!",
|
||||
diagnostic.code(),
|
||||
diagnostic.snippets().map(|snippets| {
|
||||
snippets.map(|snippet| snippet.message.map(|x| x.to_owned()))
|
||||
.collect::<Option<Vec<String>>>()
|
||||
}).flatten().map(|x| x.join(", ")).unwrap_or_else(||"try and cause miette to panic".into())
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
use atty::Stream;
|
||||
use owo_colors::Style;
|
||||
|
||||
pub struct MietteTheme {
|
||||
pub characters: MietteCharacters,
|
||||
pub styles: MietteStyles,
|
||||
}
|
||||
|
||||
impl MietteTheme {
|
||||
pub fn basic() -> Self {
|
||||
Self {
|
||||
characters: MietteCharacters::ascii(),
|
||||
styles: MietteStyles::ansi(),
|
||||
}
|
||||
}
|
||||
pub fn unicode() -> Self {
|
||||
Self {
|
||||
characters: MietteCharacters::unicode(),
|
||||
styles: MietteStyles::ansi(),
|
||||
}
|
||||
}
|
||||
pub fn unicode_nocolor() -> Self {
|
||||
Self {
|
||||
characters: MietteCharacters::unicode(),
|
||||
styles: MietteStyles::none(),
|
||||
}
|
||||
}
|
||||
pub fn none() -> Self {
|
||||
Self {
|
||||
characters: MietteCharacters::ascii(),
|
||||
styles: MietteStyles::none(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MietteTheme {
|
||||
fn default() -> Self {
|
||||
match std::env::var("NO_COLOR") {
|
||||
_ if !atty::is(Stream::Stdout) || !atty::is(Stream::Stderr) => Self::basic(),
|
||||
Ok(string) if string != "0" => Self::unicode_nocolor(),
|
||||
_ => Self::unicode(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MietteStyles {
|
||||
pub error: Style,
|
||||
pub warning: Style,
|
||||
pub advice: Style,
|
||||
pub code: Style,
|
||||
pub help: Style,
|
||||
pub filename: Style,
|
||||
pub highlights: Vec<Style>,
|
||||
}
|
||||
|
||||
fn style() -> Style {
|
||||
Style::new()
|
||||
}
|
||||
|
||||
impl MietteStyles {
|
||||
pub fn ansi() -> Self {
|
||||
Self {
|
||||
error: style().red(),
|
||||
warning: style().yellow(),
|
||||
advice: style().cyan(),
|
||||
code: style().yellow(),
|
||||
help: style().cyan(),
|
||||
filename: style().green(),
|
||||
highlights: vec![style().red(), style().magenta(), style().cyan()],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn none() -> Self {
|
||||
Self {
|
||||
error: style(),
|
||||
warning: style(),
|
||||
advice: style(),
|
||||
code: style(),
|
||||
help: style(),
|
||||
filename: style(),
|
||||
highlights: vec![style()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------
|
||||
// All code below here was taken from ariadne here:
|
||||
// https://github.com/zesterer/ariadne/blob/e3cb394cb56ecda116a0a1caecd385a49e7f6662/src/draw.rs
|
||||
pub struct MietteCharacters {
|
||||
pub hbar: char,
|
||||
pub vbar: char,
|
||||
pub xbar: char,
|
||||
pub vbar_break: char,
|
||||
|
||||
pub uarrow: char,
|
||||
pub rarrow: char,
|
||||
|
||||
pub ltop: char,
|
||||
pub mtop: char,
|
||||
pub rtop: char,
|
||||
pub lbot: char,
|
||||
pub rbot: char,
|
||||
pub mbot: char,
|
||||
|
||||
pub lbox: char,
|
||||
pub rbox: char,
|
||||
|
||||
pub lcross: char,
|
||||
pub rcross: char,
|
||||
|
||||
pub underbar: char,
|
||||
pub underline: char,
|
||||
|
||||
pub eq: char,
|
||||
}
|
||||
|
||||
impl MietteCharacters {
|
||||
pub fn unicode() -> Self {
|
||||
Self {
|
||||
hbar: '─',
|
||||
vbar: '│',
|
||||
xbar: '┼',
|
||||
vbar_break: '·',
|
||||
uarrow: '▲',
|
||||
rarrow: '▶',
|
||||
ltop: '╭',
|
||||
mtop: '┬',
|
||||
rtop: '╮',
|
||||
lbot: '╰',
|
||||
mbot: '┴',
|
||||
rbot: '╯',
|
||||
lbox: '[',
|
||||
rbox: ']',
|
||||
lcross: '├',
|
||||
rcross: '┤',
|
||||
underbar: '┬',
|
||||
underline: '─',
|
||||
eq: '﹦',
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ascii() -> Self {
|
||||
Self {
|
||||
hbar: '-',
|
||||
vbar: '|',
|
||||
xbar: '+',
|
||||
vbar_break: ':',
|
||||
uarrow: '^',
|
||||
rarrow: '>',
|
||||
ltop: ',',
|
||||
mtop: 'v',
|
||||
rtop: '.',
|
||||
lbot: '`',
|
||||
mbot: '^',
|
||||
rbot: '\'',
|
||||
lbox: '[',
|
||||
rbox: ']',
|
||||
lcross: '|',
|
||||
rcross: '|',
|
||||
underbar: '|',
|
||||
underline: '^',
|
||||
eq: '=',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,11 @@ that you can implement to get access to miette's (and related library's) full
|
|||
reporting and such features.
|
||||
*/
|
||||
|
||||
use std::{fmt::Display, fs, panic::Location};
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
fs,
|
||||
panic::Location,
|
||||
};
|
||||
|
||||
use crate::MietteError;
|
||||
|
||||
|
|
@ -63,6 +67,36 @@ impl<T: Diagnostic + Send + Sync + 'static> From<T> for Box<dyn Diagnostic + 'st
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
When used with `?`/`From`, this will wrap any Diagnostics and, when
|
||||
formatted with `Debug`, will fetch the current [DiagnosticReportPrinter] and
|
||||
use it to format the inner [Diagnostic].
|
||||
*/
|
||||
pub struct DiagnosticReport {
|
||||
diagnostic: Box<dyn Diagnostic + Send + Sync + 'static>,
|
||||
}
|
||||
|
||||
impl DiagnosticReport {
|
||||
/// Return a reference to the inner [Diagnostic].
|
||||
pub fn inner(&self) -> &(dyn Diagnostic + Send + Sync + 'static) {
|
||||
&*self.diagnostic
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for DiagnosticReport {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
crate::get_reporter().debug(&*self.diagnostic, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Diagnostic + Send + Sync + 'static> From<T> for DiagnosticReport {
|
||||
fn from(diagnostic: T) -> Self {
|
||||
DiagnosticReport {
|
||||
diagnostic: Box::new(diagnostic),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Protocol for [Diagnostic] handlers, which are responsible for actually printing out Diagnostics.
|
||||
|
||||
|
|
|
|||
243
src/reporter.rs
243
src/reporter.rs
|
|
@ -1,243 +0,0 @@
|
|||
/*!
|
||||
Basic reporter for Diagnostics. Probably good enough for most use-cases,
|
||||
but largely meant to be an example.
|
||||
*/
|
||||
use std::fmt;
|
||||
|
||||
use indenter::indented;
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
use crate::chain::Chain;
|
||||
use crate::protocol::{Diagnostic, DiagnosticReportPrinter, DiagnosticSnippet, Severity};
|
||||
use crate::MietteError;
|
||||
|
||||
static REPORTER: OnceCell<Box<dyn DiagnosticReportPrinter + Send + Sync + 'static>> =
|
||||
OnceCell::new();
|
||||
|
||||
/// Set the global [DiagnosticReportPrinter] that will be used when you report
|
||||
/// using [DiagnosticReport].
|
||||
pub fn set_reporter(
|
||||
reporter: impl DiagnosticReportPrinter + Send + Sync + 'static,
|
||||
) -> Result<(), MietteError> {
|
||||
REPORTER
|
||||
.set(Box::new(reporter))
|
||||
.map_err(|_| MietteError::ReporterInstallFailed)
|
||||
}
|
||||
|
||||
/// Used by [DiagnosticReport] to fetch the reporter that will be used to
|
||||
/// print stuff out.
|
||||
pub fn get_reporter() -> &'static (dyn DiagnosticReportPrinter + Send + Sync + 'static) {
|
||||
&**REPORTER.get_or_init(|| Box::new(DefaultReportPrinter))
|
||||
}
|
||||
|
||||
/// Convenience alias. This is intended to be used as the return type for `main()`
|
||||
pub type DiagnosticResult<T> = Result<T, DiagnosticReport>;
|
||||
|
||||
/// When used with `?`/`From`, this will wrap any Diagnostics and, when
|
||||
/// formatted with `Debug`, will fetch the current [DiagnosticReportPrinter] and
|
||||
/// use it to format the inner [Diagnostic].
|
||||
pub struct DiagnosticReport {
|
||||
diagnostic: Box<dyn Diagnostic + Send + Sync + 'static>,
|
||||
}
|
||||
|
||||
impl DiagnosticReport {
|
||||
/// Return a reference to the inner [Diagnostic].
|
||||
pub fn inner(&self) -> &(dyn Diagnostic + Send + Sync + 'static) {
|
||||
&*self.diagnostic
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for DiagnosticReport {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
get_reporter().debug(&*self.diagnostic, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Diagnostic + Send + Sync + 'static> From<T> for DiagnosticReport {
|
||||
fn from(diagnostic: T) -> Self {
|
||||
DiagnosticReport {
|
||||
diagnostic: Box::new(diagnostic),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
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;
|
||||
|
||||
impl DefaultReportPrinter {
|
||||
fn render_snippet(
|
||||
&self,
|
||||
f: &mut fmt::Formatter<'_>,
|
||||
snippet: &DiagnosticSnippet,
|
||||
) -> fmt::Result {
|
||||
use fmt::Write as _;
|
||||
if let Some(source_name) = snippet.context.label() {
|
||||
write!(f, "[{}]", source_name)?;
|
||||
}
|
||||
if let Some(msg) = &snippet.message {
|
||||
write!(f, " {}:", msg)?;
|
||||
}
|
||||
writeln!(f)?;
|
||||
writeln!(f)?;
|
||||
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 highlights = snippet.highlights.as_ref();
|
||||
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() {
|
||||
writeln!(indented(f), "{: <2} | {}", line, line_str)?;
|
||||
line_str.clear();
|
||||
if let Some(highlights) = highlights {
|
||||
for span in highlights {
|
||||
if span.offset() >= line_offset && (span.offset() + span.len()) < offset {
|
||||
// Highlight only covers one line.
|
||||
write!(indented(f), "{: <2} | ", "⫶")?;
|
||||
write!(
|
||||
f,
|
||||
"{}{} ",
|
||||
" ".repeat(span.offset() - line_offset),
|
||||
"^".repeat(span.len())
|
||||
)?;
|
||||
if let Some(label) = span.label() {
|
||||
writeln!(f, "{}", label)?;
|
||||
}
|
||||
} else if span.offset() < offset
|
||||
&& span.offset() >= line_offset
|
||||
&& (span.offset() + span.len()) >= offset
|
||||
{
|
||||
// Multiline highlight.
|
||||
todo!("Multiline highlights.");
|
||||
}
|
||||
}
|
||||
}
|
||||
line_offset = offset;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl DiagnosticReportPrinter for DefaultReportPrinter {
|
||||
fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use fmt::Write as _;
|
||||
|
||||
if f.alternate() {
|
||||
return fmt::Debug::fmt(diagnostic, f);
|
||||
}
|
||||
|
||||
let sev = match diagnostic.severity() {
|
||||
Some(Severity::Error) | None => "Error",
|
||||
Some(Severity::Warning) => "Warning",
|
||||
Some(Severity::Advice) => "Advice",
|
||||
};
|
||||
writeln!(f, "{}[{}]: {}", sev, diagnostic.code(), diagnostic)?;
|
||||
|
||||
if let Some(cause) = diagnostic.source() {
|
||||
writeln!(f)?;
|
||||
write!(f, "Caused by:")?;
|
||||
let multiple = cause.source().is_some();
|
||||
|
||||
for (n, error) in Chain::new(cause).enumerate() {
|
||||
writeln!(f)?;
|
||||
if multiple {
|
||||
write!(indented(f).ind(n), "{}", error)?;
|
||||
} else {
|
||||
write!(indented(f), "{}", error)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(snippets) = diagnostic.snippets() {
|
||||
let mut pre = false;
|
||||
for snippet in snippets {
|
||||
if !pre {
|
||||
writeln!(f)?;
|
||||
pre = true;
|
||||
}
|
||||
self.render_snippet(f, &snippet)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(help) = diagnostic.help() {
|
||||
writeln!(f)?;
|
||||
writeln!(f, "﹦{}", help)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Literally what it says on the tin.
|
||||
pub struct JokeReporter;
|
||||
|
||||
impl DiagnosticReportPrinter for JokeReporter {
|
||||
fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if f.alternate() {
|
||||
return fmt::Debug::fmt(diagnostic, f);
|
||||
}
|
||||
|
||||
let sev = match diagnostic.severity() {
|
||||
Some(Severity::Error) | None => "error",
|
||||
Some(Severity::Warning) => "warning",
|
||||
Some(Severity::Advice) => "advice",
|
||||
};
|
||||
writeln!(
|
||||
f,
|
||||
"me, with {} {}: {}",
|
||||
sev,
|
||||
diagnostic,
|
||||
diagnostic
|
||||
.help()
|
||||
.unwrap_or_else(|| Box::new(&"have you tried not failing?"))
|
||||
)?;
|
||||
writeln!(
|
||||
f,
|
||||
"miette, her eyes enormous: you {} miette? you {}? oh! oh! jail for mother! jail for mother for One Thousand Years!!!!",
|
||||
diagnostic.code(),
|
||||
diagnostic.snippets().map(|snippets| {
|
||||
snippets.map(|snippet| snippet.message.map(|x| x.to_owned()))
|
||||
.collect::<Option<Vec<String>>>()
|
||||
}).flatten().map(|x| x.join(", ")).unwrap_or_else(||"try and cause miette to panic".into())
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,10 @@ use std::fmt;
|
|||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::Diagnostic;
|
||||
use crate::{Diagnostic, DiagnosticReport};
|
||||
|
||||
/// Convenience alias. This is intended to be used as the return type for `main()`
|
||||
pub type DiagnosticResult<T> = Result<T, DiagnosticReport>;
|
||||
|
||||
/// Convenience [Diagnostic] that can be used as an "anonymous" wrapper for
|
||||
/// Errors. This is intended to be paired with [IntoDiagnostic].
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ fn fmt_help() {
|
|||
Y { len: usize },
|
||||
|
||||
#[diagnostic(code(foo::x), help("{} x {self:?} x {:?}", 1, "2"))]
|
||||
Z
|
||||
Z,
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,289 @@
|
|||
use miette::{
|
||||
DefaultReportPrinter, Diagnostic, DiagnosticReport, MietteError, MietteTheme, SourceSpan,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
fn fmt_report(diag: DiagnosticReport) -> String {
|
||||
// Mostly for dev purposes.
|
||||
let theme = if std::env::var("STYLE").is_ok() {
|
||||
MietteTheme::unicode()
|
||||
} else if std::env::var("BASIC").is_ok() {
|
||||
MietteTheme::none()
|
||||
} else {
|
||||
MietteTheme::unicode_nocolor()
|
||||
};
|
||||
let printer = DefaultReportPrinter::new_themed(theme);
|
||||
let mut out = String::new();
|
||||
printer.render_report(&mut out, diag.inner()).unwrap();
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_line_highlight() -> Result<(), MietteError> {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("oops!")]
|
||||
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
|
||||
struct MyBad {
|
||||
src: String,
|
||||
#[snippet(src, "This is the part that broke")]
|
||||
ctx: SourceSpan,
|
||||
#[highlight(ctx)]
|
||||
highlight: SourceSpan,
|
||||
}
|
||||
|
||||
let src = "source\n text\n here".to_string();
|
||||
let len = src.len();
|
||||
let err = MyBad {
|
||||
src,
|
||||
ctx: ("bad_file.rs", 0, len).into(),
|
||||
highlight: ("this bit here", 9, 4).into(),
|
||||
};
|
||||
let out = fmt_report(err.into());
|
||||
println!("{}", out);
|
||||
assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ source\n 2 │ text\n · ──┬─\n · ╰── this bit here\n 3 │ here\n\n﹦ try doing it better next time?\n".to_string(), out);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_line_highlight_no_label() -> Result<(), MietteError> {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("oops!")]
|
||||
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
|
||||
struct MyBad {
|
||||
src: String,
|
||||
#[snippet(src, "This is the part that broke")]
|
||||
ctx: SourceSpan,
|
||||
#[highlight(ctx)]
|
||||
highlight: SourceSpan,
|
||||
}
|
||||
|
||||
let src = "source\n text\n here".to_string();
|
||||
let len = src.len();
|
||||
let err = MyBad {
|
||||
src,
|
||||
ctx: ("bad_file.rs", 0, len).into(),
|
||||
highlight: (9, 4).into(),
|
||||
};
|
||||
let out = fmt_report(err.into());
|
||||
println!("{}", out);
|
||||
assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ source\n 2 │ text\n · ────\n 3 │ here\n\n﹦ try doing it better next time?\n".to_string(), out);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_same_line_highlights() -> Result<(), MietteError> {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("oops!")]
|
||||
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
|
||||
struct MyBad {
|
||||
src: String,
|
||||
#[snippet(src, "This is the part that broke")]
|
||||
ctx: SourceSpan,
|
||||
#[highlight(ctx)]
|
||||
highlight1: SourceSpan,
|
||||
#[highlight(ctx)]
|
||||
highlight2: SourceSpan,
|
||||
}
|
||||
|
||||
let src = "source\n text text text text text\n here".to_string();
|
||||
let len = src.len();
|
||||
let err = MyBad {
|
||||
src,
|
||||
ctx: ("bad_file.rs", 0, len).into(),
|
||||
highlight1: ("this bit here", 9, 4).into(),
|
||||
highlight2: ("also this bit", 14, 4).into(),
|
||||
};
|
||||
let out = fmt_report(err.into());
|
||||
println!("{}", out);
|
||||
assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ source\n 2 │ text text text text text\n · ──┬─ ──┬─\n · ╰── this bit here\n · ╰── also this bit\n 3 │ here\n\n﹦ try doing it better next time?\n".to_string(), out);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiline_highlight_adjacent() -> Result<(), MietteError> {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("oops!")]
|
||||
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
|
||||
struct MyBad {
|
||||
src: String,
|
||||
#[snippet(src, "This is the part that broke")]
|
||||
ctx: SourceSpan,
|
||||
#[highlight(ctx)]
|
||||
highlight: SourceSpan,
|
||||
}
|
||||
|
||||
let src = "source\n text\n here".to_string();
|
||||
let len = src.len();
|
||||
let err = MyBad {
|
||||
src,
|
||||
ctx: ("bad_file.rs", 0, len).into(),
|
||||
highlight: ("these two lines", 9, 11).into(),
|
||||
};
|
||||
let out = fmt_report(err.into());
|
||||
println!("{}", out);
|
||||
assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ source\n 2 │ ╭─▶ text\n 3 │ ├─▶ here\n · ╰──── these two lines\n\n﹦ try doing it better next time?\n".to_string(), out);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiline_highlight_flyby() -> Result<(), MietteError> {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("oops!")]
|
||||
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
|
||||
struct MyBad {
|
||||
src: String,
|
||||
#[snippet(src, "This is the part that broke")]
|
||||
ctx: SourceSpan,
|
||||
#[highlight(ctx)]
|
||||
highlight1: SourceSpan,
|
||||
#[highlight(ctx)]
|
||||
highlight2: SourceSpan,
|
||||
}
|
||||
|
||||
let src = r#"line1
|
||||
line2
|
||||
line3
|
||||
line4
|
||||
line5
|
||||
"#
|
||||
.to_string();
|
||||
let len = src.len();
|
||||
let err = MyBad {
|
||||
src,
|
||||
ctx: ("bad_file.rs", 0, len).into(),
|
||||
highlight1: ("block 1", 0, len).into(),
|
||||
highlight2: ("block 2", 10, 9).into(),
|
||||
};
|
||||
let out = fmt_report(err.into());
|
||||
println!("{}", out);
|
||||
assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ ╭──▶ line1\n 2 │ │╭─▶ line2\n 3 │ ││ line3\n 4 │ │├─▶ line4\n · │╰──── block 2\n 6 │ ├──▶ line5\n · ╰───── block 1\n\n﹦ try doing it better next time?\n".to_string(), out);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiline_highlight_no_label() -> Result<(), MietteError> {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("oops!")]
|
||||
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
|
||||
struct MyBad {
|
||||
src: String,
|
||||
#[snippet(src, "This is the part that broke")]
|
||||
ctx: SourceSpan,
|
||||
#[highlight(ctx)]
|
||||
highlight1: SourceSpan,
|
||||
#[highlight(ctx)]
|
||||
highlight2: SourceSpan,
|
||||
}
|
||||
|
||||
let src = r#"line1
|
||||
line2
|
||||
line3
|
||||
line4
|
||||
line5
|
||||
"#
|
||||
.to_string();
|
||||
let len = src.len();
|
||||
let err = MyBad {
|
||||
src,
|
||||
ctx: ("bad_file.rs", 0, len).into(),
|
||||
highlight1: ("block 1", 0, len).into(),
|
||||
highlight2: (10, 9).into(),
|
||||
};
|
||||
let out = fmt_report(err.into());
|
||||
println!("{}", out);
|
||||
assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ ╭──▶ line1\n 2 │ │╭─▶ line2\n 3 │ ││ line3\n 4 │ │╰─▶ line4\n 6 │ ├──▶ line5\n · ╰───── block 1\n\n﹦ try doing it better next time?\n".to_string(), out);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_multiline_highlights_adjacent() -> Result<(), MietteError> {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("oops!")]
|
||||
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
|
||||
struct MyBad {
|
||||
src: String,
|
||||
#[snippet(src, "This is the part that broke")]
|
||||
ctx: SourceSpan,
|
||||
#[highlight(ctx)]
|
||||
highlight1: SourceSpan,
|
||||
#[highlight(ctx)]
|
||||
highlight2: SourceSpan,
|
||||
}
|
||||
|
||||
let src = "source\n text\n here\nmore here".to_string();
|
||||
let len = src.len();
|
||||
let err = MyBad {
|
||||
src,
|
||||
ctx: ("bad_file.rs", 0, len).into(),
|
||||
highlight1: ("this bit here", 0, 10).into(),
|
||||
highlight2: ("also this bit", 20, 6).into(),
|
||||
};
|
||||
let out = fmt_report(err.into());
|
||||
println!("{}", out);
|
||||
assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ ╭─▶ source\n 2 │ ├─▶ text\n · ╰──── this bit here\n 3 │ ╭─▶ here\n 4 │ ├─▶ more here\n · ╰──── also this bit\n\n﹦ try doing it better next time?\n".to_string(), out);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
// TODO: This breaks because those highlights aren't "truly" overlapping (in absolute byte offset), but they ARE overlapping in lines. Need to detect the latter case better
|
||||
#[ignore]
|
||||
/// Lines are overlapping, but the offsets themselves aren't, so they _look_
|
||||
/// disjunct if you only look at offsets.
|
||||
fn multiple_multiline_highlights_overlapping_lines() -> Result<(), MietteError> {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("oops!")]
|
||||
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
|
||||
struct MyBad {
|
||||
src: String,
|
||||
#[snippet(src, "This is the part that broke")]
|
||||
ctx: SourceSpan,
|
||||
#[highlight(ctx)]
|
||||
highlight1: SourceSpan,
|
||||
#[highlight(ctx)]
|
||||
highlight2: SourceSpan,
|
||||
}
|
||||
|
||||
let src = "source\n text\n here".to_string();
|
||||
let len = src.len();
|
||||
let err = MyBad {
|
||||
src,
|
||||
ctx: ("bad_file.rs", 0, len).into(),
|
||||
highlight1: ("this bit here", 0, 8).into(),
|
||||
highlight2: ("also this bit", 9, 10).into(),
|
||||
};
|
||||
let out = fmt_report(err.into());
|
||||
println!("{}", out);
|
||||
assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ source\n 2 │ text\n · ──┬─\n · ╰── this bit here\n 3 │ here\n\n﹦ try doing it better next time?\n".to_string(), out);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Offsets themselves are overlapping, regardless of lines.
|
||||
#[ignore]
|
||||
fn multiple_multiline_highlights_overlapping_offsets() -> Result<(), MietteError> {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("oops!")]
|
||||
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
|
||||
struct MyBad {
|
||||
src: String,
|
||||
#[snippet(src, "This is the part that broke")]
|
||||
ctx: SourceSpan,
|
||||
#[highlight(ctx)]
|
||||
highlight1: SourceSpan,
|
||||
#[highlight(ctx)]
|
||||
highlight2: SourceSpan,
|
||||
}
|
||||
|
||||
let src = "source\n text\n here".to_string();
|
||||
let len = src.len();
|
||||
let err = MyBad {
|
||||
src,
|
||||
ctx: ("bad_file.rs", 0, len).into(),
|
||||
highlight1: ("this bit here", 0, 8).into(),
|
||||
highlight2: ("also this bit", 10, 10).into(),
|
||||
};
|
||||
let out = fmt_report(err.into());
|
||||
println!("{}", out);
|
||||
assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ source\n 2 │ text\n · ──┬─\n · ╰── this bit here\n 3 │ here\n\n﹦ try doing it better next time?\n".to_string(), out);
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
use miette::{Diagnostic, DiagnosticReport, DiagnosticSnippet, MietteError, SourceSpan};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("oops!")]
|
||||
struct MyBad {
|
||||
message: String,
|
||||
src: String,
|
||||
ctx: SourceSpan,
|
||||
highlight: SourceSpan,
|
||||
}
|
||||
|
||||
impl Diagnostic for MyBad {
|
||||
fn code(&self) -> Box<dyn std::fmt::Display> {
|
||||
Box::new(&"oops::my::bad")
|
||||
}
|
||||
|
||||
fn help(&self) -> Option<Box<dyn std::fmt::Display>> {
|
||||
Some(Box::new(&"try doing it better next time?"))
|
||||
}
|
||||
|
||||
fn snippets(&self) -> Option<Box<dyn Iterator<Item = DiagnosticSnippet> + '_>> {
|
||||
Some(Box::new(
|
||||
vec![DiagnosticSnippet {
|
||||
message: Some(self.message.as_ref()),
|
||||
source: &self.src,
|
||||
context: self.ctx.clone(),
|
||||
highlights: Some(vec![self.highlight.clone()]),
|
||||
}]
|
||||
.into_iter(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fancy() -> Result<(), MietteError> {
|
||||
let src = "source\n text\n here".to_string();
|
||||
let len = src.len();
|
||||
let err = MyBad {
|
||||
message: "This is the part that broke".into(),
|
||||
src,
|
||||
ctx: ("bad_file.rs", 0, len).into(),
|
||||
highlight: ("this bit here", 9, 4).into(),
|
||||
};
|
||||
let rep: DiagnosticReport = err.into();
|
||||
let out = format!("{:?}", rep);
|
||||
// println!("{}", out);
|
||||
assert_eq!("Error[oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 | source\n 2 | text\n ⫶ | ^^^^ this bit here\n 3 | here\n\n﹦try doing it better next time?\n".to_string(), out);
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Reference in New Issue