diff --git a/miette-derive/src/label.rs b/miette-derive/src/label.rs index e0bc70a..232925e 100644 --- a/miette-derive/src/label.rs +++ b/miette-derive/src/label.rs @@ -20,6 +20,7 @@ struct Label { label: Option, ty: syn::Type, span: syn::Member, + primary: bool, } struct LabelAttr { @@ -90,7 +91,16 @@ impl Labels { let mut labels = Vec::new(); for (i, field) in fields.iter().enumerate() { for attr in &field.attrs { - if attr.path().is_ident("label") { + let is_label = attr.path().is_ident("label"); + let is_primary_label = attr.path().is_ident("primary_label"); + if is_label || is_primary_label { + if is_primary_label && labels.iter().any(|l: &Label| l.primary) { + return Err(syn::Error::new( + field.span(), + "Cannot have more than one primary label.", + )); + } + let span = if let Some(ident) = field.ident.clone() { syn::Member::Named(ident) } else { @@ -106,6 +116,7 @@ impl Labels { label, span, ty: field.ty.clone(), + primary: is_primary_label, }); } } @@ -120,13 +131,23 @@ impl Labels { pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option { let (display_pat, display_members) = display_pat_members(fields); let labels = self.0.iter().map(|highlight| { - let Label { span, label, ty } = highlight; + let Label { + span, + label, + ty, + primary, + } = highlight; let var = quote! { __miette_internal_var }; + let ctor = if *primary { + quote! { miette::LabeledSpan::new_primary_with_span } + } else { + quote! { miette::LabeledSpan::new_with_span } + }; if let Some(display) = label { let (fmt, args) = display.expand_shorthand_cloned(&display_members); quote! { miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span) - .map(|#var| miette::LabeledSpan::new_with_span( + .map(|#var| #ctor( std::option::Option::Some(format!(#fmt #args)), #var.clone(), )) @@ -134,7 +155,7 @@ impl Labels { } else { quote! { miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span) - .map(|#var| miette::LabeledSpan::new_with_span( + .map(|#var| #ctor( std::option::Option::None, #var.clone(), )) @@ -161,7 +182,7 @@ impl Labels { let (display_pat, display_members) = display_pat_members(fields); labels.as_ref().and_then(|labels| { let variant_labels = labels.0.iter().map(|label| { - let Label { span, label, ty } = label; + let Label { span, label, ty, primary } = label; let field = match &span { syn::Member::Named(ident) => ident.clone(), syn::Member::Unnamed(syn::Index { index, .. }) => { @@ -169,11 +190,16 @@ impl Labels { } }; let var = quote! { __miette_internal_var }; + let ctor = if *primary { + quote! { miette::LabeledSpan::new_primary_with_span } + } else { + quote! { miette::LabeledSpan::new_with_span } + }; if let Some(display) = label { let (fmt, args) = display.expand_shorthand_cloned(&display_members); quote! { miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field) - .map(|#var| miette::LabeledSpan::new_with_span( + .map(|#var| #ctor( std::option::Option::Some(format!(#fmt #args)), #var.clone(), )) @@ -181,7 +207,7 @@ impl Labels { } else { quote! { miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field) - .map(|#var| miette::LabeledSpan::new_with_span( + .map(|#var| #ctor( std::option::Option::None, #var.clone(), )) diff --git a/miette-derive/src/lib.rs b/miette-derive/src/lib.rs index 0f7e64e..31d77fd 100644 --- a/miette-derive/src/lib.rs +++ b/miette-derive/src/lib.rs @@ -19,7 +19,15 @@ mod utils; #[proc_macro_derive( Diagnostic, - attributes(diagnostic, source_code, label, related, help, diagnostic_source) + attributes( + diagnostic, + source_code, + label, + primary_label, + related, + help, + diagnostic_source + ) )] pub fn derive_diagnostic(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = parse_macro_input!(input as DeriveInput); diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index b5dd754..0bdf6c1 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -391,6 +391,8 @@ impl GraphicalReportHandler { ) -> fmt::Result { let (contents, lines) = self.get_lines(source, context.inner())?; + let primary_label = labels.iter().find(|label| label.primary()); + // sorting is your friend let labels = labels .iter() @@ -431,19 +433,33 @@ impl GraphicalReportHandler { self.theme.characters.hbar, )?; - if let Some(source_name) = contents.name() { + // If there is a primary label, then use its span + // as the reference point for line/column information. + let primary_contents = match primary_label { + Some(label) => source + .read_span(label.inner(), 0, 0) + .map_err(|_| fmt::Error)?, + None => contents, + }; + + if let Some(source_name) = primary_contents.name() { let source_name = source_name.style(self.theme.styles.link); writeln!( f, "[{}:{}:{}]", source_name, - contents.line() + 1, - contents.column() + 1 + primary_contents.line() + 1, + primary_contents.column() + 1 )?; } else if lines.len() <= 1 { writeln!(f, "{}", self.theme.characters.hbar.to_string().repeat(3))?; } else { - writeln!(f, "[{}:{}]", contents.line() + 1, contents.column() + 1)?; + writeln!( + f, + "[{}:{}]", + primary_contents.line() + 1, + primary_contents.column() + 1 + )?; } // Now it's time for the fun part--actually rendering everything! diff --git a/src/miette_diagnostic.rs b/src/miette_diagnostic.rs index 67b75d0..4135103 100644 --- a/src/miette_diagnostic.rs +++ b/src/miette_diagnostic.rs @@ -292,14 +292,16 @@ fn test_serialize_miette_diagnostic() { "offset": 0, "length": 0 }, - "label": "label1" + "label": "label1", + "primary": false }, { "span": { "offset": 1, "length": 2 }, - "label": "label2" + "label": "label2", + "primary": false } ] }); @@ -350,14 +352,16 @@ fn test_deserialize_miette_diagnostic() { "offset": 0, "length": 0 }, - "label": "label1" + "label": "label1", + "primary": false }, { "span": { "offset": 1, "length": 2 }, - "label": "label2" + "label": "label2", + "primary": false } ] }); diff --git a/src/protocol.rs b/src/protocol.rs index 36c3539..fd032bd 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -249,6 +249,7 @@ pub struct LabeledSpan { #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] label: Option, span: SourceSpan, + primary: bool, } impl LabeledSpan { @@ -257,6 +258,7 @@ impl LabeledSpan { Self { label, span: SourceSpan::new(SourceOffset(offset), SourceOffset(len)), + primary: false, } } @@ -265,6 +267,16 @@ impl LabeledSpan { Self { label, span: span.into(), + primary: false, + } + } + + /// Makes a new labeled primary span using an existing span. + pub fn new_primary_with_span(label: Option, span: impl Into) -> Self { + Self { + label, + span: span.into(), + primary: true, } } @@ -340,6 +352,11 @@ impl LabeledSpan { pub const fn is_empty(&self) -> bool { self.span.is_empty() } + + /// True if this `LabeledSpan` is a primary span. + pub const fn primary(&self) -> bool { + self.primary + } } #[cfg(feature = "serde")] @@ -350,7 +367,8 @@ fn test_serialize_labeled_span() { assert_eq!( json!(LabeledSpan::new(None, 0, 0)), json!({ - "span": { "offset": 0, "length": 0 } + "span": { "offset": 0, "length": 0, }, + "primary": false, }) ); @@ -358,7 +376,8 @@ fn test_serialize_labeled_span() { json!(LabeledSpan::new(Some("label".to_string()), 0, 0)), json!({ "label": "label", - "span": { "offset": 0, "length": 0 } + "span": { "offset": 0, "length": 0, }, + "primary": false, }) ) } @@ -370,20 +389,23 @@ fn test_deserialize_labeled_span() { let span: LabeledSpan = serde_json::from_value(json!({ "label": null, - "span": { "offset": 0, "length": 0 } + "span": { "offset": 0, "length": 0, }, + "primary": false, })) .unwrap(); assert_eq!(span, LabeledSpan::new(None, 0, 0)); let span: LabeledSpan = serde_json::from_value(json!({ - "span": { "offset": 0, "length": 0 } + "span": { "offset": 0, "length": 0, }, + "primary": false })) .unwrap(); assert_eq!(span, LabeledSpan::new(None, 0, 0)); let span: LabeledSpan = serde_json::from_value(json!({ "label": "label", - "span": { "offset": 0, "length": 0 } + "span": { "offset": 0, "length": 0, }, + "primary": false })) .unwrap(); assert_eq!(span, LabeledSpan::new(Some("label".to_string()), 0, 0)) diff --git a/tests/graphical.rs b/tests/graphical.rs index 0c69470..87a6c21 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -1212,3 +1212,33 @@ fn zero_length_eol_span() { assert_eq!(expected, out); } + +#[test] +fn primary_label() { + #[derive(Error, Debug, Diagnostic)] + #[error("oops!")] + struct MyBad { + #[source_code] + src: NamedSource, + #[primary_label("The root cause")] + bad_bit: SourceSpan, + } + let err = MyBad { + src: NamedSource::new("issue", "this is the first line\nthis is the second line"), + bad_bit: (24, 4).into(), + }; + let out = fmt_report(err.into()); + println!("Error: {}", out); + + let expected = r#" × oops! + ╭─[issue:2:2] + 1 │ this is the first line + 2 │ this is the second line + · ──┬─ + · ╰── The root cause + ╰──── +"# + .to_string(); + + assert_eq!(expected, out); +}