Autocomplete rsx (#2421)

* autocomplete rsx

* hide the completions module

* fix autocomplete for identifiers that start with a raw identifier

* add field completion hints for props

* remove extra import

* Revert "add field completion hints for props"

This reverts commit 194899c73783e0f9b5d601d53365746d838f2048.

* fix autocomplete inside raw expressions

* fix autofmt

* feat: just use regular ifmt if it's compatible

reuses the tokens for ifmt so RA provides its HIR lowering on simple formatted strings

* fix merging ifmt strings

* add a better error message for components called like functions

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
Evan AlmloffandJonathan Kelley authored and GitHub committed 2024-05-22 07:24:50 -05:00
1 parent 245003a5d4
commit 2523581cf4
21 files changed
+788 -217

No files matched your search

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