mirror of https://github.com/zkat/miette.git
feat(links): added URL linking support and automatic docs.rs link generation
Fixes: https://github.com/zkat/miette/issues/17
This commit is contained in:
parent
3546dcec98
commit
7e76e2dea4
52
README.md
52
README.md
|
|
@ -34,6 +34,7 @@ diagnostic error code: ruget::api::bad_json
|
||||||
- [... in libraries](#-in-libraries)
|
- [... in libraries](#-in-libraries)
|
||||||
- [... in application code](#-in-application-code)
|
- [... in application code](#-in-application-code)
|
||||||
- [... in `main()`](#-in-main)
|
- [... in `main()`](#-in-main)
|
||||||
|
- [... diagnostic code URLs](#-diagnostic-code-urls)
|
||||||
- [... snippets](#-snippets)
|
- [... snippets](#-snippets)
|
||||||
- [Acknowledgements](#acknowledgements)
|
- [Acknowledgements](#acknowledgements)
|
||||||
- [License](#license)
|
- [License](#license)
|
||||||
|
|
@ -42,6 +43,7 @@ diagnostic error code: ruget::api::bad_json
|
||||||
|
|
||||||
- Generic [Diagnostic] protocol, compatible (and dependent on) `std::error::Error`.
|
- Generic [Diagnostic] protocol, compatible (and dependent on) `std::error::Error`.
|
||||||
- Unique error codes on every [Diagnostic].
|
- Unique error codes on every [Diagnostic].
|
||||||
|
- Custom links to get more details on error codes.
|
||||||
- Super handy derive macro for defining diagnostic metadata.
|
- Super handy derive macro for defining diagnostic metadata.
|
||||||
- Lightweight [`anyhow`](https://docs.rs/anyhow)/[`eyre`](https://docs.rs/eyre)-style error wrapper type, [DiagnosticReport],
|
- Lightweight [`anyhow`](https://docs.rs/anyhow)/[`eyre`](https://docs.rs/eyre)-style error wrapper type, [DiagnosticReport],
|
||||||
which can be returned from `main`.
|
which can be returned from `main`.
|
||||||
|
|
@ -54,6 +56,7 @@ The `miette` crate also comes bundles with a default [DiagnosticReportPrinter] w
|
||||||
- Screen reader/braille support, gated on [`NO_COLOR`](http://no-color.org/), and other heuristics.
|
- Screen reader/braille support, gated on [`NO_COLOR`](http://no-color.org/), and other heuristics.
|
||||||
- Fully customizable graphical theming (or overriding the printers entirely).
|
- Fully customizable graphical theming (or overriding the printers entirely).
|
||||||
- Cause chain printing
|
- Cause chain printing
|
||||||
|
- Turns diagnostic codes into links in [supported terminals](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda).
|
||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
|
|
||||||
|
|
@ -219,6 +222,55 @@ fn pretend_this_is_main() -> DiagnosticResult<()> {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### ... diagnostic code URLs
|
||||||
|
|
||||||
|
`miette` supports providing a URL for individual diagnostics. This URL will be
|
||||||
|
displayed as an actual link in supported terminals, like so:
|
||||||
|
|
||||||
|
<img
|
||||||
|
src="https://raw.githubusercontent.com/zkat/miette/main/images/code_linking.png"
|
||||||
|
alt=" Example showing the graphical report printer for miette pretty-printing
|
||||||
|
an error code. The code is underlined and followed by text saying to 'click
|
||||||
|
here'. A hover tooltip shows a full-fledged URL that can be Ctrl+Clicked to
|
||||||
|
open in a browser.
|
||||||
|
\
|
||||||
|
This feature is also available in the narratable printer. It will add a line after printing the error code showing a plain URL that you can visit.
|
||||||
|
">
|
||||||
|
|
||||||
|
To use this, you can add a `url()` sub-param to your `#[diagnostic]` attribute:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use miette::Diagnostic;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Diagnostic, Debug)]
|
||||||
|
#[diagnostic(
|
||||||
|
code(my_app::my_error),
|
||||||
|
// You can do formatting!
|
||||||
|
url("https://my_website.com/error_codes#{}", self.code())
|
||||||
|
)]
|
||||||
|
struct MyErr;
|
||||||
|
```
|
||||||
|
|
||||||
|
Additionally, if you're developing a library and your error type is exported
|
||||||
|
from your crate's top level, you can use a special `url(docsrs)` option
|
||||||
|
instead of manually constructing the URL. This will automatically create a
|
||||||
|
link to this diagnostic on `docs.rs`, so folks can just go straight to
|
||||||
|
your (very high quality and detailed!) documentation on this diagnostic:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use miette::Diagnostic;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Diagnostic, Debug)]
|
||||||
|
#[diagnostic(
|
||||||
|
code(my_app::my_error),
|
||||||
|
// Will link users to https://docs.rs/my_crate/0.0.0/my_crate/struct.MyErr.html
|
||||||
|
url(docsrs)
|
||||||
|
)]
|
||||||
|
struct MyErr;
|
||||||
|
```
|
||||||
|
|
||||||
### ... snippets
|
### ... snippets
|
||||||
|
|
||||||
Along with its general error handling and reporting features, `miette` also
|
Along with its general error handling and reporting features, `miette` also
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 365 KiB |
|
|
@ -7,6 +7,7 @@ use crate::diagnostic_arg::DiagnosticArg;
|
||||||
use crate::help::Help;
|
use crate::help::Help;
|
||||||
use crate::severity::Severity;
|
use crate::severity::Severity;
|
||||||
use crate::snippets::Snippets;
|
use crate::snippets::Snippets;
|
||||||
|
use crate::url::Url;
|
||||||
|
|
||||||
pub enum Diagnostic {
|
pub enum Diagnostic {
|
||||||
Struct {
|
Struct {
|
||||||
|
|
@ -17,6 +18,7 @@ pub enum Diagnostic {
|
||||||
severity: Option<Severity>,
|
severity: Option<Severity>,
|
||||||
help: Option<Help>,
|
help: Option<Help>,
|
||||||
snippets: Option<Snippets>,
|
snippets: Option<Snippets>,
|
||||||
|
url: Option<Url>,
|
||||||
},
|
},
|
||||||
Enum {
|
Enum {
|
||||||
ident: syn::Ident,
|
ident: syn::Ident,
|
||||||
|
|
@ -32,6 +34,7 @@ pub struct DiagnosticVariant {
|
||||||
pub severity: Option<Severity>,
|
pub severity: Option<Severity>,
|
||||||
pub help: Option<Help>,
|
pub help: Option<Help>,
|
||||||
pub snippets: Option<Snippets>,
|
pub snippets: Option<Snippets>,
|
||||||
|
pub url: Option<Url>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Diagnostic {
|
impl Diagnostic {
|
||||||
|
|
@ -45,12 +48,16 @@ impl Diagnostic {
|
||||||
let mut code = None;
|
let mut code = None;
|
||||||
let mut severity = None;
|
let mut severity = None;
|
||||||
let mut help = None;
|
let mut help = None;
|
||||||
|
let mut url = None;
|
||||||
for arg in args {
|
for arg in args {
|
||||||
match arg {
|
match arg {
|
||||||
DiagnosticArg::Code(new_code) => {
|
DiagnosticArg::Code(new_code) => {
|
||||||
// TODO: error on multiple?
|
// TODO: error on multiple?
|
||||||
code = Some(new_code);
|
code = Some(new_code);
|
||||||
}
|
}
|
||||||
|
DiagnosticArg::Url(u) => {
|
||||||
|
url = Some(u);
|
||||||
|
}
|
||||||
DiagnosticArg::Severity(sev) => {
|
DiagnosticArg::Severity(sev) => {
|
||||||
severity = Some(sev);
|
severity = Some(sev);
|
||||||
}
|
}
|
||||||
|
|
@ -69,6 +76,7 @@ impl Diagnostic {
|
||||||
help,
|
help,
|
||||||
severity,
|
severity,
|
||||||
snippets,
|
snippets,
|
||||||
|
url,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Also handle when there's multiple `#[diagnostic]` attrs?
|
// Also handle when there's multiple `#[diagnostic]` attrs?
|
||||||
|
|
@ -88,6 +96,7 @@ impl Diagnostic {
|
||||||
let mut code = None;
|
let mut code = None;
|
||||||
let mut severity = None;
|
let mut severity = None;
|
||||||
let mut help = None;
|
let mut help = None;
|
||||||
|
let mut url = None;
|
||||||
for arg in args {
|
for arg in args {
|
||||||
match arg {
|
match arg {
|
||||||
DiagnosticArg::Code(new_code) => {
|
DiagnosticArg::Code(new_code) => {
|
||||||
|
|
@ -100,6 +109,9 @@ impl Diagnostic {
|
||||||
DiagnosticArg::Help(hl) => {
|
DiagnosticArg::Help(hl) => {
|
||||||
help = Some(hl);
|
help = Some(hl);
|
||||||
}
|
}
|
||||||
|
DiagnosticArg::Url(u) => {
|
||||||
|
url = Some(u);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let snippets = Snippets::from_fields(&var.fields)?;
|
let snippets = Snippets::from_fields(&var.fields)?;
|
||||||
|
|
@ -113,6 +125,7 @@ impl Diagnostic {
|
||||||
help,
|
help,
|
||||||
severity,
|
severity,
|
||||||
snippets,
|
snippets,
|
||||||
|
url,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Also handle when there's multiple `#[diagnostic]` attrs?
|
// Also handle when there's multiple `#[diagnostic]` attrs?
|
||||||
|
|
@ -147,12 +160,14 @@ impl Diagnostic {
|
||||||
severity,
|
severity,
|
||||||
help,
|
help,
|
||||||
snippets,
|
snippets,
|
||||||
|
url,
|
||||||
} => {
|
} => {
|
||||||
let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl();
|
let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl();
|
||||||
let code_body = code.gen_struct();
|
let code_body = code.gen_struct();
|
||||||
let help_body = help.as_ref().and_then(|x| x.gen_struct(fields));
|
let help_body = help.as_ref().and_then(|x| x.gen_struct(fields));
|
||||||
let sev_body = severity.as_ref().and_then(|x| x.gen_struct());
|
let sev_body = severity.as_ref().and_then(|x| x.gen_struct());
|
||||||
let snip_body = snippets.as_ref().and_then(|x| x.gen_struct());
|
let snip_body = snippets.as_ref().and_then(|x| x.gen_struct());
|
||||||
|
let url_body = url.as_ref().and_then(|x| x.gen_struct(ident, fields));
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
|
impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
|
||||||
|
|
@ -160,6 +175,7 @@ impl Diagnostic {
|
||||||
#help_body
|
#help_body
|
||||||
#sev_body
|
#sev_body
|
||||||
#snip_body
|
#snip_body
|
||||||
|
#url_body
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -173,13 +189,14 @@ impl Diagnostic {
|
||||||
let help_body = Help::gen_enum(variants);
|
let help_body = Help::gen_enum(variants);
|
||||||
let sev_body = Severity::gen_enum(variants);
|
let sev_body = Severity::gen_enum(variants);
|
||||||
let snip_body = Snippets::gen_enum(variants);
|
let snip_body = Snippets::gen_enum(variants);
|
||||||
|
let url_body = Url::gen_enum(ident, variants);
|
||||||
quote! {
|
quote! {
|
||||||
impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
|
impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
|
||||||
#code_body
|
#code_body
|
||||||
#help_body
|
#help_body
|
||||||
#sev_body
|
#sev_body
|
||||||
#snip_body
|
#snip_body
|
||||||
|
#url_body
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ use syn::parse::{Parse, ParseStream};
|
||||||
use crate::code::Code;
|
use crate::code::Code;
|
||||||
use crate::help::Help;
|
use crate::help::Help;
|
||||||
use crate::severity::Severity;
|
use crate::severity::Severity;
|
||||||
|
use crate::url::Url;
|
||||||
|
|
||||||
pub enum DiagnosticArg {
|
pub enum DiagnosticArg {
|
||||||
Code(Code),
|
Code(Code),
|
||||||
Severity(Severity),
|
Severity(Severity),
|
||||||
Help(Help),
|
Help(Help),
|
||||||
|
Url(Url),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Parse for DiagnosticArg {
|
impl Parse for DiagnosticArg {
|
||||||
|
|
@ -19,6 +21,8 @@ impl Parse for DiagnosticArg {
|
||||||
Ok(DiagnosticArg::Severity(input.parse()?))
|
Ok(DiagnosticArg::Severity(input.parse()?))
|
||||||
} else if ident == "help" {
|
} else if ident == "help" {
|
||||||
Ok(DiagnosticArg::Help(input.parse()?))
|
Ok(DiagnosticArg::Help(input.parse()?))
|
||||||
|
} else if ident == "url" {
|
||||||
|
Ok(DiagnosticArg::Url(input.parse()?))
|
||||||
} else {
|
} else {
|
||||||
Err(syn::Error::new(
|
Err(syn::Error::new(
|
||||||
ident.span(),
|
ident.span(),
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ mod fmt;
|
||||||
mod help;
|
mod help;
|
||||||
mod severity;
|
mod severity;
|
||||||
mod snippets;
|
mod snippets;
|
||||||
|
mod url;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
#[proc_macro_derive(Diagnostic, attributes(diagnostic, snippet, highlight))]
|
#[proc_macro_derive(Diagnostic, attributes(diagnostic, snippet, highlight))]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use syn::{
|
||||||
|
parenthesized,
|
||||||
|
parse::{Parse, ParseStream},
|
||||||
|
spanned::Spanned,
|
||||||
|
Fields, Token,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::diagnostic::DiagnosticVariant;
|
||||||
|
use crate::fmt::{self, Display};
|
||||||
|
|
||||||
|
pub enum Url {
|
||||||
|
Display(Display),
|
||||||
|
DocsRs,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for Url {
|
||||||
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
|
let ident = input.parse::<syn::Ident>()?;
|
||||||
|
if ident == "url" {
|
||||||
|
let la = input.lookahead1();
|
||||||
|
if la.peek(syn::token::Paren) {
|
||||||
|
let content;
|
||||||
|
parenthesized!(content in input);
|
||||||
|
if content.peek(syn::LitStr) {
|
||||||
|
let fmt = content.parse()?;
|
||||||
|
let args = if content.is_empty() {
|
||||||
|
TokenStream::new()
|
||||||
|
} else {
|
||||||
|
content.parse::<Token![,]>()?;
|
||||||
|
fmt::parse_token_expr(&content, false)?
|
||||||
|
};
|
||||||
|
let display = Display {
|
||||||
|
fmt,
|
||||||
|
args,
|
||||||
|
has_bonus_display: false,
|
||||||
|
};
|
||||||
|
Ok(Url::Display(display))
|
||||||
|
} else {
|
||||||
|
let option = content.parse::<syn::Ident>()?;
|
||||||
|
if option == "docsrs" {
|
||||||
|
Ok(Url::DocsRs)
|
||||||
|
} else {
|
||||||
|
Err(syn::Error::new(option.span(), "Invalid argument to url() sub-attribute. It must be either a string or a plain `docsrs` identifier"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
input.parse::<Token![=]>()?;
|
||||||
|
Ok(Url::Display(Display {
|
||||||
|
fmt: input.parse()?,
|
||||||
|
args: TokenStream::new(),
|
||||||
|
has_bonus_display: false,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(syn::Error::new(ident.span(), "not a url"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Url {
|
||||||
|
pub(crate) fn gen_enum(
|
||||||
|
enum_name: &syn::Ident,
|
||||||
|
variants: &[DiagnosticVariant],
|
||||||
|
) -> Option<TokenStream> {
|
||||||
|
let url_pairs = variants
|
||||||
|
.iter()
|
||||||
|
.filter(|v| v.url.is_some())
|
||||||
|
.map(
|
||||||
|
|DiagnosticVariant {
|
||||||
|
ref ident,
|
||||||
|
ref url,
|
||||||
|
ref fields,
|
||||||
|
..
|
||||||
|
}| {
|
||||||
|
let member_idents = fields.iter().enumerate().map(|(i, field)| {
|
||||||
|
field
|
||||||
|
.ident
|
||||||
|
.as_ref()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| format_ident!("_{}", i))
|
||||||
|
});
|
||||||
|
let members: HashSet<syn::Member> = fields.iter().enumerate().map(|(i, field)| {
|
||||||
|
if let Some(ident) = field.ident.as_ref().cloned() {
|
||||||
|
syn::Member::Named(ident)
|
||||||
|
} else {
|
||||||
|
syn::Member::Unnamed(syn::Index { index: i as u32, span: field.span() })
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
let (fmt, args) = match url.as_ref().expect("MIETTE BUG: we already checked for `Some`") {
|
||||||
|
Url::Display(display) => {
|
||||||
|
let mut display = display.clone();
|
||||||
|
display.expand_shorthand(&members);
|
||||||
|
let Display { fmt, args, .. } = display;
|
||||||
|
(fmt.value(), args)
|
||||||
|
}
|
||||||
|
Url::DocsRs => {
|
||||||
|
let fmt = "https://docs.rs/{crate_name}/{crate_version}/{crate_name}/{item_path}".into();
|
||||||
|
let item_path = format!("enum.{}.html#variant.{}", enum_name, ident);
|
||||||
|
let args = quote! {
|
||||||
|
crate_name=env!("CARGO_PKG_NAME"),
|
||||||
|
crate_version=env!("CARGO_PKG_VERSION"),
|
||||||
|
item_path=#item_path
|
||||||
|
};
|
||||||
|
(fmt, args)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match fields {
|
||||||
|
syn::Fields::Named(_) => {
|
||||||
|
quote! { Self::#ident{ #(#member_idents),* } => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))), }
|
||||||
|
}
|
||||||
|
syn::Fields::Unnamed(_) => {
|
||||||
|
quote! { Self::#ident( #(#member_idents),* ) => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))), }
|
||||||
|
}
|
||||||
|
syn::Fields::Unit =>
|
||||||
|
quote! { Self::#ident => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args))), },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if url_pairs.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(quote! {
|
||||||
|
fn url<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
|
||||||
|
#[allow(unused_variables, deprecated)]
|
||||||
|
match self {
|
||||||
|
#(#url_pairs)*
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn gen_struct(
|
||||||
|
&self,
|
||||||
|
struct_name: &syn::Ident,
|
||||||
|
fields: &Fields,
|
||||||
|
) -> Option<TokenStream> {
|
||||||
|
let members: HashSet<syn::Member> = fields
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, field)| {
|
||||||
|
if let Some(ident) = field.ident.as_ref().cloned() {
|
||||||
|
syn::Member::Named(ident)
|
||||||
|
} else {
|
||||||
|
syn::Member::Unnamed(syn::Index {
|
||||||
|
index: i as u32,
|
||||||
|
span: field.span(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let (fmt, args) = match self {
|
||||||
|
Url::Display(display) => {
|
||||||
|
let mut display = display.clone();
|
||||||
|
display.expand_shorthand(&members);
|
||||||
|
let Display { fmt, args, .. } = display;
|
||||||
|
(fmt.value(), args)
|
||||||
|
}
|
||||||
|
Url::DocsRs => {
|
||||||
|
let fmt =
|
||||||
|
"https://docs.rs/{crate_name}/{crate_version}/{crate_name}/{item_path}".into();
|
||||||
|
let item_path = format!("struct.{}.html", struct_name);
|
||||||
|
let args = quote! {
|
||||||
|
crate_name=env!("CARGO_PKG_NAME"),
|
||||||
|
crate_version=env!("CARGO_PKG_VERSION"),
|
||||||
|
item_path=#item_path
|
||||||
|
};
|
||||||
|
(fmt, args)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let members = members.iter();
|
||||||
|
let fields_pat = match fields {
|
||||||
|
Fields::Named(_) => quote! {
|
||||||
|
let Self { #(#members),* } = self;
|
||||||
|
},
|
||||||
|
Fields::Unnamed(_) => {
|
||||||
|
let vars = members.map(|member| {
|
||||||
|
if let syn::Member::Unnamed(member) = member {
|
||||||
|
format_ident!("_{}", member)
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
quote! {
|
||||||
|
let Self(#(#vars),*) = self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Fields::Unit => quote! {},
|
||||||
|
};
|
||||||
|
Some(quote! {
|
||||||
|
fn url<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
|
||||||
|
#[allow(unused_variables, deprecated)]
|
||||||
|
#fields_pat
|
||||||
|
std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #args)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,7 @@ miette::set_printer(GraphicalReportPrinter::new_themed(GraphicalTheme::unicode_n
|
||||||
*/
|
*/
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct GraphicalReportPrinter {
|
pub struct GraphicalReportPrinter {
|
||||||
|
pub(crate) linkify_code: bool,
|
||||||
pub(crate) theme: GraphicalTheme,
|
pub(crate) theme: GraphicalTheme,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,13 +34,23 @@ impl GraphicalReportPrinter {
|
||||||
/// [GraphicalTheme]. This will use both unicode characters and colors.
|
/// [GraphicalTheme]. This will use both unicode characters and colors.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
linkify_code: true,
|
||||||
theme: GraphicalTheme::default(),
|
theme: GraphicalTheme::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///Create a new [GraphicalReportPrinter] with a given [GraphicalTheme].
|
///Create a new [GraphicalReportPrinter] with a given [GraphicalTheme].
|
||||||
pub fn new_themed(theme: GraphicalTheme) -> Self {
|
pub fn new_themed(theme: GraphicalTheme) -> Self {
|
||||||
Self { theme }
|
Self {
|
||||||
|
linkify_code: true,
|
||||||
|
theme,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disables error code linkification using [Diagnostic::url].
|
||||||
|
pub fn without_code_linking(mut self) -> Self {
|
||||||
|
self.linkify_code = false;
|
||||||
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,14 +90,16 @@ impl GraphicalReportPrinter {
|
||||||
Some(Severity::Warning) => (self.theme.styles.warning, self.theme.characters.warning),
|
Some(Severity::Warning) => (self.theme.styles.warning, self.theme.characters.warning),
|
||||||
Some(Severity::Advice) => (self.theme.styles.advice, self.theme.characters.point_right),
|
Some(Severity::Advice) => (self.theme.styles.advice, self.theme.characters.point_right),
|
||||||
};
|
};
|
||||||
let code = diagnostic.code();
|
write!(f, "{}", self.theme.characters.hbar.to_string().repeat(4))?;
|
||||||
writeln!(
|
if self.linkify_code && diagnostic.url().is_some() {
|
||||||
f,
|
let url = diagnostic.url().unwrap(); // safe
|
||||||
"{}[{}]{}",
|
let code = format!("{} (click for details)", diagnostic.code());
|
||||||
self.theme.characters.hbar.to_string().repeat(4),
|
let link = format!("\u{1b}]8;;{}\u{1b}\\{}\u{1b}]8;;\u{1b}\\", url, code);
|
||||||
code.style(self.theme.styles.code),
|
write!(f, "[{}]", link.style(self.theme.styles.code))?;
|
||||||
self.theme.characters.hbar.to_string().repeat(20),
|
} else {
|
||||||
)?;
|
write!(f, "[{}]", diagnostic.code().style(self.theme.styles.code))?;
|
||||||
|
}
|
||||||
|
writeln!(f, "{}", self.theme.characters.hbar.to_string().repeat(20),)?;
|
||||||
writeln!(f)?;
|
writeln!(f)?;
|
||||||
writeln!(
|
writeln!(
|
||||||
f,
|
f,
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,7 @@ fn get_default_printer() -> Box<dyn DiagnosticReportPrinter + Send + Sync + 'sta
|
||||||
atty::is(Stream::Stdout) && atty::is(Stream::Stderr) && !ci_info::is_ci()
|
atty::is(Stream::Stdout) && atty::is(Stream::Stderr) && !ci_info::is_ci()
|
||||||
};
|
};
|
||||||
if fancy {
|
if fancy {
|
||||||
Box::new(GraphicalReportPrinter {
|
Box::new(GraphicalReportPrinter::new())
|
||||||
theme: GraphicalTheme::default(),
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
Box::new(NarratableReportPrinter)
|
Box::new(NarratableReportPrinter)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,9 @@ impl NarratableReportPrinter {
|
||||||
writeln!(f, "diagnostic help: {}", help)?;
|
writeln!(f, "diagnostic help: {}", help)?;
|
||||||
}
|
}
|
||||||
writeln!(f, "diagnostic error code: {}", diagnostic.code())?;
|
writeln!(f, "diagnostic error code: {}", diagnostic.code())?;
|
||||||
|
if let Some(url) = diagnostic.url() {
|
||||||
|
writeln!(f, "For more details, see {}", url)?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ impl GraphicalTheme {
|
||||||
pub fn unicode() -> Self {
|
pub fn unicode() -> Self {
|
||||||
Self {
|
Self {
|
||||||
characters: ThemeCharacters::unicode(),
|
characters: ThemeCharacters::unicode(),
|
||||||
styles: ThemeStyles::ansi(),
|
styles: ThemeStyles::rgb(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,11 @@ pub trait Diagnostic: std::error::Error {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// URL to visit for a more details explanation/help about this Diagnostic.
|
||||||
|
fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Additional contextual snippets. This is typically used for adding
|
/// Additional contextual snippets. This is typically used for adding
|
||||||
/// marked-up source file output the way compilers often do.
|
/// marked-up source file output the way compilers often do.
|
||||||
fn snippets(&self) -> Option<Box<dyn Iterator<Item = DiagnosticSnippet<'_>> + '_>> {
|
fn snippets(&self) -> Option<Box<dyn Iterator<Item = DiagnosticSnippet<'_>> + '_>> {
|
||||||
|
|
|
||||||
|
|
@ -293,3 +293,32 @@ fn test_snippet_enum() {
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn url_basic() {
|
||||||
|
#[derive(Debug, Diagnostic, Error)]
|
||||||
|
#[error("welp")]
|
||||||
|
#[diagnostic(code(foo::bar::baz), url("https://example.com/foo/bar"))]
|
||||||
|
struct Foo {}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
"https://example.com/foo/bar".to_string(),
|
||||||
|
Foo {}.url().unwrap().to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn url_docsrs() {
|
||||||
|
#[derive(Debug, Diagnostic, Error)]
|
||||||
|
#[error("welp")]
|
||||||
|
#[diagnostic(code(foo::bar::baz), url(docsrs))]
|
||||||
|
struct Foo {}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
format!(
|
||||||
|
"https://docs.rs/miette/{}/miette/struct.Foo.html",
|
||||||
|
env!("CARGO_PKG_VERSION")
|
||||||
|
),
|
||||||
|
Foo {}.url().unwrap().to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -407,3 +407,45 @@ fn multiple_multiline_highlights_overlapping_offsets() -> Result<(), MietteError
|
||||||
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);
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn url_links() -> Result<(), MietteError> {
|
||||||
|
#[derive(Debug, Diagnostic, Error)]
|
||||||
|
#[error("oops!")]
|
||||||
|
#[diagnostic(
|
||||||
|
code(oops::my::bad),
|
||||||
|
help("try doing it better next time?"),
|
||||||
|
url("https://example.com")
|
||||||
|
)]
|
||||||
|
struct MyBad;
|
||||||
|
let err = MyBad;
|
||||||
|
let out = fmt_report(err.into());
|
||||||
|
println!("{}", out);
|
||||||
|
assert!(out.contains("https://example.com"));
|
||||||
|
assert!(out.contains("click for details"));
|
||||||
|
assert!(out.contains("oops::my::bad"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disable_url_links() -> Result<(), MietteError> {
|
||||||
|
#[derive(Debug, Diagnostic, Error)]
|
||||||
|
#[error("oops!")]
|
||||||
|
#[diagnostic(
|
||||||
|
code(oops::my::bad),
|
||||||
|
help("try doing it better next time?"),
|
||||||
|
url("https://example.com")
|
||||||
|
)]
|
||||||
|
struct MyBad;
|
||||||
|
let err = MyBad;
|
||||||
|
let mut out = String::new();
|
||||||
|
GraphicalReportPrinter::new_themed(GraphicalTheme::unicode_nocolor())
|
||||||
|
.without_code_linking()
|
||||||
|
.render_report(&mut out, &err)
|
||||||
|
.unwrap();
|
||||||
|
println!("{}", out);
|
||||||
|
assert!(!out.contains("https://example.com"));
|
||||||
|
assert!(!out.contains("click for details"));
|
||||||
|
assert!(out.contains("oops::my::bad"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue