Fix rsx autocomplete and diagnostics in the root; provide better completions after attributes are finished (#2656)
* fix rsx autocomplete and diagnostics in the root; provide better completions after attributes are finished * clean up pr to use internal child parsing * Fix peek_lowercase name * fix comma diagnostics in the root * Fix parsing components with a path * remove simple routes trailing comma * Fix incomplete_root_elements test * Remove trailing commas from root body node in tests --------- Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
1 parent
71a48686dc
commit
fa4e5dbf62
8 files changed
+179
-79
No files matched your search
@@ -11,7 +11,7 @@ use proc_macro2::Span;
|
||||
use proc_macro2_diagnostics::SpanDiagnosticExt;
|
||||
use syn::{
|
||||
ext::IdentExt,
|
||||
parse::{Parse, ParseBuffer},
|
||||
parse::{Parse, ParseBuffer, ParseStream},
|
||||
spanned::Spanned,
|
||||
token::{self, Brace},
|
||||
Expr, Ident, LitStr, Token,
|
||||
@@ -31,7 +31,7 @@ use syn::{
|
||||
/// The name of the block is expected to be parsed by the parent parser. It will accept items out of
|
||||
/// order if possible and then bubble up diagnostics to the parent. This lets us give better errors
|
||||
/// and autocomplete
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Default)]
|
||||
pub struct RsxBlock {
|
||||
pub brace: token::Brace,
|
||||
pub attributes: Vec<Attribute>,
|
||||
@@ -49,16 +49,37 @@ impl Parse for RsxBlock {
|
||||
}
|
||||
|
||||
impl RsxBlock {
|
||||
/// Only parse the children of the block - all others will be rejected
|
||||
pub fn parse_children(content: &ParseBuffer) -> syn::Result<Self> {
|
||||
let mut nodes = vec![];
|
||||
let mut diagnostics = Diagnostics::new();
|
||||
while !content.is_empty() {
|
||||
nodes.push(Self::parse_body_node_with_comma_diagnostics(
|
||||
content,
|
||||
&mut diagnostics,
|
||||
)?);
|
||||
}
|
||||
Ok(Self {
|
||||
children: nodes,
|
||||
diagnostics,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_inner(content: &ParseBuffer, brace: token::Brace) -> syn::Result<Self> {
|
||||
let mut items = vec![];
|
||||
let mut diagnostics = Diagnostics::new();
|
||||
|
||||
// If we are after attributes, we can try to provide better completions and diagnostics
|
||||
// by parsing the following nodes as body nodes if they are ambiguous, we can parse them as body nodes
|
||||
let mut after_attributes = false;
|
||||
|
||||
// Lots of manual parsing but it's important to do it all here to give the best diagnostics possible
|
||||
// We can do things like lookaheads, peeking, etc. to give better errors and autocomplete
|
||||
// We allow parsing in any order but complain if its done out of order.
|
||||
// Autofmt will fortunately fix this for us in most cases
|
||||
//
|
||||
// Weo do this by parsing the unambiguous cases first and then do some clever lookahead to parse the rest
|
||||
// We do this by parsing the unambiguous cases first and then do some clever lookahead to parse the rest
|
||||
while !content.is_empty() {
|
||||
// Parse spread attributes
|
||||
if content.peek(Token![..]) {
|
||||
@@ -89,6 +110,7 @@ impl RsxBlock {
|
||||
);
|
||||
}
|
||||
items.push(RsxItem::Spread(attr));
|
||||
after_attributes = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
@@ -113,7 +135,7 @@ impl RsxBlock {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Eagerly match on children, generally
|
||||
// Eagerly match on completed children, generally
|
||||
if content.peek(LitStr)
|
||||
| content.peek(Token![for])
|
||||
| content.peek(Token![if])
|
||||
@@ -122,10 +144,13 @@ impl RsxBlock {
|
||||
// web components
|
||||
| (content.peek(Ident::peek_any) && content.peek2(Token![-]))
|
||||
// elements
|
||||
| (content.peek(Ident::peek_any) && content.peek2(token::Brace))
|
||||
// todo: eager parse components?
|
||||
| (content.peek(Ident::peek_any) && (after_attributes || content.peek2(token::Brace)))
|
||||
// components
|
||||
| (content.peek(Ident::peek_any) && (after_attributes || content.peek2(token::Brace) || content.peek2(Token![::])))
|
||||
{
|
||||
items.push(RsxItem::Child(content.parse::<BodyNode>()?));
|
||||
items.push(RsxItem::Child(
|
||||
Self::parse_body_node_with_comma_diagnostics(content, &mut diagnostics)?,
|
||||
));
|
||||
if !content.is_empty() && content.peek(Token![,]) {
|
||||
let comma = content.parse::<Token![,]>()?;
|
||||
diagnostics.push(
|
||||
@@ -134,30 +159,20 @@ impl RsxBlock {
|
||||
),
|
||||
);
|
||||
}
|
||||
after_attributes = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse shorthand attributes
|
||||
// todo: this might cause complications with partial expansion... think more about the cases
|
||||
// where we can imagine expansion and what better diagnostics we can provide
|
||||
let looks_like_attribute = match content.fork().parse::<Ident>() {
|
||||
// If it is an ident, it is only a shorthand attribute if it starts with a lowercase letter
|
||||
// Otherwise this is the start of a component
|
||||
Ok(ident) => ident
|
||||
.to_string()
|
||||
.chars()
|
||||
.next()
|
||||
.unwrap()
|
||||
.is_ascii_lowercase(),
|
||||
Err(_) => false,
|
||||
};
|
||||
if looks_like_attribute
|
||||
&& !content.peek2(Brace)
|
||||
&& !content.peek2(Token![:]) // regular attributes / components with generics
|
||||
&& !content.peek2(Token![-]) // web components
|
||||
&& !content.peek2(Token![<]) // generics on components
|
||||
// generics on components
|
||||
&& !content.peek2(Token![::])
|
||||
if Self::peek_lowercase_ident(&content)
|
||||
&& !content.peek2(Brace)
|
||||
&& !content.peek2(Token![:]) // regular attributes / components with generics
|
||||
&& !content.peek2(Token![-]) // web components
|
||||
&& !content.peek2(Token![<]) // generics on components
|
||||
// generics on components
|
||||
&& !content.peek2(Token![::])
|
||||
{
|
||||
let attribute = content.parse::<Attribute>()?;
|
||||
|
||||
@@ -176,7 +191,9 @@ impl RsxBlock {
|
||||
}
|
||||
|
||||
// Finally just attempt a bodynode parse
|
||||
items.push(RsxItem::Child(content.parse::<BodyNode>()?))
|
||||
items.push(RsxItem::Child(
|
||||
Self::parse_body_node_with_comma_diagnostics(content, &mut diagnostics)?,
|
||||
))
|
||||
}
|
||||
|
||||
// Validate the order of the items
|
||||
@@ -204,6 +221,36 @@ impl RsxBlock {
|
||||
})
|
||||
}
|
||||
|
||||
// Parse a body node with diagnostics for unnecessary trailing commas
|
||||
fn parse_body_node_with_comma_diagnostics(
|
||||
content: &ParseBuffer,
|
||||
diagnostics: &mut Diagnostics,
|
||||
) -> syn::Result<BodyNode> {
|
||||
let body_node = content.parse::<BodyNode>()?;
|
||||
if !content.is_empty() && content.peek(Token![,]) {
|
||||
let comma = content.parse::<Token![,]>()?;
|
||||
diagnostics.push(
|
||||
comma
|
||||
.span()
|
||||
.warning("Elements and text nodes do not need to be separated by commas."),
|
||||
);
|
||||
}
|
||||
Ok(body_node)
|
||||
}
|
||||
|
||||
fn peek_lowercase_ident(stream: &ParseStream) -> bool {
|
||||
let Ok(ident) = stream.fork().parse::<Ident>() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
ident
|
||||
.to_string()
|
||||
.chars()
|
||||
.next()
|
||||
.unwrap()
|
||||
.is_ascii_lowercase()
|
||||
}
|
||||
|
||||
/// Ensure the ordering of the items is correct
|
||||
/// - Attributes must come before children
|
||||
/// - Spreads must come before children
|
||||
@@ -551,4 +598,27 @@ mod tests {
|
||||
|
||||
let _parsed: RsxBlock = syn::parse2(input).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_root_elements() {
|
||||
use syn::parse::Parser;
|
||||
|
||||
let input = quote::quote! {
|
||||
di
|
||||
};
|
||||
|
||||
let parsed = RsxBlock::parse_children.parse2(input).unwrap();
|
||||
let children = parsed.children;
|
||||
|
||||
assert_eq!(children.len(), 1);
|
||||
if let BodyNode::Element(parsed) = &children[0] {
|
||||
assert_eq!(
|
||||
parsed.name,
|
||||
ElementName::Ident(Ident::new("di", Span::call_site()))
|
||||
);
|
||||
} else {
|
||||
panic!("expected element, got {:?}", children);
|
||||
}
|
||||
assert!(parsed.diagnostics.is_empty());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user