소스 검색

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>
Evan Almloff 1 년 전
부모
커밋
2523581cf4

+ 5 - 1
packages/autofmt/src/element.rs

@@ -52,6 +52,10 @@ impl Writer<'_> {
             ..
         } = el;
 
+        let brace = brace
+            .as_ref()
+            .expect("braces should always be present in strict mode");
+
         /*
             1. Write the tag
             2. Write the key
@@ -426,7 +430,7 @@ impl Writer<'_> {
     }
 }
 
-fn get_expr_length(expr: &Expr) -> Option<usize> {
+fn get_expr_length(expr: &impl Spanned) -> Option<usize> {
     let span = expr.span();
     let (start, end) = (span.start(), span.end());
     if start.line == end.line {

+ 4 - 4
packages/autofmt/src/lib.rs

@@ -9,7 +9,7 @@ use collect_macros::byte_offset;
 use dioxus_rsx::{BodyNode, CallBody, IfmtInput};
 use proc_macro2::LineColumn;
 use quote::ToTokens;
-use syn::{ExprMacro, MacroDelimiter};
+use syn::{parse::Parser, ExprMacro, MacroDelimiter};
 
 mod buffer;
 mod collect_macros;
@@ -77,7 +77,7 @@ pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
             continue;
         }
 
-        let body = item.parse_body::<CallBody>().unwrap();
+        let body = item.parse_body_with(CallBody::parse_strict).unwrap();
 
         let rsx_start = macro_path.span().start();
 
@@ -153,7 +153,7 @@ fn write_body(buf: &mut Writer, body: &CallBody) {
 }
 
 pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option<String> {
-    let body = syn::parse2::<CallBody>(expr.mac.tokens).unwrap();
+    let body = CallBody::parse_strict.parse2(expr.mac.tokens).unwrap();
 
     let mut buf = Writer::new(raw);
 
@@ -163,7 +163,7 @@ pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option<String> {
 }
 
 pub fn fmt_block(block: &str, indent_level: usize, indent: IndentOptions) -> Option<String> {
-    let body = syn::parse_str::<dioxus_rsx::CallBody>(block).unwrap();
+    let body = CallBody::parse_strict.parse_str(block).unwrap();
 
     let mut buf = Writer::new(block);
 

+ 29 - 0
packages/core-macro/src/component.rs

@@ -51,11 +51,15 @@ impl ToTokens for ComponentBody {
             }
         };
 
+        let completion_hints = self.completion_hints();
+
         tokens.append_all(quote! {
             #props_struct
 
             #[allow(non_snake_case)]
             #comp_fn
+
+            #completion_hints
         });
     }
 }
@@ -221,6 +225,31 @@ impl ComponentBody {
 
         false
     }
+
+    // We generate an extra enum to help us autocomplete the braces after the component.
+    // This is a bit of a hack, but it's the only way to get the braces to autocomplete.
+    fn completion_hints(&self) -> TokenStream {
+        let comp_fn = &self.item_fn.sig.ident;
+        let completions_mod = Ident::new(&format!("{}_completions", comp_fn), comp_fn.span());
+
+        let vis = &self.item_fn.vis;
+
+        quote! {
+            #[allow(non_snake_case)]
+            #[doc(hidden)]
+            mod #completions_mod {
+                #[doc(hidden)]
+                #[allow(non_camel_case_types)]
+                /// This enum is generated to help autocomplete the braces after the component. It does nothing
+                pub enum Component {
+                    #comp_fn {}
+                }
+            }
+
+            #[allow(unused)]
+            #vis use #completions_mod::Component::#comp_fn;
+        }
+    }
 }
 
 struct DocField<'a> {

+ 1 - 1
packages/dioxus-lib/src/lib.rs

@@ -42,7 +42,7 @@ pub mod prelude {
     pub use dioxus_html as dioxus_elements;
 
     #[cfg(feature = "html")]
-    pub use dioxus_elements::{prelude::*, GlobalAttributes, SvgAttributes};
+    pub use dioxus_elements::{global_attributes, prelude::*, svg_attributes};
 
     pub use dioxus_core;
 }

+ 1 - 1
packages/dioxus/src/lib.rs

@@ -65,7 +65,7 @@ pub mod prelude {
 
     #[cfg(feature = "html")]
     #[cfg_attr(docsrs, doc(cfg(feature = "html")))]
-    pub use dioxus_elements::{prelude::*, GlobalAttributes, SvgAttributes};
+    pub use dioxus_elements::{global_attributes, prelude::*, svg_attributes};
 
     #[cfg(all(
         not(any(target_arch = "wasm32", target_os = "ios", target_os = "android")),

+ 2 - 20
packages/html-internal-macro/src/lib.rs

@@ -14,7 +14,6 @@ pub fn impl_extension_attributes(input: TokenStream) -> TokenStream {
 }
 
 struct ImplExtensionAttributes {
-    is_element: bool,
     name: Ident,
     attrs: Punctuated<Ident, Token![,]>,
 }
@@ -23,16 +22,11 @@ impl Parse for ImplExtensionAttributes {
     fn parse(input: ParseStream) -> syn::Result<Self> {
         let content;
 
-        let element: Ident = input.parse()?;
         let name = input.parse()?;
         braced!(content in input);
         let attrs = content.parse_terminated(Ident::parse, Token![,])?;
 
-        Ok(ImplExtensionAttributes {
-            is_element: element == "ELEMENT",
-            name,
-            attrs,
-        })
+        Ok(ImplExtensionAttributes { name, attrs })
     }
 }
 
@@ -44,22 +38,10 @@ impl ToTokens for ImplExtensionAttributes {
             .strip_prefix("r#")
             .unwrap_or(&name_string)
             .to_case(Case::UpperCamel);
-        let impl_name = Ident::new(format!("{}Impl", &camel_name).as_str(), name.span());
         let extension_name = Ident::new(format!("{}Extension", &camel_name).as_str(), name.span());
 
-        if !self.is_element {
-            tokens.append_all(quote! {
-                struct #impl_name;
-                impl #name for #impl_name {}
-            });
-        }
-
         let impls = self.attrs.iter().map(|ident| {
-            let d = if self.is_element {
-                quote! { #name::#ident }
-            } else {
-                quote! { <#impl_name as #name>::#ident }
-            };
+            let d = quote! { #name::#ident };
             quote! {
                 fn #ident(self, value: impl IntoAttributeValue) -> Self {
                     let d = #d;

+ 14 - 13
packages/html/src/global_attributes.rs → packages/html/src/attribute_groups.rs

@@ -7,7 +7,7 @@ use dioxus_html_internal_macro::impl_extension_attributes;
 use crate::AttributeDiscription;
 
 #[cfg(feature = "hot-reload-context")]
-macro_rules! trait_method_mapping {
+macro_rules! mod_method_mapping {
     (
         $matching:ident;
         $(#[$attr:meta])*
@@ -68,11 +68,11 @@ macro_rules! html_to_rsx_attribute_mapping {
     };
 }
 
-macro_rules! trait_methods {
+macro_rules! mod_methods {
     (
         @base
-        $(#[$trait_attr:meta])*
-        $trait:ident;
+        $(#[$mod_attr:meta])*
+        $mod:ident;
         $fn:ident;
         $fn_html_to_rsx:ident;
         $(
@@ -80,18 +80,19 @@ macro_rules! trait_methods {
             $name:ident $(: $($arg:literal),*)*;
         )+
     ) => {
-        $(#[$trait_attr])*
-        pub trait $trait {
+        $(#[$mod_attr])*
+        pub mod $mod {
+            use super::*;
             $(
                 $(#[$attr])*
-                const $name: AttributeDiscription = trait_methods! { $name $(: $($arg),*)*; };
+                pub const $name: AttributeDiscription = mod_methods! { $name $(: $($arg),*)*; };
             )*
         }
 
         #[cfg(feature = "hot-reload-context")]
         pub(crate) fn $fn(attr: &str) -> Option<(&'static str, Option<&'static str>)> {
             $(
-                trait_method_mapping! {
+                mod_method_mapping! {
                     attr;
                     $name$(: $($arg),*)*;
                 }
@@ -111,7 +112,7 @@ macro_rules! trait_methods {
             None
         }
 
-        impl_extension_attributes![GLOBAL $trait { $($name,)* }];
+        impl_extension_attributes![$mod { $($name,)* }];
     };
 
     // Rename the incoming ident and apply a custom namespace
@@ -124,10 +125,10 @@ macro_rules! trait_methods {
     ( $name:ident; ) => { (stringify!($name), None, false) };
 }
 
-trait_methods! {
+mod_methods! {
     @base
 
-    GlobalAttributes;
+    global_attributes;
     map_global_attributes;
     map_html_global_attributes_to_rsx;
 
@@ -1640,9 +1641,9 @@ trait_methods! {
     aria_setsize: "aria-setsize";
 }
 
-trait_methods! {
+mod_methods! {
     @base
-    SvgAttributes;
+    svg_attributes;
     map_svg_attributes;
     map_html_svg_attributes_to_rsx;
 

+ 26 - 15
packages/html/src/elements.rs

@@ -8,7 +8,6 @@ use dioxus_rsx::HotReloadingContext;
 
 #[cfg(feature = "hot-reload-context")]
 use crate::{map_global_attributes, map_svg_attributes};
-use crate::{GlobalAttributes, SvgAttributes};
 
 pub type AttributeDiscription = (&'static str, Option<&'static str>, bool);
 
@@ -115,9 +114,11 @@ macro_rules! impl_element {
     ) => {
         #[allow(non_camel_case_types)]
         $(#[$attr])*
-        pub struct $name;
+        pub mod $name {
+            #[allow(unused)]
+            use super::*;
+            pub use crate::attribute_groups::global_attributes::*;
 
-        impl $name {
             pub const TAG_NAME: &'static str = stringify!($name);
             pub const NAME_SPACE: Option<&'static str> = None;
 
@@ -128,8 +129,6 @@ macro_rules! impl_element {
                 );
             )*
         }
-
-        impl GlobalAttributes for $name {}
     };
 
     (
@@ -141,13 +140,12 @@ macro_rules! impl_element {
             )*
         }
     ) => {
-        #[allow(non_camel_case_types)]
         $(#[$attr])*
-        pub struct $name;
-
-        impl SvgAttributes for $name {}
+        pub mod $name {
+            #[allow(unused)]
+            use super::*;
+            pub use crate::attribute_groups::svg_attributes::*;
 
-        impl $name {
             pub const TAG_NAME: &'static str = stringify!($name);
             pub const NAME_SPACE: Option<&'static str> = Some($namespace);
 
@@ -171,11 +169,11 @@ macro_rules! impl_element {
     ) => {
         #[allow(non_camel_case_types)]
         $(#[$attr])*
-        pub struct $element;
-
-        impl SvgAttributes for $element {}
+        pub mod $element {
+            #[allow(unused)]
+            use super::*;
+            pub use crate::attribute_groups::svg_attributes::*;
 
-        impl $element {
             pub const TAG_NAME: &'static str = $name;
             pub const NAME_SPACE: Option<&'static str> = Some($namespace);
 
@@ -384,10 +382,23 @@ macro_rules! builder_constructors {
             );
         )*
 
+        /// This module contains helpers for rust analyzer autocompletion
+        #[doc(hidden)]
+        pub mod completions {
+            /// This helper tells rust analyzer that it should autocomplete the element name with braces.
+            #[allow(non_camel_case_types)]
+            pub enum CompleteWithBraces {
+                $(
+                    $(#[$attr])*
+                    $name {}
+                ),*
+            }
+        }
+
         pub(crate) mod extensions {
             use super::*;
             $(
-                impl_extension_attributes![ELEMENT $name { $($fil,)* }];
+                impl_extension_attributes![$name { $($fil,)* }];
             )*
         }
     };

+ 5 - 5
packages/html/src/lib.rs

@@ -16,7 +16,7 @@
 //!
 //! Currently, we don't validate for structures, but do validate attributes.
 
-mod elements;
+pub mod elements;
 #[cfg(feature = "hot-reload-context")]
 pub use elements::HtmlCtx;
 #[cfg(feature = "html-to-rsx")]
@@ -24,8 +24,8 @@ pub use elements::{map_html_attribute_to_rsx, map_html_element_to_rsx};
 pub mod events;
 pub(crate) mod file_data;
 pub use file_data::*;
+mod attribute_groups;
 pub mod geometry;
-mod global_attributes;
 pub mod input_data;
 #[cfg(feature = "native-bind")]
 pub mod native_bind;
@@ -40,25 +40,25 @@ mod transit;
 #[cfg(feature = "serialize")]
 pub use transit::*;
 
+pub use attribute_groups::*;
 pub use elements::*;
 pub use events::*;
-pub use global_attributes::*;
 pub use render_template::*;
 
 #[cfg(feature = "eval")]
 pub mod eval;
 
 pub mod extensions {
+    pub use crate::attribute_groups::{GlobalAttributesExtension, SvgAttributesExtension};
     pub use crate::elements::extensions::*;
-    pub use crate::global_attributes::{GlobalAttributesExtension, SvgAttributesExtension};
 }
 
 pub mod prelude {
+    pub use crate::attribute_groups::{GlobalAttributesExtension, SvgAttributesExtension};
     pub use crate::elements::extensions::*;
     #[cfg(feature = "eval")]
     pub use crate::eval::*;
     pub use crate::events::*;
-    pub use crate::global_attributes::{GlobalAttributesExtension, SvgAttributesExtension};
     pub use crate::point_interaction::*;
     pub use keyboard_types::{self, Code, Key, Location, Modifiers};
 }

+ 24 - 29
packages/rsx-rosetta/src/lib.rs

@@ -64,50 +64,46 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
                         }
                     };
 
-                    AttributeType::Named(ElementAttrNamed {
-                        el_name: el_name.clone(),
-                        attr,
-                    })
+                    AttributeType::Named(ElementAttrNamed::new(el_name.clone(), attr))
                 })
                 .collect();
 
             let class = el.classes.join(" ");
             if !class.is_empty() {
-                attributes.push(AttributeType::Named(ElementAttrNamed {
-                    el_name: el_name.clone(),
-                    attr: ElementAttr {
+                attributes.push(AttributeType::Named(ElementAttrNamed::new(
+                    el_name.clone(),
+                    ElementAttr {
                         name: dioxus_rsx::ElementAttrName::BuiltIn(Ident::new(
                             "class",
                             Span::call_site(),
                         )),
                         value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(&class)),
                     },
-                }));
+                )));
             }
 
             if let Some(id) = &el.id {
-                attributes.push(AttributeType::Named(ElementAttrNamed {
-                    el_name: el_name.clone(),
-                    attr: ElementAttr {
+                attributes.push(AttributeType::Named(ElementAttrNamed::new(
+                    el_name.clone(),
+                    ElementAttr {
                         name: dioxus_rsx::ElementAttrName::BuiltIn(Ident::new(
                             "id",
                             Span::call_site(),
                         )),
                         value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(id)),
                     },
-                }));
+                )));
             }
 
             let children = el.children.iter().filter_map(rsx_node_from_html).collect();
 
-            Some(BodyNode::Element(Element {
-                name: el_name,
-                children,
+            Some(BodyNode::Element(Element::new(
+                None,
+                el_name,
                 attributes,
-                merged_attributes: Default::default(),
-                key: None,
-                brace: Default::default(),
-            }))
+                children,
+                Default::default(),
+            )))
         }
 
         // We ignore comments
@@ -132,19 +128,18 @@ pub fn collect_svgs(children: &mut [BodyNode], out: &mut Vec<BodyNode>) {
                 segments.push(new_name.clone().into());
 
                 // Replace this instance with a component
-                let mut new_comp = BodyNode::Component(Component {
-                    name: syn::Path {
+                let mut new_comp = BodyNode::Component(Component::new(
+                    syn::Path {
                         leading_colon: None,
                         segments,
                     },
-                    prop_gen_args: None,
-                    fields: vec![],
-                    children: vec![],
-                    manual_props: None,
-                    key: None,
-                    brace: Default::default(),
-                    location: Default::default(),
-                });
+                    None,
+                    vec![],
+                    vec![],
+                    None,
+                    None,
+                    Default::default(),
+                ));
 
                 std::mem::swap(child, &mut new_comp);
 

+ 90 - 5
packages/rsx/src/attribute.rs

@@ -3,7 +3,7 @@ use std::fmt::{Display, Formatter};
 use super::*;
 
 use proc_macro2::{Span, TokenStream as TokenStream2};
-use quote::quote;
+use quote::{quote, quote_spanned};
 use syn::{parse_quote, spanned::Spanned, Expr, ExprIf, Ident, LitStr};
 
 #[derive(PartialEq, Eq, Clone, Debug, Hash)]
@@ -95,6 +95,9 @@ impl AttributeType {
 pub struct ElementAttrNamed {
     pub el_name: ElementName,
     pub attr: ElementAttr,
+    // If this is the last attribute of an element and it doesn't have a tailing comma,
+    // we add hints so that rust analyzer completes it either as an attribute or element
+    pub(crate) followed_by_comma: bool,
 }
 
 impl Hash for ElementAttrNamed {
@@ -112,6 +115,15 @@ impl PartialEq for ElementAttrNamed {
 impl Eq for ElementAttrNamed {}
 
 impl ElementAttrNamed {
+    /// Create a new ElementAttrNamed
+    pub fn new(el_name: ElementName, attr: ElementAttr) -> Self {
+        Self {
+            el_name,
+            attr,
+            followed_by_comma: true,
+        }
+    }
+
     pub(crate) fn try_combine(&self, other: &Self) -> Option<Self> {
         if self.el_name == other.el_name && self.attr.name == other.attr.name {
             if let Some(separator) = self.attr.name.multi_attribute_separator() {
@@ -121,16 +133,64 @@ impl ElementAttrNamed {
                         name: self.attr.name.clone(),
                         value: self.attr.value.combine(separator, &other.attr.value),
                     },
+                    followed_by_comma: self.followed_by_comma || other.followed_by_comma,
                 });
             }
         }
         None
     }
+
+    /// If this is the last attribute of an element and it doesn't have a tailing comma,
+    /// we add hints so that rust analyzer completes it either as an attribute or element
+    fn completion_hints(&self) -> TokenStream2 {
+        let ElementAttrNamed {
+            el_name,
+            attr,
+            followed_by_comma,
+        } = self;
+
+        // If there is a trailing comma, rust analyzer does a good job of completing the attribute by itself
+        if *followed_by_comma {
+            return quote! {};
+        }
+        // Only add hints if the attribute is:
+        // - a built in attribute (not a literal)
+        // - an build in element (not a custom element)
+        // - a shorthand attribute
+        let (
+            ElementName::Ident(el),
+            ElementAttrName::BuiltIn(name),
+            ElementAttrValue::Shorthand(_),
+        ) = (el_name, &attr.name, &attr.value)
+        else {
+            return quote! {};
+        };
+        // If the attribute is a shorthand attribute, but it is an event handler, rust analyzer already does a good job of completing the attribute by itself
+        if name.to_string().starts_with("on") {
+            return quote! {};
+        }
+
+        quote! {
+            {
+                #[allow(dead_code)]
+                #[doc(hidden)]
+                mod __completions {
+                    // Autocomplete as an attribute
+                    pub use super::dioxus_elements::#el::*;
+                    // Autocomplete as an element
+                    pub use super::dioxus_elements::elements::completions::CompleteWithBraces::*;
+                    fn ignore() {
+                        #name
+                    }
+                }
+            }
+        }
+    }
 }
 
 impl ToTokens for ElementAttrNamed {
     fn to_tokens(&self, tokens: &mut TokenStream2) {
-        let ElementAttrNamed { el_name, attr } = self;
+        let ElementAttrNamed { el_name, attr, .. } = self;
 
         let ns = |name: &ElementAttrName| match (el_name, name) {
             (ElementName::Ident(i), ElementAttrName::BuiltIn(_)) => {
@@ -186,19 +246,25 @@ impl ToTokens for ElementAttrNamed {
                 }
                 ElementAttrValue::EventTokens(tokens) => match &self.attr.name {
                     ElementAttrName::BuiltIn(name) => {
-                        quote! {
+                        quote_spanned! { tokens.span() =>
                             dioxus_elements::events::#name(#tokens)
                         }
                     }
                     ElementAttrName::Custom(_) => unreachable!("Handled elsewhere in the macro"),
                 },
                 _ => {
-                    quote! { dioxus_elements::events::#value(#value) }
+                    quote_spanned! { value.span() => dioxus_elements::events::#value(#value) }
                 }
             }
         };
 
-        tokens.append_all(attribute);
+        let completion_hints = self.completion_hints();
+        tokens.append_all(quote! {
+            {
+                #completion_hints
+                #attribute
+            }
+        });
     }
 }
 
@@ -289,6 +355,11 @@ impl ToTokens for ElementAttrValue {
 }
 
 impl ElementAttrValue {
+    /// Create a new ElementAttrValue::Shorthand from an Ident and normalize the identifier
+    pub(crate) fn shorthand(name: &Ident) -> Self {
+        Self::Shorthand(normalize_raw_ident(name))
+    }
+
     pub fn is_shorthand(&self) -> bool {
         matches!(self, ElementAttrValue::Shorthand(_))
     }
@@ -388,6 +459,16 @@ impl ElementAttrValue {
     }
 }
 
+// Create and normalize a built-in attribute name
+// If the identifier is a reserved keyword, this method will create a raw identifier
+fn normalize_raw_ident(ident: &Ident) -> Ident {
+    if syn::parse2::<syn::Ident>(ident.to_token_stream()).is_err() {
+        syn::Ident::new_raw(&ident.to_string(), ident.span())
+    } else {
+        ident.clone()
+    }
+}
+
 #[derive(PartialEq, Eq, Clone, Debug, Hash)]
 pub enum ElementAttrName {
     BuiltIn(Ident),
@@ -395,6 +476,10 @@ pub enum ElementAttrName {
 }
 
 impl ElementAttrName {
+    pub(crate) fn built_in(name: &Ident) -> Self {
+        Self::BuiltIn(normalize_raw_ident(name))
+    }
+
     fn multi_attribute_separator(&self) -> Option<&'static str> {
         match self {
             ElementAttrName::BuiltIn(i) => match i.to_string().as_str() {

+ 123 - 19
packages/rsx/src/component.rs

@@ -11,18 +11,18 @@
 //! - [ ] Keys
 //! - [ ] Properties spreading with with `..` syntax
 
-use self::location::CallerLocation;
+use self::{location::CallerLocation, util::try_parse_braces};
 
 use super::*;
 
 use proc_macro2::TokenStream as TokenStream2;
-use quote::quote;
+use quote::{quote, quote_spanned};
 use syn::{
-    ext::IdentExt, parse::ParseBuffer, spanned::Spanned, token::Brace,
-    AngleBracketedGenericArguments, Error, Expr, Ident, LitStr, PathArguments, Token,
+    ext::IdentExt, spanned::Spanned, token::Brace, AngleBracketedGenericArguments, Error, Expr,
+    Ident, LitStr, PathArguments, Token,
 };
 
-#[derive(PartialEq, Eq, Clone, Debug, Hash)]
+#[derive(Clone, Debug)]
 pub struct Component {
     pub name: syn::Path,
     pub prop_gen_args: Option<AngleBracketedGenericArguments>,
@@ -30,8 +30,35 @@ pub struct Component {
     pub fields: Vec<ComponentField>,
     pub children: Vec<BodyNode>,
     pub manual_props: Option<Expr>,
-    pub brace: syn::token::Brace,
+    pub brace: Option<syn::token::Brace>,
     pub location: CallerLocation,
+    errors: Vec<syn::Error>,
+}
+
+impl PartialEq for Component {
+    fn eq(&self, other: &Self) -> bool {
+        self.name == other.name
+            && self.prop_gen_args == other.prop_gen_args
+            && self.key == other.key
+            && self.fields == other.fields
+            && self.children == other.children
+            && self.manual_props == other.manual_props
+            && self.brace == other.brace
+    }
+}
+
+impl Eq for Component {}
+
+impl Hash for Component {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.name.hash(state);
+        self.prop_gen_args.hash(state);
+        self.key.hash(state);
+        self.fields.hash(state);
+        self.children.hash(state);
+        self.manual_props.hash(state);
+        self.brace.hash(state);
+    }
 }
 
 impl Parse for Component {
@@ -42,8 +69,10 @@ impl Parse for Component {
         // extract the path arguments from the path into prop_gen_args
         let prop_gen_args = normalize_path(&mut name);
 
-        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 component. We still parse it so that we can autocomplete it, but we don't need to parse the children
+            return Ok(Self::incomplete(name));
+        };
 
         let mut fields = Vec::new();
         let mut children = Vec::new();
@@ -65,7 +94,7 @@ impl Parse for Component {
                     && !content.peek2(Token![:])
                     && !content.peek2(Token![-]))
             {
-                // If it
+                // If it is a key, make sure it isn't static and then add it to the component
                 if content.fork().parse::<Ident>()? == "key" {
                     _ = content.parse::<Ident>()?;
                     _ = content.parse::<Token![:]>()?;
@@ -94,8 +123,9 @@ impl Parse for Component {
             fields,
             children,
             manual_props,
-            brace,
+            brace: Some(brace),
             key,
+            errors: Vec::new(),
         })
     }
 }
@@ -116,19 +146,78 @@ impl ToTokens for Component {
 
         let fn_name = self.fn_name();
 
-        tokens.append_all(quote! {
+        let errors = self.errors();
+
+        let component_node = quote_spanned! { name.span() =>
             dioxus_core::DynamicNode::Component({
+                #[allow(unused_imports)]
                 use dioxus_core::prelude::Properties;
                 (#builder).into_vcomponent(
                     #name #prop_gen_args,
                     #fn_name
                 )
             })
-        })
+        };
+
+        let component = if errors.is_empty() {
+            component_node
+        } else {
+            quote_spanned! {
+                name.span() => {
+                    #errors
+                    #component_node
+                }
+            }
+        };
+
+        tokens.append_all(component);
     }
 }
 
 impl Component {
+    /// Create a new Component
+    pub fn new(
+        name: syn::Path,
+        prop_gen_args: Option<AngleBracketedGenericArguments>,
+        fields: Vec<ComponentField>,
+        children: Vec<BodyNode>,
+        manual_props: Option<Expr>,
+        key: Option<IfmtInput>,
+        brace: syn::token::Brace,
+    ) -> Self {
+        Self {
+            errors: vec![],
+            name,
+            prop_gen_args,
+            fields,
+            children,
+            manual_props,
+            brace: Some(brace),
+            key,
+            location: CallerLocation::default(),
+        }
+    }
+
+    pub(crate) fn incomplete(name: syn::Path) -> Self {
+        Self {
+            errors: vec![syn::Error::new(
+                name.span(),
+                format!(
+                    "Missing braces after component name `{}`",
+                    name.segments.last().unwrap().ident
+                ),
+            )],
+            name,
+            prop_gen_args: None,
+            fields: Vec::new(),
+            children: Vec::new(),
+            manual_props: None,
+            brace: None,
+            key: None,
+            location: CallerLocation::default(),
+        }
+    }
+
     fn validate_component_path(path: &syn::Path) -> Result<()> {
         // ensure path segments doesn't have PathArguments, only the last
         // segment is allowed to have one.
@@ -157,15 +246,18 @@ impl Component {
     }
 
     fn collect_manual_props(&self, manual_props: &Expr) -> TokenStream2 {
-        let mut toks = quote! { let mut __manual_props = #manual_props; };
+        let mut toks =
+            quote_spanned! { manual_props.span() => let mut __manual_props = #manual_props; };
         for field in &self.fields {
             if field.name == "key" {
                 continue;
             }
             let ComponentField { name, content } = field;
-            toks.append_all(quote! { __manual_props.#name = #content; });
+            toks.append_all(
+                quote_spanned! { manual_props.span() => __manual_props.#name = #content; },
+            );
         }
-        toks.append_all(quote! { __manual_props });
+        toks.append_all(quote_spanned! { manual_props.span() => __manual_props });
         quote! {{ #toks }}
     }
 
@@ -173,23 +265,35 @@ impl Component {
         let name = &self.name;
 
         let mut toks = match &self.prop_gen_args {
-            Some(gen_args) => quote! { fc_to_builder(#name #gen_args) },
-            None => quote! { fc_to_builder(#name) },
+            Some(gen_args) => quote_spanned! { name.span() => fc_to_builder(#name #gen_args) },
+            None => quote_spanned! { name.span() => fc_to_builder(#name) },
         };
         for field in &self.fields {
             toks.append_all(quote! {#field})
         }
         if !self.children.is_empty() {
             let renderer = TemplateRenderer::as_tokens(&self.children, None);
-            toks.append_all(quote! { .children( { #renderer } ) });
+            toks.append_all(quote_spanned! { name.span() => .children( #renderer ) });
         }
-        toks.append_all(quote! { .build() });
+        toks.append_all(quote_spanned! { name.span() => .build() });
         toks
     }
 
     fn fn_name(&self) -> String {
         self.name.segments.last().unwrap().ident.to_string()
     }
+
+    /// If this element is only partially complete, return the errors that occurred during parsing
+    pub(crate) fn errors(&self) -> TokenStream2 {
+        let Self { errors, .. } = self;
+
+        let mut tokens = quote! {};
+        for error in errors {
+            tokens.append_all(error.to_compile_error());
+        }
+
+        tokens
+    }
 }
 
 // the struct's fields info

+ 22 - 12
packages/rsx/src/context.rs

@@ -2,7 +2,7 @@ use std::collections::HashMap;
 
 use crate::*;
 use proc_macro2::TokenStream as TokenStream2;
-use quote::quote;
+use quote::{quote, quote_spanned};
 
 /// As we create the dynamic nodes, we want to keep track of them in a linear fashion
 /// We'll use the size of the vecs to determine the index of the dynamic node in the final output
@@ -112,15 +112,13 @@ impl<'a> DynamicContext<'a> {
 
     fn render_static_element(&mut self, el: &'a Element) -> TokenStream2 {
         let el_name = &el.name;
-        let ns = |name| match el_name {
-            ElementName::Ident(i) => quote! { dioxus_elements::#i::#name },
-            ElementName::Custom(_) => quote! { None },
-        };
+        let ns = el_name.namespace();
+        let span = el_name.span();
 
         let static_attrs = el
             .merged_attributes
             .iter()
-            .map(|attr| self.render_merged_attributes(attr, ns, el_name))
+            .map(|attr| self.render_merged_attributes(attr, el_name))
             .collect::<Vec<_>>();
 
         let children = el
@@ -130,16 +128,30 @@ impl<'a> DynamicContext<'a> {
             .map(|(idx, root)| self.render_children_nodes(idx, root))
             .collect::<Vec<_>>();
 
-        let ns = ns(quote!(NAME_SPACE));
         let el_name = el_name.tag_name();
+        let completion_hints = el.completion_hints();
+        let errors = el.errors();
 
-        quote! {
+        let element = quote_spanned! {
+            span =>
             dioxus_core::TemplateNode::Element {
                 tag: #el_name,
                 namespace: #ns,
                 attrs: &[ #(#static_attrs)* ],
                 children: &[ #(#children),* ],
             }
+        };
+
+        if errors.is_empty() && completion_hints.is_empty() {
+            element
+        } else {
+            quote! {
+                {
+                    #completion_hints
+                    #errors
+                    #element
+                }
+            }
         }
     }
 
@@ -154,13 +166,12 @@ impl<'a> DynamicContext<'a> {
     fn render_merged_attributes(
         &mut self,
         attr: &'a AttributeType,
-        ns: impl Fn(TokenStream2) -> TokenStream2,
         el_name: &ElementName,
     ) -> TokenStream2 {
         // Rendering static attributes requires a bit more work than just a dynamic attrs
         match attr.as_static_str_literal() {
             // If it's static, we'll take this little optimization
-            Some((name, value)) => Self::render_static_attr(value, name, ns, el_name),
+            Some((name, value)) => Self::render_static_attr(value, name, el_name),
 
             // Otherwise, we'll just render it as a dynamic attribute
             // This will also insert the attribute into the dynamic_attributes list to assemble the final template
@@ -171,13 +182,12 @@ impl<'a> DynamicContext<'a> {
     fn render_static_attr(
         value: &IfmtInput,
         name: &ElementAttrName,
-        ns: impl Fn(TokenStream2) -> TokenStream2,
         el_name: &ElementName,
     ) -> TokenStream2 {
         let value = value.to_static().unwrap();
 
         let ns = match name {
-            ElementAttrName::BuiltIn(name) => ns(quote!(#name.1)),
+            ElementAttrName::BuiltIn(name) => quote! { #el_name::#name.1 },
             ElementAttrName::Custom(_) => quote!(None),
         };
 

+ 198 - 63
packages/rsx/src/element.rs

@@ -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![:]) {
-                attr_after_element!(content.span());
-            }
-
-            if (content.peek(Ident) && content.peek2(Token![:])) && !content.peek3(Token![:]) {
+            if ((content.peek(Ident) || content.peek(LitStr)) && 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 }),
         }
     }

+ 7 - 1
packages/rsx/src/errors.rs

@@ -1,6 +1,12 @@
+use proc_macro2::Span;
+
+pub(crate) fn missing_trailing_comma(span: Span) -> syn::Error {
+    syn::Error::new(span, "missing trailing comma")
+}
+
 macro_rules! missing_trailing_comma {
     ($span:expr) => {
-        return Err(syn::Error::new($span, "missing trailing comma"));
+        return Err(crate::errors::missing_trailing_comma($span));
     };
 }
 

+ 39 - 2
packages/rsx/src/ifmt.rs

@@ -32,6 +32,17 @@ impl IfmtInput {
             self.segments.push(Segment::Literal(separator.to_string()));
         }
         self.segments.extend(other.segments);
+        if let Some(source) = &other.source {
+            self.source = Some(LitStr::new(
+                &format!(
+                    "{}{}{}",
+                    self.source.as_ref().unwrap().value(),
+                    separator,
+                    source.value()
+                ),
+                source.span(),
+            ));
+        }
         self
     }
 
@@ -44,6 +55,12 @@ impl IfmtInput {
 
     pub fn push_str(&mut self, s: &str) {
         self.segments.push(Segment::Literal(s.to_string()));
+        if let Some(source) = &self.source {
+            self.source = Some(LitStr::new(
+                &format!("{}{}", source.value(), s),
+                source.span(),
+            ));
+        }
     }
 
     pub fn is_static(&self) -> bool {
@@ -66,6 +83,15 @@ impl IfmtInput {
             })
     }
 
+    fn is_simple_expr(&self) -> bool {
+        self.segments.iter().all(|seg| match seg {
+            Segment::Literal(_) => true,
+            Segment::Formatted(FormattedSegment { segment, .. }) => {
+                matches!(segment, FormattedSegmentType::Ident(_))
+            }
+        })
+    }
+
     /// Try to convert this into a single _.to_string() call if possible
     ///
     /// Using "{single_expression}" is pretty common, but you don't need to go through the whole format! machinery for that, so we optimize it here.
@@ -177,8 +203,19 @@ impl FromStr for IfmtInput {
 impl ToTokens for IfmtInput {
     fn to_tokens(&self, tokens: &mut TokenStream) {
         // Try to turn it into a single _.to_string() call
-        if let Some(single_dynamic) = self.try_to_string() {
-            tokens.extend(single_dynamic);
+        if !cfg!(debug_assertions) {
+            if let Some(single_dynamic) = self.try_to_string() {
+                tokens.extend(single_dynamic);
+                return;
+            }
+        }
+
+        // If the segments are not complex exprs, we can just use format! directly to take advantage of RA rename/expansion
+        if self.is_simple_expr() {
+            let raw = &self.source;
+            tokens.extend(quote! {
+                ::std::format_args!(#raw)
+            });
             return;
         }
 

+ 50 - 4
packages/rsx/src/lib.rs

@@ -13,6 +13,41 @@
 //! - [x] Good errors if parsing fails
 //!
 //! Any errors in using rsx! will likely occur when people start using it, so the first errors must be really helpful.
+//!
+//! # Completions
+//! Rust analyzer completes macros by looking at the expansion of the macro and trying to match the start of identifiers in the macro to identifiers in the current scope
+//!
+//! Eg, if a macro expands to this:
+//! ```rust, ignore
+//! struct MyStruct;
+//!
+//! // macro expansion
+//! My
+//! ```
+//! Then the analyzer will try to match the start of the identifier "My" to an identifier in the current scope (MyStruct in this case).
+//!
+//! In dioxus, our macros expand to the completions module if we know the identifier is incomplete:
+//! ```rust, ignore
+//! // In the root of the macro, identifiers must be elements
+//! // rsx! { di }
+//! dioxus_elements::elements::di
+//!
+//! // Before the first child element, every following identifier is either an attribute or an element
+//! // rsx! { div { ta } }
+//! // Isolate completions scope
+//! mod completions__ {
+//!     // import both the attributes and elements this could complete to
+//!     use dioxus_elements::elements::div::*;
+//!     use dioxus_elements::elements::*;
+//!     fn complete() {
+//!         ta;
+//!     }
+//! }
+//!
+//! // After the first child element, every following identifier is another element
+//! // rsx! { div { attribute: value, child {} di } }
+//! dioxus_elements::elements::di
+//! ```
 
 #[macro_use]
 mod errors;
@@ -145,14 +180,19 @@ impl CallBody {
             ),
         })
     }
-}
 
-impl Parse for CallBody {
-    fn parse(input: ParseStream) -> Result<Self> {
+    /// Parse a stream into a CallBody. Return all error immediately instead of trying to partially expand the macro
+    ///
+    /// This should be preferred over `parse` if you are outside of a macro
+    pub fn parse_strict(input: ParseStream) -> Result<Self> {
+        Self::parse_with_options(input, false)
+    }
+
+    fn parse_with_options(input: ParseStream, partial_completions: bool) -> Result<Self> {
         let mut roots = Vec::new();
 
         while !input.is_empty() {
-            let node = input.parse::<BodyNode>()?;
+            let node = BodyNode::parse_with_options(input, partial_completions)?;
 
             if input.peek(Token![,]) {
                 let _ = input.parse::<Token![,]>();
@@ -165,6 +205,12 @@ impl Parse for CallBody {
     }
 }
 
+impl Parse for CallBody {
+    fn parse(input: ParseStream) -> Result<Self> {
+        Self::parse_with_options(input, true)
+    }
+}
+
 impl ToTokens for CallBody {
     fn to_tokens(&self, out_tokens: &mut TokenStream2) {
         // Empty templates just are placeholders for "none"

+ 131 - 20
packages/rsx/src/node.rs

@@ -6,9 +6,10 @@ use proc_macro2::{Span, TokenStream as TokenStream2};
 use quote::quote;
 use syn::{
     braced,
+    parse::ParseBuffer,
     spanned::Spanned,
     token::{self, Brace},
-    Expr, ExprIf, LitStr, Pat,
+    Expr, ExprCall, ExprIf, Ident, LitStr, Pat,
 };
 
 /*
@@ -19,17 +20,45 @@ Parse
 -> "text {with_args}"
 -> {(0..10).map(|f| rsx!("asd"))}  // <--- notice the curly braces
 */
