Autocomplete rsx (#2421)
* autocomplete rsx * hide the completions module * fix autocomplete for identifiers that start with a raw identifier * add field completion hints for props * remove extra import * Revert "add field completion hints for props" This reverts commit 194899c73783e0f9b5d601d53365746d838f2048. * fix autocomplete inside raw expressions * fix autofmt * feat: just use regular ifmt if it's compatible reuses the tokens for ifmt so RA provides its HIR lowering on simple formatted strings * fix merging ifmt strings * add a better error message for components called like functions --------- Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
1 parent
245003a5d4
commit
2523581cf4
21 files changed
+788
-217
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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")),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,)* }];
|
||||
)*
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 }),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 : & [] , }
|
||||
Reference in new issue
Block a user