feat(protocol): new SourceSpans with labels

This commit is contained in:
Kat Marchán 2021-08-17 08:40:30 -07:00
parent 36b86df9f5
commit acfeb9c5b0
No known key found for this signature in database
GPG Key ID: AEB529C08A3C7E9E
7 changed files with 89 additions and 133 deletions

View File

@ -104,8 +104,8 @@ fn pretend_this_is_main() -> Result<(), MyBad> {
Err(MyBad { Err(MyBad {
src: Arc::new(src), src: Arc::new(src),
filename: "bad_file.rs".into(), filename: "bad_file.rs".into(),
snip: (0, (len - 1)).into(), snip: (0, len).into(),
bad_bit: (9, 12).into(), bad_bit: (9, 3).into(),
}) })
} }
``` ```

View File

@ -16,25 +16,21 @@ pub struct Snippets(Vec<Snippet>);
struct Snippet { struct Snippet {
message: Option<MemberOrString>, message: Option<MemberOrString>,
highlights: Vec<Highlight>, highlights: Vec<Highlight>,
source_name: MemberOrString,
source: syn::Member, source: syn::Member,
snippet: syn::Member, snippet: syn::Member,
} }
struct Highlight { struct Highlight {
highlight: syn::Member, highlight: syn::Member,
label: Option<MemberOrString>,
} }
struct SnippetAttr { struct SnippetAttr {
source: syn::Member, source: syn::Member,
source_name: MemberOrString,
message: Option<MemberOrString>, message: Option<MemberOrString>,
} }
struct HighlightAttr { struct HighlightAttr {
snippet: syn::Member, snippet: syn::Member,
label: Option<MemberOrString>,
} }
enum MemberOrString { enum MemberOrString {
@ -82,15 +78,8 @@ impl Parse for SnippetAttr {
)) ))
} }
}; };
let src_name = iter
.next()
.ok_or_else(|| syn::Error::new(span, "Expected a source name."))?;
let message = iter.next(); let message = iter.next();
Ok(SnippetAttr { Ok(SnippetAttr { source, message })
source,
source_name: src_name,
message,
})
} }
} }
@ -107,8 +96,7 @@ impl Parse for HighlightAttr {
"must be an identifier that refers to something with a #[snippet] attribute.", "must be an identifier that refers to something with a #[snippet] attribute.",
)), )),
}; };
let label = iter.next(); Ok(HighlightAttr { snippet })
Ok(HighlightAttr { snippet, label })
} }
} }
@ -137,18 +125,13 @@ impl Snippets {
span: field.span(), span: field.span(),
}) })
}; };
let SnippetAttr { let SnippetAttr { source, message } = attr.parse_args::<SnippetAttr>()?;
source,
message,
source_name,
} = attr.parse_args::<SnippetAttr>()?;
// TODO: useful error when source refers to a field that doesn't exist. // TODO: useful error when source refers to a field that doesn't exist.
snippets.insert( snippets.insert(
snippet.clone(), snippet.clone(),
Snippet { Snippet {
message, message,
highlights: Vec::new(), highlights: Vec::new(),
source_name,
source, source,
snippet, snippet,
}, },
@ -160,7 +143,7 @@ impl Snippets {
for (i, field) in fields.iter().enumerate() { for (i, field) in fields.iter().enumerate() {
for attr in &field.attrs { for attr in &field.attrs {
if attr.path.is_ident("highlight") { if attr.path.is_ident("highlight") {
let HighlightAttr { snippet, label } = attr.parse_args::<HighlightAttr>()?; let HighlightAttr { snippet } = attr.parse_args::<HighlightAttr>()?;
if let Some(snippet) = snippets.get_mut(&snippet) { if let Some(snippet) = snippets.get_mut(&snippet) {
let member = if let Some(ident) = field.ident.clone() { let member = if let Some(ident) = field.ident.clone() {
syn::Member::Named(ident) syn::Member::Named(ident)
@ -170,10 +153,7 @@ impl Snippets {
span: field.span(), span: field.span(),
}) })
}; };
snippet.highlights.push(Highlight { snippet.highlights.push(Highlight { highlight: member });
label,
highlight: member,
});
} else { } else {
return Err(syn::Error::new(snippet.span(), "Highlight must refer to an existing field with a #[snippet(...)] attribute.")); return Err(syn::Error::new(snippet.span(), "Highlight must refer to an existing field with a #[snippet(...)] attribute."));
} }
@ -218,18 +198,6 @@ impl Snippets {
source: self.#src_ident.clone(), source: self.#src_ident.clone(),
}; };
// Source name
let src_name = match &snippet.source_name {
MemberOrString::String(str) => {
quote! {
source_name: #str.into(),
}
}
MemberOrString::Member(member) => quote! {
source_name: self.#member.clone(),
},
};
// Context // Context
let context = &snippet.snippet; let context = &snippet.snippet;
let context = quote! { let context = quote! {
@ -238,9 +206,9 @@ impl Snippets {
// Highlights // Highlights
let highlights = snippet.highlights.iter().map(|highlight| { let highlights = snippet.highlights.iter().map(|highlight| {
let Highlight { highlight, label } = highlight; let Highlight { highlight } = highlight;
quote! { quote! {
(#label.into(), self.#highlight.clone()) self.#highlight.clone()
} }
}); });
let highlights = quote! { let highlights = quote! {
@ -253,7 +221,6 @@ impl Snippets {
quote! { quote! {
miette::DiagnosticSnippet { miette::DiagnosticSnippet {
#msg #msg
#src_name
#src_ident #src_ident
#context #context
#highlights #highlights
@ -314,26 +281,6 @@ impl Snippets {
source: #src_ident.clone(), source: #src_ident.clone(),
}; };
// Source name
let src_name = match &snippet.source_name {
MemberOrString::String(str) => {
quote! {
source_name: #str.into(),
}
}
MemberOrString::Member(m) => {
let m = match m {
syn::Member::Named(id) => id.clone(),
syn::Member::Unnamed(syn::Index { index, .. }) => {
format_ident!("_{}", index)
}
};
quote! {
source_name: #m.clone(),
}
}
};
// Context // Context
let context = match &snippet.snippet { let context = match &snippet.snippet {
syn::Member::Named(id) => id.clone(), syn::Member::Named(id) => id.clone(),
@ -347,7 +294,7 @@ impl Snippets {
// Highlights // Highlights
let highlights = snippet.highlights.iter().map(|highlight| { let highlights = snippet.highlights.iter().map(|highlight| {
let Highlight { highlight, label } = highlight; let Highlight { highlight } = highlight;
let m = match highlight { let m = match highlight {
syn::Member::Named(id) => id.clone(), syn::Member::Named(id) => id.clone(),
syn::Member::Unnamed(syn::Index { index, .. }) => { syn::Member::Unnamed(syn::Index { index, .. }) => {
@ -355,7 +302,7 @@ impl Snippets {
} }
}; };
quote! { quote! {
(#label.into(), #m.clone()) #m.clone()
} }
}); });
let highlights = quote! { let highlights = quote! {
@ -368,7 +315,6 @@ impl Snippets {
quote! { quote! {
miette::DiagnosticSnippet { miette::DiagnosticSnippet {
#msg #msg
#src_name
#src_ident #src_ident
#context #context
#highlights #highlights

View File

@ -174,8 +174,6 @@ A snippet from a [Source] to be displayed with a message and possibly some highl
pub struct DiagnosticSnippet { pub struct DiagnosticSnippet {
/// Explanation of this specific diagnostic snippet. /// Explanation of this specific diagnostic snippet.
pub message: Option<String>, pub message: Option<String>,
/// The "filename" for this snippet.
pub source_name: String,
/// A [Source] that can be used to read the actual text of a source. /// A [Source] that can be used to read the actual text of a source.
pub source: Arc<dyn Source>, pub source: Arc<dyn Source>,
/// The primary [SourceSpan] where this diagnostic is located. /// The primary [SourceSpan] where this diagnostic is located.
@ -183,7 +181,7 @@ pub struct DiagnosticSnippet {
/// Additional [SourceSpan]s that mark specific sections of the span, for /// Additional [SourceSpan]s that mark specific sections of the span, for
/// example, to underline specific text within the larger span. They're /// example, to underline specific text within the larger span. They're
/// paired with labels that should be applied to those sections. /// paired with labels that should be applied to those sections.
pub highlights: Option<Vec<(String, SourceSpan)>>, pub highlights: Option<Vec<SourceSpan>>,
} }
/** /**
@ -191,35 +189,65 @@ Span within a [Source] with an associated message.
*/ */
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct SourceSpan { pub struct SourceSpan {
/// An optional label for this span. Rendered differently depending on
/// context.
label: Option<String>,
/// The start of the span. /// The start of the span.
pub start: SourceOffset, offset: SourceOffset,
/// The (exclusive) end of the span. /// The total length of the span. Think of this as an offset from `start`.
pub end: SourceOffset, length: SourceOffset,
} }
impl SourceSpan { impl SourceSpan {
pub fn new(start: SourceOffset, end: SourceOffset) -> Self { pub fn new(start: SourceOffset, length: SourceOffset) -> Self {
assert!( Self {
start.offset() <= end.offset(), label: None,
"Starting offset must come before the end offset." offset: start,
); length,
Self { start, end } }
}
pub fn new_labeled(label: impl AsRef<str>, start: SourceOffset, length: SourceOffset) -> Self {
Self {
label: Some(label.as_ref().into()),
offset: start,
length,
}
}
pub fn offset(&self) -> usize {
self.offset.offset()
}
pub fn label(&self) -> Option<&str> {
self.label.as_ref().map(|x| &x[..])
} }
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.end.offset() - self.start.offset() + 1 self.length.offset()
} }
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.start.offset() == self.end.offset() self.length.offset() == 0
} }
} }
impl From<(ByteOffset, ByteOffset)> for SourceSpan { impl From<(ByteOffset, ByteOffset)> for SourceSpan {
fn from((start, end): (ByteOffset, ByteOffset)) -> Self { fn from((start, len): (ByteOffset, ByteOffset)) -> Self {
Self { Self {
start: start.into(), label: None,
end: end.into(), offset: start.into(),
length: len.into(),
}
}
}
impl<T: AsRef<str>> From<(T, ByteOffset, ByteOffset)> for SourceSpan {
fn from((label, start, len): (T, ByteOffset, ByteOffset)) -> Self {
Self {
label: Some(label.as_ref().into()),
offset: start.into(),
length: len.into(),
} }
} }
} }

View File

@ -23,7 +23,9 @@ impl MietteReporter {
snippet: &DiagnosticSnippet, snippet: &DiagnosticSnippet,
) -> fmt::Result { ) -> fmt::Result {
use fmt::Write as _; use fmt::Write as _;
write!(f, "[{}]", snippet.source_name)?; if let Some(source_name) = snippet.context.label() {
write!(f, "[{}]", source_name)?;
}
if let Some(msg) = &snippet.message { if let Some(msg) = &snippet.message {
write!(f, " {}:", msg)?; write!(f, " {}:", msg)?;
} }
@ -36,7 +38,7 @@ impl MietteReporter {
let context = std::str::from_utf8(context_data.data()).expect("Bad utf8 detected"); let context = std::str::from_utf8(context_data.data()).expect("Bad utf8 detected");
let mut line = context_data.line(); let mut line = context_data.line();
let mut column = context_data.column(); let mut column = context_data.column();
let mut offset = snippet.context.start.offset(); let mut offset = snippet.context.offset();
let mut line_offset = offset; let mut line_offset = offset;
let mut iter = context.chars().peekable(); let mut iter = context.chars().peekable();
let mut line_str = String::new(); let mut line_str = String::new();
@ -71,20 +73,22 @@ impl MietteReporter {
writeln!(indented(f), "{: <2} | {}", line, line_str)?; writeln!(indented(f), "{: <2} | {}", line, line_str)?;
line_str.clear(); line_str.clear();
if let Some(highlights) = highlights { if let Some(highlights) = highlights {
for (label, span) in highlights { for span in highlights {
if span.start.offset() >= line_offset && span.end.offset() < offset { if span.offset() >= line_offset && (span.offset() + span.len()) < offset {
// Highlight only covers one line. // Highlight only covers one line.
write!(indented(f), "{: <2} | ", "")?; write!(indented(f), "{: <2} | ", "")?;
write!( write!(
f, f,
"{}{} ", "{}{} ",
" ".repeat(span.start.offset() - line_offset), " ".repeat(span.offset() - line_offset),
"^".repeat(span.len()) "^".repeat(span.len())
)?; )?;
writeln!(f, "{}", label)?; if let Some(label) = span.label() {
} else if span.start.offset() < offset writeln!(f, "{}", label)?;
&& span.start.offset() >= line_offset }
&& span.end.offset() >= offset } else if span.offset() < offset
&& span.offset() >= line_offset
&& (span.offset() + span.len()) >= offset
{ {
// Multiline highlight. // Multiline highlight.
todo!("Multiline highlights."); todo!("Multiline highlights.");

View File

@ -10,7 +10,7 @@ impl Source for String {
let mut start_column = 0usize; let mut start_column = 0usize;
let mut iter = self.chars().peekable(); let mut iter = self.chars().peekable();
while let Some(char) = iter.next() { while let Some(char) = iter.next() {
if offset < span.start.offset() { if offset < span.offset() {
match char { match char {
'\r' => { '\r' => {
if iter.next_if_eq(&'\n').is_some() { if iter.next_if_eq(&'\n').is_some() {
@ -29,9 +29,9 @@ impl Source for String {
} }
} }
if offset >= span.end.offset() { if offset >= span.offset() + span.len() - 1 {
return Ok(Box::new(MietteSpanContents::new( return Ok(Box::new(MietteSpanContents::new(
&self.as_bytes()[span.start.offset()..=span.end.offset()], &self.as_bytes()[span.offset()..span.offset() + span.len()],
start_line, start_line,
start_column, start_column,
))); )));
@ -50,10 +50,7 @@ mod tests {
#[test] #[test]
fn basic() -> Result<(), MietteError> { fn basic() -> Result<(), MietteError> {
let src = String::from("foo\n"); let src = String::from("foo\n");
let contents = src.read_span(&SourceSpan { let contents = src.read_span(&(0, 4).into())?;
start: 0.into(),
end: 3.into(),
})?;
assert_eq!("foo\n", std::str::from_utf8(contents.data()).unwrap()); assert_eq!("foo\n", std::str::from_utf8(contents.data()).unwrap());
Ok(()) Ok(())
} }
@ -61,10 +58,7 @@ mod tests {
#[test] #[test]
fn middle() -> Result<(), MietteError> { fn middle() -> Result<(), MietteError> {
let src = String::from("foo\nbar\nbaz\n"); let src = String::from("foo\nbar\nbaz\n");
let contents = src.read_span(&SourceSpan { let contents = src.read_span(&(4, 4).into())?;
start: 4.into(),
end: 7.into(),
})?;
assert_eq!("bar\n", std::str::from_utf8(contents.data()).unwrap()); assert_eq!("bar\n", std::str::from_utf8(contents.data()).unwrap());
Ok(()) Ok(())
} }
@ -72,10 +66,7 @@ mod tests {
#[test] #[test]
fn with_crlf() -> Result<(), MietteError> { fn with_crlf() -> Result<(), MietteError> {
let src = String::from("foo\r\nbar\r\nbaz\r\n"); let src = String::from("foo\r\nbar\r\nbaz\r\n");
let contents = src.read_span(&SourceSpan { let contents = src.read_span(&(5, 5).into())?;
start: 5.into(),
end: 9.into(),
})?;
assert_eq!("bar\r\n", std::str::from_utf8(contents.data()).unwrap()); assert_eq!("bar\r\n", std::str::from_utf8(contents.data()).unwrap());
Ok(()) Ok(())
} }

View File

@ -183,8 +183,8 @@ fn test_snippet_named_struct() {
// / [my_snippet]: hi this is where the thing went wrong. // / [my_snippet]: hi this is where the thing went wrong.
// 1 | hello // 1 | hello
// 2 | world // 2 | world
#[snippet(src, "my_snippet.rs", "hi this is where the thing went wrong")] #[snippet(src, "hi this is where the thing went wrong")]
snip: SourceSpan, snip: SourceSpan, // Defines filename using `label`
// "Highlights" are the specific highlights _inside_ the snippet. // "Highlights" are the specific highlights _inside_ the snippet.
// These will be used to underline/point to specific sections of the // These will be used to underline/point to specific sections of the
@ -196,9 +196,9 @@ fn test_snippet_named_struct() {
// | ^^^^ ^^^^ - var 2 // | ^^^^ ^^^^ - var 2
// | | // | |
// | var 1 // | var 1
#[highlight(snip, "var 1")] #[highlight(snip)]
var1: SourceSpan, var1: SourceSpan, // label from SourceSpan is used, if any.
#[highlight(snip, "var 2")] #[highlight(snip)]
var2: SourceSpan, var2: SourceSpan,
// Now with member source names // Now with member source names
@ -216,15 +216,14 @@ fn test_snippet_unnamed_struct() {
#[diagnostic(code(foo::bar::baz))] #[diagnostic(code(foo::bar::baz))]
struct Foo( struct Foo(
Arc<String>, Arc<String>,
#[snippet(0, "my_snippet.rs", "hi")] SourceSpan, #[snippet(0, "hi")] SourceSpan,
#[highlight(1, "var 1")] SourceSpan, #[highlight(1)] SourceSpan,
#[highlight(1, "var 2")] SourceSpan, #[highlight(1)] SourceSpan,
// referenced source name // referenced source name
String, String,
String, #[snippet(0, 4)] SourceSpan,
#[snippet(0, 4, 5)] SourceSpan, #[highlight(5)] SourceSpan,
#[highlight(6, "var 3")] SourceSpan, #[highlight(5)] SourceSpan,
#[highlight(6, "var 4")] SourceSpan,
); );
} }

View File

@ -1,8 +1,6 @@
use std::{fmt, sync::Arc}; use std::{fmt, sync::Arc};
use miette::{ use miette::{Diagnostic, DiagnosticReporter, DiagnosticSnippet, MietteError, MietteReporter};
Diagnostic, DiagnosticReporter, DiagnosticSnippet, MietteError, MietteReporter, SourceSpan,
};
use thiserror::Error; use thiserror::Error;
#[derive(Error)] #[derive(Error)]
@ -51,19 +49,9 @@ fn fancy() -> Result<(), MietteError> {
let err = MyBad { let err = MyBad {
snippets: vec![DiagnosticSnippet { snippets: vec![DiagnosticSnippet {
message: Some("This is the part that broke".into()), message: Some("This is the part that broke".into()),
source_name: "bad_file.rs".into(),
source: Arc::new(src), source: Arc::new(src),
highlights: Some(vec![( highlights: Some(vec![("this bit here", 9, 4).into()]),
"this bit here".into(), context: ("bad_file.rs", 0, len).into(),
SourceSpan {
start: 9.into(),
end: 12.into(),
},
)]),
context: SourceSpan {
start: 0.into(),
end: (len - 1).into(),
},
}], }],
}; };
let out = format!("{:?}", err); let out = format!("{:?}", err);