-#[derive(PartialEq, Eq, Clone, Debug, Hash)]
+#[derive(Clone, Debug)]
 pub enum BodyNode {
     Element(Element),
     Text(IfmtInput),
-    RawExpr(Expr),
-
+    RawExpr(TokenStream2),
     Component(Component),
     ForLoop(ForLoop),
     IfChain(IfChain),
 }
 
+impl PartialEq for BodyNode {
+    fn eq(&self, other: &Self) -> bool {
+        match (self, other) {
+            (Self::Element(l), Self::Element(r)) => l == r,
+            (Self::Text(l), Self::Text(r)) => l == r,
+            (Self::RawExpr(l), Self::RawExpr(r)) => l.to_string() == r.to_string(),
+            (Self::Component(l), Self::Component(r)) => l == r,
+            (Self::ForLoop(l), Self::ForLoop(r)) => l == r,
+            (Self::IfChain(l), Self::IfChain(r)) => l == r,
+            _ => false,
+        }
+    }
+}
+
+impl Eq for BodyNode {}
+
+impl Hash for BodyNode {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        match self {
+            Self::Element(el) => el.hash(state),
+            Self::Text(text) => text.hash(state),
+            Self::RawExpr(exp) => exp.to_string().hash(state),
+            Self::Component(comp) => comp.hash(state),
+            Self::ForLoop(for_loop) => for_loop.hash(state),
+            Self::IfChain(if_chain) => if_chain.hash(state),
+        }
+    }
+}
+
 impl BodyNode {
     pub fn is_litstr(&self) -> bool {
         matches!(self, BodyNode::Text { .. })
@@ -45,10 +74,16 @@ impl BodyNode {
             BodyNode::IfChain(f) => f.if_token.span(),
         }
     }
-}
 
