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:
Evan AlmloffandJonathan Kelley authored and GitHub committed 2024-05-22 07:24:50 -05:00
1 parent 245003a5d4
commit 2523581cf4
21 files changed
+788 -217

No files matched your search

+198 -63
View File
@@ -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 }),
}
}