Autocomplete rsx (#2421)
* autocomplete rsx * hide the completions module * fix autocomplete for identifiers that start with a raw identifier * add field completion hints for props * remove extra import * Revert "add field completion hints for props" This reverts commit 194899c73783e0f9b5d601d53365746d838f2048. * fix autocomplete inside raw expressions * fix autofmt * feat: just use regular ifmt if it's compatible reuses the tokens for ifmt so RA provides its HIR lowering on simple formatted strings * fix merging ifmt strings * add a better error message for components called like functions --------- Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
1 parent
245003a5d4
commit
2523581cf4
21 files changed
+788
-217
No files matched your search
+198
-63
@@ -1,25 +1,55 @@
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use crate::errors::missing_trailing_comma;
|
||||
|
||||
use self::util::try_parse_braces;
|
||||
|
||||
use super::*;
|
||||
|
||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||
use quote::quote;
|
||||
use syn::{
|
||||
parse::ParseBuffer, punctuated::Punctuated, spanned::Spanned, token::Brace, Expr, Ident,
|
||||
LitStr, Token,
|
||||
ext::IdentExt, punctuated::Punctuated, spanned::Spanned, token::Brace, Expr, Ident, LitStr,
|
||||
Token,
|
||||
};
|
||||
|
||||
// =======================================
|
||||
// Parse the VNode::Element type
|
||||
// =======================================
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Element {
|
||||
pub name: ElementName,
|
||||
pub key: Option<IfmtInput>,
|
||||
pub attributes: Vec<AttributeType>,
|
||||
pub merged_attributes: Vec<AttributeType>,
|
||||
pub children: Vec<BodyNode>,
|
||||
pub brace: syn::token::Brace,
|
||||
pub brace: Option<syn::token::Brace>,
|
||||
// Non-fatal errors that occurred during parsing
|
||||
errors: Vec<syn::Error>,
|
||||
}
|
||||
|
||||
impl PartialEq for Element {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name == other.name
|
||||
&& self.key == other.key
|
||||
&& self.attributes == other.attributes
|
||||
&& self.merged_attributes == other.merged_attributes
|
||||
&& self.children == other.children
|
||||
&& self.brace == other.brace
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Element {}
|
||||
|
||||
impl Hash for Element {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
self.key.hash(state);
|
||||
self.attributes.hash(state);
|
||||
self.merged_attributes.hash(state);
|
||||
self.children.hash(state);
|
||||
self.brace.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Element {
|
||||
@@ -58,22 +88,61 @@ impl Element {
|
||||
attributes,
|
||||
merged_attributes,
|
||||
children,
|
||||
brace,
|
||||
brace: Some(brace),
|
||||
errors: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for Element {
|
||||
fn parse(stream: ParseStream) -> Result<Self> {
|
||||
/// Create a new incomplete element that has not been fully typed yet
|
||||
fn incomplete(name: ElementName) -> Self {
|
||||
Self {
|
||||
errors: vec![syn::Error::new(
|
||||
name.span(),
|
||||
format!("Missing braces after element name `{}`", name),
|
||||
)],
|
||||
name,
|
||||
key: None,
|
||||
attributes: Vec::new(),
|
||||
merged_attributes: Vec::new(),
|
||||
children: Vec::new(),
|
||||
brace: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_with_options(
|
||||
stream: ParseStream,
|
||||
partial_completions: bool,
|
||||
) -> Result<Self> {
|
||||
fn peek_any_ident(input: ParseStream) -> bool {
|
||||
input.peek(Ident::peek_any)
|
||||
&& !input.peek(Token![for])
|
||||
&& !input.peek(Token![if])
|
||||
&& !input.peek(Token![match])
|
||||
}
|
||||
|
||||
let el_name = ElementName::parse(stream)?;
|
||||
|
||||
// parse the guts
|
||||
let content: ParseBuffer;
|
||||
let brace = syn::braced!(content in stream);
|
||||
let Ok((brace, content)) = try_parse_braces(stream) else {
|
||||
// If there are no braces, this is an incomplete element. We still parse it so that we can autocomplete it, but we don't need to parse the children
|
||||
return Ok(Self::incomplete(el_name));
|
||||
};
|
||||
|
||||
let mut attributes: Vec<AttributeType> = vec![];
|
||||
let mut children: Vec<BodyNode> = vec![];
|
||||
let mut key = None;
|
||||
let mut errors = Vec::new();
|
||||
|
||||
macro_rules! accumulate_or_return_error {
|
||||
($error:expr) => {
|
||||
let error = $error;
|
||||
if partial_completions {
|
||||
errors.push(error);
|
||||
} else {
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// parse fields with commas
|
||||
// break when we don't get this pattern anymore
|
||||
@@ -92,7 +161,7 @@ impl Parse for Element {
|
||||
}
|
||||
|
||||
if content.parse::<Token![,]>().is_err() {
|
||||
missing_trailing_comma!(span);
|
||||
accumulate_or_return_error!(missing_trailing_comma(span));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -106,28 +175,30 @@ impl Parse for Element {
|
||||
content.parse::<Token![:]>()?;
|
||||
|
||||
let value = content.parse::<ElementAttrValue>()?;
|
||||
let followed_by_comma = content.parse::<Token![,]>().is_ok();
|
||||
attributes.push(attribute::AttributeType::Named(ElementAttrNamed {
|
||||
el_name: el_name.clone(),
|
||||
attr: ElementAttr {
|
||||
name: ElementAttrName::Custom(name),
|
||||
value,
|
||||
},
|
||||
followed_by_comma,
|
||||
}));
|
||||
|
||||
if content.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
if content.parse::<Token![,]>().is_err() {
|
||||
missing_trailing_comma!(ident.span());
|
||||
if !followed_by_comma {
|
||||
accumulate_or_return_error!(missing_trailing_comma(ident.span()));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse
|
||||
// abc: 123,
|
||||
if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
|
||||
let name = content.parse::<Ident>()?;
|
||||
if peek_any_ident(&content) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
|
||||
let name = Ident::parse_any(&content)?;
|
||||
|
||||
let name_str = name.to_string();
|
||||
content.parse::<Token![:]>()?;
|
||||
@@ -136,36 +207,7 @@ impl Parse for Element {
|
||||
// for example the `hi` part of `class: "hi"`.
|
||||
let span = content.span();
|
||||
|
||||
if name_str.starts_with("on") {
|
||||
// check for any duplicate event listeners
|
||||
if attributes.iter().any(|f| {
|
||||
if let AttributeType::Named(ElementAttrNamed {
|
||||
attr:
|
||||
ElementAttr {
|
||||
name: ElementAttrName::BuiltIn(n),
|
||||
value: ElementAttrValue::EventTokens(_),
|
||||
},
|
||||
..
|
||||
}) = f
|
||||
{
|
||||
n == &name_str
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}) {
|
||||
return Err(syn::Error::new(
|
||||
name.span(),
|
||||
format!("Duplicate event listener `{}`", name),
|
||||
));
|
||||
}
|
||||
attributes.push(attribute::AttributeType::Named(ElementAttrNamed {
|
||||
el_name: el_name.clone(),
|
||||
attr: ElementAttr {
|
||||
name: ElementAttrName::BuiltIn(name),
|
||||
value: ElementAttrValue::EventTokens(content.parse()?),
|
||||
},
|
||||
}));
|
||||
} else if name_str == "key" {
|
||||
if name_str == "key" {
|
||||
let _key: IfmtInput = content.parse()?;
|
||||
|
||||
if _key.is_static() {
|
||||
@@ -174,13 +216,39 @@ impl Parse for Element {
|
||||
|
||||
key = Some(_key);
|
||||
} else {
|
||||
let value = content.parse::<ElementAttrValue>()?;
|
||||
let value = if name_str.starts_with("on") {
|
||||
// check for any duplicate event listeners
|
||||
if attributes.iter().any(|f| {
|
||||
if let AttributeType::Named(ElementAttrNamed {
|
||||
attr:
|
||||
ElementAttr {
|
||||
name: ElementAttrName::BuiltIn(n),
|
||||
value: ElementAttrValue::EventTokens(_),
|
||||
},
|
||||
..
|
||||
}) = f
|
||||
{
|
||||
n == &name_str
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}) {
|
||||
return Err(syn::Error::new(
|
||||
name.span(),
|
||||
format!("Duplicate event listener `{}`", name),
|
||||
));
|
||||
}
|
||||
ElementAttrValue::EventTokens(content.parse()?)
|
||||
} else {
|
||||
content.parse::<ElementAttrValue>()?
|
||||
};
|
||||
attributes.push(attribute::AttributeType::Named(ElementAttrNamed {
|
||||
el_name: el_name.clone(),
|
||||
attr: ElementAttr {
|
||||
name: ElementAttrName::BuiltIn(name),
|
||||
name: ElementAttrName::built_in(&name),
|
||||
value,
|
||||
},
|
||||
followed_by_comma: content.peek(Token![,]),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -189,18 +257,18 @@ impl Parse for Element {
|
||||
}
|
||||
|
||||
if content.parse::<Token![,]>().is_err() {
|
||||
missing_trailing_comma!(span);
|
||||
accumulate_or_return_error!(missing_trailing_comma(span));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse shorthand fields
|
||||
if content.peek(Ident)
|
||||
if peek_any_ident(&content)
|
||||
&& !content.peek2(Brace)
|
||||
&& !content.peek2(Token![:])
|
||||
&& !content.peek2(Token![-])
|
||||
{
|
||||
let name = content.parse::<Ident>()?;
|
||||
let name = Ident::parse_any(&content)?;
|
||||
let name_ = name.clone();
|
||||
|
||||
// If the shorthand field is children, these are actually children!
|
||||
@@ -216,21 +284,37 @@ Like so:
|
||||
));
|
||||
};
|
||||
|
||||
let value = ElementAttrValue::Shorthand(name.clone());
|
||||
let followed_by_comma = content.parse::<Token![,]>().is_ok();
|
||||
|
||||
// If the shorthand field starts with a capital letter and it isn't followed by a comma, it's actually the start of typing a component
|
||||
let starts_with_capital = match name.to_string().chars().next() {
|
||||
Some(c) => c.is_uppercase(),
|
||||
None => false,
|
||||
};
|
||||
|
||||
if starts_with_capital && !followed_by_comma {
|
||||
children.push(BodyNode::Component(Component::incomplete(name.into())));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, it is really a shorthand field
|
||||
let value = ElementAttrValue::shorthand(&name);
|
||||
|
||||
attributes.push(attribute::AttributeType::Named(ElementAttrNamed {
|
||||
el_name: el_name.clone(),
|
||||
attr: ElementAttr {
|
||||
name: ElementAttrName::BuiltIn(name),
|
||||
name: ElementAttrName::built_in(&name),
|
||||
value,
|
||||
},
|
||||
followed_by_comma,
|
||||
}));
|
||||
|
||||
if content.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
if content.parse::<Token![,]>().is_err() {
|
||||
missing_trailing_comma!(name_.span());
|
||||
if !followed_by_comma {
|
||||
accumulate_or_return_error!(missing_trailing_comma(name_.span()));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -239,15 +323,13 @@ Like so:
|
||||
}
|
||||
|
||||
while !content.is_empty() {
|
||||
if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) {
|
||||
if ((content.peek(Ident) || content.peek(LitStr)) && content.peek2(Token![:]))
|
||||
&& !content.peek3(Token![:])
|
||||
{
|
||||
attr_after_element!(content.span());
|
||||
}
|
||||
|
||||
if (content.peek(Ident) && content.peek2(Token![:])) && !content.peek3(Token![:]) {
|
||||
attr_after_element!(content.span());
|
||||
}
|
||||
|
||||
children.push(content.parse::<BodyNode>()?);
|
||||
children.push(BodyNode::parse_with_options(&content, partial_completions)?);
|
||||
// consume comma if it exists
|
||||
// we don't actually care if there *are* commas after elements/text
|
||||
if content.peek(Token![,]) {
|
||||
@@ -255,7 +337,53 @@ Like so:
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self::new(key, el_name, attributes, children, brace))
|
||||
let mut myself = Self::new(key, el_name, attributes, children, brace);
|
||||
|
||||
myself.errors = errors;
|
||||
|
||||
Ok(myself)
|
||||
}
|
||||
|
||||
/// If this element doesn't include braces, the user is probably still typing the element name.
|
||||
/// We can add hints for rust analyzer to complete the element name better.
|
||||
pub(crate) fn completion_hints(&self) -> TokenStream2 {
|
||||
let Element { name, brace, .. } = self;
|
||||
|
||||
// If there are braces, this is a complete element and we don't need to add any hints
|
||||
if brace.is_some() {
|
||||
return quote! {};
|
||||
}
|
||||
|
||||
// Only complete the element name if it's a built in element
|
||||
let ElementName::Ident(name) = name else {
|
||||
return quote! {};
|
||||
};
|
||||
|
||||
quote! {
|
||||
#[allow(dead_code)]
|
||||
{
|
||||
// Autocomplete as an element
|
||||
dioxus_elements::elements::completions::CompleteWithBraces::#name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If this element is only partially complete, return the errors that occurred during parsing
|
||||
pub(crate) fn errors(&self) -> TokenStream2 {
|
||||
let Element { errors, .. } = self;
|
||||
|
||||
let mut tokens = quote! {};
|
||||
for error in errors {
|
||||
tokens.append_all(error.to_compile_error());
|
||||
}
|
||||
|
||||
tokens
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for Element {
|
||||
fn parse(stream: ParseStream) -> Result<Self> {
|
||||
Self::parse_with_options(stream, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,10 +396,17 @@ pub enum ElementName {
|
||||
impl ElementName {
|
||||
pub(crate) fn tag_name(&self) -> TokenStream2 {
|
||||
match self {
|
||||
ElementName::Ident(i) => quote! { dioxus_elements::#i::TAG_NAME },
|
||||
ElementName::Ident(i) => quote! { dioxus_elements::elements::#i::TAG_NAME },
|
||||
ElementName::Custom(s) => quote! { #s },
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn namespace(&self) -> TokenStream2 {
|
||||
match self {
|
||||
ElementName::Ident(i) => quote! { dioxus_elements::elements::#i::NAME_SPACE },
|
||||
ElementName::Custom(_) => quote! { None },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ElementName {
|
||||
@@ -322,7 +457,7 @@ impl Parse for ElementName {
|
||||
impl ToTokens for ElementName {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
match self {
|
||||
ElementName::Ident(i) => tokens.append_all(quote! { dioxus_elements::#i }),
|
||||
ElementName::Ident(i) => tokens.append_all(quote! { dioxus_elements::elements::#i }),
|
||||
ElementName::Custom(s) => tokens.append_all(quote! { #s }),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user