-impl Parse for BodyNode {
-    fn parse(stream: ParseStream) -> Result<Self> {
+    pub(crate) fn parse_with_options(
+        stream: ParseStream,
+        partial_completions: bool,
+    ) -> Result<Self> {
+        // Make sure the next token is a brace if we're not in partial completion mode
+        fn peek_brace(stream: &ParseBuffer, partial_completions: bool) -> bool {
+            partial_completions || stream.peek(token::Brace)
+        }
+
         if stream.peek(LitStr) {
             return Ok(BodyNode::Text(stream.parse()?));
         }
@@ -56,7 +91,7 @@ impl Parse for BodyNode {
         // if this is a dash-separated path, it's a web component (custom element)
         let body_stream = stream.fork();
         if let Ok(ElementName::Custom(name)) = body_stream.parse::<ElementName>() {
-            if name.value().contains('-') && body_stream.peek(token::Brace) {
+            if name.value().contains('-') && peek_brace(&body_stream, partial_completions) {
                 return Ok(BodyNode::Element(stream.parse::<Element>()?));
             }
         }
@@ -73,21 +108,65 @@ impl Parse for BodyNode {
             // example:
             // div {}
             if let Some(ident) = path.get_ident() {
-                let el_name = ident.to_string();
-
-                let first_char = el_name.chars().next().unwrap();
-
-                if body_stream.peek(token::Brace)
-                    && first_char.is_ascii_lowercase()
-                    && !el_name.contains('_')
+                if peek_brace(&body_stream, partial_completions)
+                    && !ident_looks_like_component(ident)
                 {
-                    return Ok(BodyNode::Element(stream.parse::<Element>()?));
+                    return Ok(BodyNode::Element(Element::parse_with_options(
+                        stream,
+                        partial_completions,
+                    )?));
+                }
+            }
+
+            // If it is a single function call with a name that looks like a component, it should probably be a component
+            // Eg, if we run into this:
+            // ```rust
+            // my_function(key, prop)
+            // ```
+            // We should tell the user that they need braces around props instead of turning the component call into an expression
+            if let Ok(call) = stream.fork().parse::<ExprCall>() {
+                if let Expr::Path(path) = call.func.as_ref() {
+                    if let Some(ident) = path.path.get_ident() {
+                        if ident_looks_like_component(ident) {
+                            let function_args: Vec<_> = call
+                                .args
+                                .iter()
+                                .map(|arg| arg.to_token_stream().to_string())
+                                .collect();
+                            let function_call = format!("{}({})", ident, function_args.join(", "));
+                            let component_call = if function_args.is_empty() {
+                                format!("{} {{}}", ident)
+                            } else {
+                                let component_args: Vec<_> = call
+                                    .args
+                                    .iter()
+                                    .enumerate()
+                                    .map(|(prop_count, arg)| {
+                                        // Try to parse it as a shorthand field
+                                        if let Ok(simple_ident) =
+                                            syn::parse2::<Ident>(arg.to_token_stream())
+                                        {
+                                            format!("{}", simple_ident)
+                                        } else {
+                                            let ident = format!("prop{}", prop_count + 1);
+                                            format!("{}: {}", ident, arg.to_token_stream())
+                                        }
+                                    })
+                                    .collect();
+                                format!("{} {{\n\t{}\n}}", ident, component_args.join(",\n\t"))
+                            };
+                            let error_text = format!(
+                                "Expected a valid body node found a function call. Did you forget to add braces around props?\nComponents should be called with braces instead of being called as expressions.\nInstead of:\n```rust\n{function_call}\n```\nTry:\n```rust\n{component_call}\n```\nIf you are trying to call a function, not a component, you need to wrap your expression in braces.",
+                            );
+                            return Err(syn::Error::new(call.span(), error_text));
+                        }
+                    }
                 }
             }
 
             // Otherwise this should be Component, allowed syntax:
             // - syn::Path
-            // - PathArguments can only apper in last segment
+            // - PathArguments can only appear in last segment
             // - followed by `{` or `(`, note `(` cannot be used with one ident
             //
             // example
@@ -99,7 +178,7 @@ impl Parse for BodyNode {
             // crate::component{}
             // Input::<InputProps<'_, i32> {}
             // crate::Input::<InputProps<'_, i32> {}
-            if body_stream.peek(token::Brace) {
+            if peek_brace(&body_stream, partial_completions) {
                 return Ok(BodyNode::Component(stream.parse()?));
             }
         }
@@ -124,11 +203,26 @@ impl Parse for BodyNode {
         // }
         // ```
         if stream.peek(Token![match]) {
-            return Ok(BodyNode::RawExpr(stream.parse::<Expr>()?));
+            return Ok(BodyNode::RawExpr(stream.parse::<Expr>()?.to_token_stream()));
         }
 
         if stream.peek(token::Brace) {
-            return Ok(BodyNode::RawExpr(stream.parse::<Expr>()?));
+            // If we are in strict mode, make sure thing inside the braces is actually a valid expression
+            let combined = if !partial_completions {
+                stream.parse::<Expr>()?.to_token_stream()
+            } else {
+                // otherwise, just take whatever is inside the braces. It might be invalid, but we still want to spit it out so we get completions
+                let content;
+                let brace = braced!(content in stream);
+                let content: TokenStream2 = content.parse()?;
+                let mut combined = TokenStream2::new();
+                brace.surround(&mut combined, |inside_brace| {
+                    inside_brace.append_all(content);
+                });
+                combined
+            };
+
+            return Ok(BodyNode::RawExpr(combined));
         }
 
         Err(syn::Error::new(
@@ -138,6 +232,20 @@ impl Parse for BodyNode {
     }
 }
 
+// Checks if an ident looks like a component
+fn ident_looks_like_component(ident: &Ident) -> bool {
+    let as_string = ident.to_string();
+    let first_char = as_string.chars().next().unwrap();
+    // Components either start with an uppercase letter or have an underscore in them
+    first_char.is_ascii_uppercase() || as_string.contains('_')
+}
+
+impl Parse for BodyNode {
+    fn parse(stream: ParseStream) -> Result<Self> {
+        Self::parse_with_options(stream, true)
+    }
+}
+
 impl ToTokens for BodyNode {
     fn to_tokens(&self, tokens: &mut TokenStream2) {
         match self {
@@ -153,6 +261,7 @@ impl ToTokens for BodyNode {
             // Expressons too
             BodyNode::RawExpr(exp) => tokens.append_all(quote! {
                 {
+                    #[allow(clippy::let_and_return)]
                     let ___nodes = (#exp).into_dyn_node();
                     ___nodes
                 }
@@ -226,6 +335,7 @@ impl ToTokens for ForLoop {
         // And then we can return them into the dyn loop
         tokens.append_all(quote! {
             {
+                #[allow(clippy::let_and_return)]
                 let ___nodes = (#expr).into_iter().map(|#pat| { #renderer }).into_dyn_node();
                 ___nodes
             }
@@ -320,6 +430,7 @@ impl ToTokens for IfChain {
 
         tokens.append_all(quote! {
             {
+                #[allow(clippy::let_and_return)]
                 let ___nodes = (#body).into_dyn_node();
                 ___nodes
             }

+ 9 - 0
packages/rsx/src/renderer.rs

@@ -58,6 +58,7 @@ impl<'a> TemplateRenderer<'a> {
 
             {
                 // NOTE: Allocating a temporary is important to make reads within rsx drop before the value is returned
+                #[allow(clippy::let_and_return)]
                 let __vnodes = dioxus_core::VNode::new(
                     #key_tokens,
                     TEMPLATE,
@@ -97,6 +98,14 @@ impl<'a> TemplateRenderer<'a> {
         let root_col = match self.roots.first() {
             Some(first_root) => {
                 let first_root_span = format!("{:?}", first_root.span());
+
+                // Rust analyzer will not autocomplete properly if we change the name every time you type a character
+                // If it looks like we are running in rust analyzer, we'll just use a placeholder location
+                let looks_like_rust_analyzer = first_root_span.contains("SpanData");
+                if looks_like_rust_analyzer {
+                    return "0".to_string();
+                }
+
                 first_root_span
                     .rsplit_once("..")
                     .and_then(|(_, after)| after.split_once(')').map(|(before, _)| before))

+ 7 - 1
packages/rsx/src/util.rs

@@ -1 +1,7 @@
-
+pub(crate) fn try_parse_braces<'a>(
+    input: &syn::parse::ParseBuffer<'a>,
+) -> syn::Result<(syn::token::Brace, syn::parse::ParseBuffer<'a>)> {
+    let content;
+    let brace = syn::braced!(content in input);
+    Ok((brace, content))
+}

+ 1 - 1
packages/rsx/tests/parsing/multiexpr.expanded.rsx

@@ -1 +1 @@
-dioxus_core :: TemplateNode :: Element { tag : dioxus_elements :: circle :: TAG_NAME , namespace : dioxus_elements :: circle :: NAME_SPACE , attrs : & [dioxus_core :: TemplateAttribute :: Dynamic { id : 0usize } , dioxus_core :: TemplateAttribute :: Dynamic { id : 1usize } , dioxus_core :: TemplateAttribute :: Dynamic { id : 2usize } , dioxus_core :: TemplateAttribute :: Static { name : dioxus_elements :: circle :: stroke . 0 , namespace : dioxus_elements :: circle :: stroke . 1 , value : "green" , } , dioxus_core :: TemplateAttribute :: Static { name : dioxus_elements :: circle :: fill . 0 , namespace : dioxus_elements :: circle :: fill . 1 , value : "yellow" , } ,] , children : & [] , }
+dioxus_core :: TemplateNode :: Element { tag : dioxus_elements :: elements :: circle :: TAG_NAME , namespace : dioxus_elements :: elements :: circle :: NAME_SPACE , attrs : & [dioxus_core :: TemplateAttribute :: Dynamic { id : 0usize } , dioxus_core :: TemplateAttribute :: Dynamic { id : 1usize } , dioxus_core :: TemplateAttribute :: Dynamic { id : 2usize } , dioxus_core :: TemplateAttribute :: Static { name : dioxus_elements :: elements :: circle :: stroke . 0 , namespace : dioxus_elements :: elements :: circle :: stroke . 1 , value : "green" , } , dioxus_core :: TemplateAttribute :: Static { name : dioxus_elements :: elements :: circle :: fill . 0 , namespace : dioxus_elements :: elements :: circle :: fill . 1 , value : "yellow" , } ,] , children : & [] , }