diff --git a/Cargo.toml b/Cargo.toml index 52d3e051..41afbf8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index f886891f..f151b754 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/miette-derive/src/fmt.rs b/miette-derive/src/fmt.rs index d076e761..f26e08f0 100644 --- a/miette-derive/src/fmt.rs +++ b/miette-derive/src/fmt.rs @@ -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 { diff --git a/src/lib.rs b/src/lib.rs index 1400894c..1c189952 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/printer/default_reporter.rs b/src/printer/default_reporter.rs new file mode 100644 index 00000000..3ce73318 --- /dev/null +++ b/src/printer/default_reporter.rs @@ -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::>(); + + // 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, 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 { + 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() + } +} diff --git a/src/printer/mod.rs b/src/printer/mod.rs new file mode 100644 index 00000000..407a8fc3 --- /dev/null +++ b/src/printer/mod.rs @@ -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> = + 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::>>() + }).flatten().map(|x| x.join(", ")).unwrap_or_else(||"try and cause miette to panic".into()) + )?; + + Ok(()) + } +} diff --git a/src/printer/theme.rs b/src/printer/theme.rs new file mode 100644 index 00000000..0eff3bea --- /dev/null +++ b/src/printer/theme.rs @@ -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