diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index 361a3839b1..5bf10af690 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -294,10 +294,7 @@ fn WithInline(text: String) -> Element { } #[component] -fn Label(text: T) -> Element -where - T: Display, -{ +fn Label(text: T) -> Element { rsx! { p { "{text}" } } diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index 774fac38d8..babda20152 100644 --- a/packages/autofmt/src/element.rs +++ b/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 { +fn get_expr_length(expr: &impl Spanned) -> Option { let span = expr.span(); let (start, end) = (span.start(), span.end()); if start.line == end.line { diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs index 9cfb26c078..2de7b40028 100644 --- a/packages/autofmt/src/lib.rs +++ b/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 { continue; } - let body = item.parse_body::().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 { - let body = syn::parse2::(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 { } pub fn fmt_block(block: &str, indent_level: usize, indent: IndentOptions) -> Option { - let body = syn::parse_str::(block).unwrap(); + let body = CallBody::parse_strict.parse_str(block).unwrap(); let mut buf = Writer::new(block); diff --git a/packages/core-macro/src/component.rs b/packages/core-macro/src/component.rs index a347fee64e..ed88c25e02 100644 --- a/packages/core-macro/src/component.rs +++ b/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> { diff --git a/packages/dioxus-lib/src/lib.rs b/packages/dioxus-lib/src/lib.rs index f73e32125d..6748d472fa 100644 --- a/packages/dioxus-lib/src/lib.rs +++ b/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; } diff --git a/packages/dioxus/src/lib.rs b/packages/dioxus/src/lib.rs index bfb1fcdae1..caf1ea3460 100644 --- a/packages/dioxus/src/lib.rs +++ b/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")), diff --git a/packages/html-internal-macro/src/lib.rs b/packages/html-internal-macro/src/lib.rs index 460cf8f6b0..88d857d8af 100644 --- a/packages/html-internal-macro/src/lib.rs +++ b/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, } @@ -23,16 +22,11 @@ impl Parse for ImplExtensionAttributes { fn parse(input: ParseStream) -> syn::Result { 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; diff --git a/packages/html/src/global_attributes.rs b/packages/html/src/attribute_groups.rs similarity index 99% rename from packages/html/src/global_attributes.rs rename to packages/html/src/attribute_groups.rs index cbac1d87e9..cc4fbb09e9 100644 --- a/packages/html/src/global_attributes.rs +++ b/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; diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 5275fa4f6e..cdf9c2a764 100644 --- a/packages/html/src/elements.rs +++ b/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,)* }]; )* } }; diff --git a/packages/html/src/lib.rs b/packages/html/src/lib.rs index 17ca25aef5..c37fefe2c8 100644 --- a/packages/html/src/lib.rs +++ b/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}; } diff --git a/packages/rsx-rosetta/src/lib.rs b/packages/rsx-rosetta/src/lib.rs index 1707a9a3a7..2ff7ef7bb9 100644 --- a/packages/rsx-rosetta/src/lib.rs +++ b/packages/rsx-rosetta/src/lib.rs @@ -64,50 +64,46 @@ pub fn rsx_node_from_html(node: &Node) -> Option { } }; - 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) { 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); diff --git a/packages/rsx/src/attribute.rs b/packages/rsx/src/attribute.rs index 038e596d21..7d4af03033 100644 --- a/packages/rsx/src/attribute.rs +++ b/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 { 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::(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() { diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs index cbfe5aea5e..33f71a2e56 100644 --- a/packages/rsx/src/component.rs +++ b/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, @@ -30,8 +30,35 @@ pub struct Component { pub fields: Vec, pub children: Vec, pub manual_props: Option, - pub brace: syn::token::Brace, + pub brace: Option, pub location: CallerLocation, + errors: Vec, +} + +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(&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::()? == "key" { _ = content.parse::()?; _ = content.parse::()?; @@ -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, + fields: Vec, + children: Vec, + manual_props: Option, + key: Option, + 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 diff --git a/packages/rsx/src/context.rs b/packages/rsx/src/context.rs index 9dfeda3d3e..9ce9686669 100644 --- a/packages/rsx/src/context.rs +++ b/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::>(); let children = el @@ -130,16 +128,30 @@ impl<'a> DynamicContext<'a> { .map(|(idx, root)| self.render_children_nodes(idx, root)) .collect::>(); - 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), }; diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index a060a53215..1515f12fc5 100644 --- a/packages/rsx/src/element.rs +++ b/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, pub attributes: Vec, pub merged_attributes: Vec, pub children: Vec, - pub brace: syn::token::Brace, + pub brace: Option, + // Non-fatal errors that occurred during parsing + errors: Vec, +} + +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(&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 { + /// 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 { + 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 = vec![]; let mut children: Vec = 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::().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::()?; let value = content.parse::()?; + let followed_by_comma = content.parse::().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::().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::()?; + 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::()?; @@ -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::()?; + 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::()? + }; 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::().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::()?; + 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::().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::().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::()?); + 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::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 }), } } diff --git a/packages/rsx/src/errors.rs b/packages/rsx/src/errors.rs index 94ff60ae51..70cbc7fd0d 100644 --- a/packages/rsx/src/errors.rs +++ b/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)); }; } diff --git a/packages/rsx/src/ifmt.rs b/packages/rsx/src/ifmt.rs index fc4eac383b..a2518b7dd1 100644 --- a/packages/rsx/src/ifmt.rs +++ b/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; } diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index 4a1ce335b4..c7ac2f4f84 100644 --- a/packages/rsx/src/lib.rs +++ b/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 { + /// 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::parse_with_options(input, false) + } + + fn parse_with_options(input: ParseStream, partial_completions: bool) -> Result { let mut roots = Vec::new(); while !input.is_empty() { - let node = input.parse::()?; + let node = BodyNode::parse_with_options(input, partial_completions)?; if input.peek(Token![,]) { let _ = input.parse::(); @@ -165,6 +205,12 @@ impl Parse for CallBody { } } +impl Parse for CallBody { + fn parse(input: ParseStream) -> Result { + 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" diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index ff73b233c5..c27f0eb147 100644 --- a/packages/rsx/src/node.rs +++ b/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(&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 { + pub(crate) fn parse_with_options( + stream: ParseStream, + partial_completions: bool, + ) -> Result { + // 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::() { - 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::()?)); } } @@ -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::()?)); + 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::() { + 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::(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:: {} // crate::Input:: {} - 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::()?)); + return Ok(BodyNode::RawExpr(stream.parse::()?.to_token_stream())); } if stream.peek(token::Brace) { - return Ok(BodyNode::RawExpr(stream.parse::()?)); + // If we are in strict mode, make sure thing inside the braces is actually a valid expression + let combined = if !partial_completions { + stream.parse::()?.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::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 } diff --git a/packages/rsx/src/renderer.rs b/packages/rsx/src/renderer.rs index 81df936d24..8605125e2f 100644 --- a/packages/rsx/src/renderer.rs +++ b/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)) diff --git a/packages/rsx/src/util.rs b/packages/rsx/src/util.rs index 8b13789179..9d5fbcda55 100644 --- a/packages/rsx/src/util.rs +++ b/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)) +} diff --git a/packages/rsx/tests/parsing/multiexpr.expanded.rsx b/packages/rsx/tests/parsing/multiexpr.expanded.rsx index d503733b31..73158f1c64 100644 --- a/packages/rsx/tests/parsing/multiexpr.expanded.rsx +++ b/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 : & [] , }