Hotreloading of for/if/body, formatted strings, literals, component props, nested rsx, light CLI rewrite, cli TUI (#2258)
Hotreload the contents of for loops, if chains, component bodies, props, attributes, and any literals discovered in rsx! Add a TUI renderer to the CLI. Improve the CLI build system to be async and parallel. Refactor RSX to allow partial expansion of expressions. Merge autofmt implementations for consistency. Merge the representation of elements and components under the hood. Add a diagnostics system for rsx for improved error messages. Drop interprocess and move to websockets for communication between the CLI and the server. Assign IDs to nodes and attributes in a stable way to be used in non compiler contexts. Add hotreloading to any body of component/for loop/if chain/etc. --------- Co-authored-by: Evan Almloff <evanalmloff@gmail.com> Co-authored-by: Liam Mitchell <liamkarlmitchell@gmail.com>
This commit is contained in:
181 files changed
+12755
-10806
No files matched your search
+548
-441
File diff suppressed because it is too large.
Load diff
+385
-326
@@ -1,224 +1,134 @@
|
||||
//! Parse components into the VNode::Component variant
|
||||
//! ==========================================
|
||||
//!
|
||||
//! We can be reasonably sure that whatever enters this parsing path is in the right format.
|
||||
//! This feature must support
|
||||
//! - [x] Namespaced components
|
||||
//! - [x] Fields
|
||||
//! - [x] Componentbuilder synax
|
||||
//! - [x] Optional commas
|
||||
//! - [ ] Children
|
||||
//! - [ ] Keys
|
||||
//! - [ ] Properties spreading with with `..` syntax
|
||||
|
||||
use self::{location::CallerLocation, util::try_parse_braces};
|
||||
|
||||
use super::*;
|
||||
//! Uses the regular robust RsxBlock parser and then validates the component, emitting errors as
|
||||
//! diagnostics. This was refactored from a straightforward parser to this validation approach so
|
||||
//! that we can emit errors as diagnostics instead of returning results.
|
||||
//!
|
||||
//! Using this approach we can provide *much* better errors as well as partial expansion whereever
|
||||
//! possible.
|
||||
//!
|
||||
//! It does lead to the code actually being larger than it was before, but it should be much easier
|
||||
//! to work with and extend. To add new syntax, we add it to the RsxBlock parser and then add a
|
||||
//! validation step here. This does make using the component as a source of truth not as good, but
|
||||
//! oddly enoughly, we want the tree to actually be capable of being technically invalid. This is not
|
||||
//! usual for building in Rust - you want strongly typed things to be valid - but in this case, we
|
||||
//! want to accept all sorts of malformed input and then provide the best possible error messages.
|
||||
//!
|
||||
//! If you're generally parsing things, you'll just want to parse and then check if it's valid.
|
||||
|
||||
use crate::innerlude::*;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{quote, quote_spanned};
|
||||
use proc_macro2_diagnostics::SpanDiagnosticExt;
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use std::collections::HashSet;
|
||||
use syn::{
|
||||
ext::IdentExt, spanned::Spanned, token::Brace, AngleBracketedGenericArguments, Error, Expr,
|
||||
Ident, LitStr, PathArguments, Token,
|
||||
parse::{Parse, ParseStream},
|
||||
spanned::Spanned,
|
||||
token, AngleBracketedGenericArguments, Expr, Ident, PathArguments, Result,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub struct Component {
|
||||
pub name: syn::Path,
|
||||
pub prop_gen_args: Option<AngleBracketedGenericArguments>,
|
||||
pub key: Option<IfmtInput>,
|
||||
pub fields: Vec<ComponentField>,
|
||||
pub children: Vec<BodyNode>,
|
||||
pub manual_props: Option<Expr>,
|
||||
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);
|
||||
}
|
||||
pub generics: Option<AngleBracketedGenericArguments>,
|
||||
pub fields: Vec<Attribute>,
|
||||
pub spreads: Vec<Spread>,
|
||||
pub brace: token::Brace,
|
||||
pub children: TemplateBody,
|
||||
pub dyn_idx: DynIdx,
|
||||
pub diagnostics: Diagnostics,
|
||||
}
|
||||
|
||||
impl Parse for Component {
|
||||
fn parse(stream: ParseStream) -> Result<Self> {
|
||||
let mut name = stream.parse::<syn::Path>()?;
|
||||
Component::validate_component_path(&name)?;
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let mut name = input.parse::<syn::Path>()?;
|
||||
let generics = normalize_path(&mut name);
|
||||
|
||||
// extract the path arguments from the path into prop_gen_args
|
||||
let prop_gen_args = normalize_path(&mut name);
|
||||
|
||||
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));
|
||||
if !input.peek(token::Brace) {
|
||||
return Ok(Self::empty(name, generics));
|
||||
};
|
||||
|
||||
let mut fields = Vec::new();
|
||||
let mut children = Vec::new();
|
||||
let mut manual_props = None;
|
||||
let mut key = None;
|
||||
|
||||
while !content.is_empty() {
|
||||
// if we splat into a component then we're merging properties
|
||||
if content.peek(Token![..]) {
|
||||
content.parse::<Token![..]>()?;
|
||||
manual_props = Some(content.parse()?);
|
||||
} else if
|
||||
// Named fields
|
||||
(content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]))
|
||||
// shorthand struct initialization
|
||||
// Not a div {}, mod::Component {}, or web-component {}
|
||||
|| (content.peek(Ident)
|
||||
&& !content.peek2(Brace)
|
||||
&& !content.peek2(Token![:])
|
||||
&& !content.peek2(Token![-]))
|
||||
{
|
||||
// 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![:]>()?;
|
||||
|
||||
let _key: IfmtInput = content.parse()?;
|
||||
if _key.is_static() {
|
||||
invalid_key!(_key);
|
||||
}
|
||||
key = Some(_key);
|
||||
} else {
|
||||
fields.push(content.parse()?);
|
||||
}
|
||||
} else {
|
||||
children.push(content.parse()?);
|
||||
}
|
||||
|
||||
if content.peek(Token![,]) {
|
||||
let _ = content.parse::<Token![,]>();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
location: CallerLocation::default(),
|
||||
name,
|
||||
prop_gen_args,
|
||||
fields,
|
||||
let RsxBlock {
|
||||
attributes: fields,
|
||||
children,
|
||||
manual_props,
|
||||
brace: Some(brace),
|
||||
key,
|
||||
errors: Vec::new(),
|
||||
})
|
||||
brace,
|
||||
spreads,
|
||||
diagnostics,
|
||||
} = input.parse::<RsxBlock>()?;
|
||||
|
||||
let mut component = Self {
|
||||
dyn_idx: DynIdx::default(),
|
||||
children: TemplateBody::new(children),
|
||||
name,
|
||||
generics,
|
||||
fields,
|
||||
brace,
|
||||
spreads,
|
||||
diagnostics,
|
||||
};
|
||||
|
||||
// We've received a valid rsx block, but it's not necessarily a valid component
|
||||
// validating it will dump diagnostics into the output
|
||||
component.validate_component_path();
|
||||
component.validate_fields();
|
||||
component.validate_key();
|
||||
component.validate_component_spread();
|
||||
|
||||
Ok(component)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Component {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let Self {
|
||||
name,
|
||||
prop_gen_args,
|
||||
..
|
||||
} = self;
|
||||
let Self { name, generics, .. } = self;
|
||||
|
||||
let builder = self
|
||||
.manual_props
|
||||
.as_ref()
|
||||
.map(|props| self.collect_manual_props(props))
|
||||
.unwrap_or_else(|| self.collect_props());
|
||||
// Create props either from manual props or from the builder approach
|
||||
let props = self.create_props();
|
||||
|
||||
let fn_name = self.fn_name();
|
||||
// Make sure we stringify the component name
|
||||
let fn_name = self.fn_name().to_string();
|
||||
|
||||
let errors = self.errors();
|
||||
// Make sure we emit any errors
|
||||
let diagnostics = &self.diagnostics;
|
||||
|
||||
let component_node = quote_spanned! { name.span() =>
|
||||
tokens.append_all(quote! {
|
||||
dioxus_core::DynamicNode::Component({
|
||||
#[allow(unused_imports)]
|
||||
|
||||
// todo: ensure going through the trait actually works
|
||||
// we want to avoid importing traits
|
||||
// use dioxus_core::prelude::Properties;
|
||||
use dioxus_core::prelude::Properties;
|
||||
(#builder).into_vcomponent(
|
||||
#name #prop_gen_args,
|
||||
let __comp = ({
|
||||
#props
|
||||
}).into_vcomponent(
|
||||
#name #generics,
|
||||
#fn_name
|
||||
)
|
||||
);
|
||||
#diagnostics
|
||||
__comp
|
||||
})
|
||||
};
|
||||
|
||||
let component = if errors.is_empty() {
|
||||
component_node
|
||||
} else {
|
||||
quote_spanned! {
|
||||
name.span() => {
|
||||
#component_node
|
||||
#errors
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
// Make sure this a proper component path (uppercase ident, a path, or contains an underscorea)
|
||||
// This should be validated by the RsxBlock parser when it peeks bodynodes
|
||||
fn validate_component_path(&mut self) {
|
||||
let path = &self.name;
|
||||
|
||||
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(),
|
||||
// First, ensure the path is not a single lowercase ident with no underscores
|
||||
if path.segments.len() == 1 {
|
||||
let seg = path.segments.first().unwrap();
|
||||
if seg.ident.to_string().chars().next().unwrap().is_lowercase()
|
||||
&& !seg.ident.to_string().contains('_')
|
||||
{
|
||||
self.diagnostics.push(seg.ident.span().error(
|
||||
"Component names must be uppercase, contain an underscore, or abe a path.",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_component_path(path: &syn::Path) -> Result<()> {
|
||||
// ensure path segments doesn't have PathArguments, only the last
|
||||
// segment is allowed to have one.
|
||||
if path
|
||||
@@ -227,7 +137,9 @@ impl Component {
|
||||
.take(path.segments.len() - 1)
|
||||
.any(|seg| seg.arguments != PathArguments::None)
|
||||
{
|
||||
component_path_cannot_have_arguments!(path.span());
|
||||
self.diagnostics.push(path.span().error(
|
||||
"Component names must not have path arguments. Only the last segment is allowed to have one.",
|
||||
));
|
||||
}
|
||||
|
||||
// ensure last segment only have value of None or AngleBracketed
|
||||
@@ -235,183 +147,330 @@ impl Component {
|
||||
path.segments.last().unwrap().arguments,
|
||||
PathArguments::None | PathArguments::AngleBracketed(_)
|
||||
) {
|
||||
invalid_component_path!(path.span());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn key(&self) -> Option<&IfmtInput> {
|
||||
self.key.as_ref()
|
||||
}
|
||||
|
||||
fn collect_manual_props(&self, manual_props: &Expr) -> TokenStream2 {
|
||||
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_spanned! { manual_props.span() => __manual_props.#name = #content; },
|
||||
self.diagnostics.push(
|
||||
path.span()
|
||||
.error("Component names must have no arguments or angle bracketed arguments."),
|
||||
);
|
||||
}
|
||||
toks.append_all(quote_spanned! { manual_props.span() => __manual_props });
|
||||
quote! {{ #toks }}
|
||||
}
|
||||
|
||||
fn collect_props(&self) -> TokenStream2 {
|
||||
// Make sure the spread argument is being used as props spreading
|
||||
fn validate_component_spread(&mut self) {
|
||||
// Next, ensure that there's only one spread argument in the attributes *and* it's the last one
|
||||
for spread in self.spreads.iter().skip(1) {
|
||||
self.diagnostics.push(
|
||||
spread
|
||||
.expr
|
||||
.span()
|
||||
.error("Only one set of manual props is allowed for a component."),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure only one key and that the key is not a static str
|
||||
///
|
||||
/// todo: we want to allow arbitrary exprs for keys provided they impl hash / eq
|
||||
fn validate_key(&mut self) {
|
||||
let key = self.get_key();
|
||||
|
||||
if let Some(attr) = key {
|
||||
let diagnostic = match &attr.value {
|
||||
AttributeValue::AttrLiteral(ifmt) if ifmt.is_static() => {
|
||||
ifmt.span().error("Key must not be a static string. Make sure to use a formatted string like `key: \"{value}\"")
|
||||
}
|
||||
AttributeValue::AttrLiteral(_) => return,
|
||||
_ => attr
|
||||
.value
|
||||
.span()
|
||||
.error("Key must be in the form of a formatted string like `key: \"{value}\""),
|
||||
};
|
||||
|
||||
self.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_key(&self) -> Option<&Attribute> {
|
||||
self.fields
|
||||
.iter()
|
||||
.find(|attr| matches!(&attr.name, AttributeName::BuiltIn(key) if key == "key"))
|
||||
}
|
||||
|
||||
/// Ensure there's no duplicate props - this will be a compile error but we can move it to a
|
||||
/// diagnostic, thankfully
|
||||
///
|
||||
/// Also ensure there's no stringly typed propsa
|
||||
fn validate_fields(&mut self) {
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
for field in self.fields.iter() {
|
||||
match &field.name {
|
||||
AttributeName::Custom(name) => self.diagnostics.push(
|
||||
name.span()
|
||||
.error("Custom attributes are not supported for Components. Only known attributes are allowed."),
|
||||
),
|
||||
AttributeName::BuiltIn(k) => {
|
||||
if !seen.contains(k) {
|
||||
seen.insert(k);
|
||||
} else {
|
||||
self.diagnostics.push(
|
||||
k.span()
|
||||
.error("Duplicate prop field found. Only one prop field per name is allowed."),
|
||||
);
|
||||
}
|
||||
},
|
||||
AttributeName::Spread(_) => {
|
||||
unreachable!("Spread attributes should be handled in the spread validation step.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the tokens we'll use for the props of the component
|
||||
///
|
||||
/// todo: don't create the tokenstream from scratch and instead dump it into the existing streama
|
||||
fn create_props(&self) -> TokenStream2 {
|
||||
let manual_props = self.manual_props();
|
||||
|
||||
let name = &self.name;
|
||||
let generics = &self.generics;
|
||||
|
||||
let mut toks = match &self.prop_gen_args {
|
||||
Some(gen_args) => quote_spanned! { name.span() => fc_to_builder(#name #gen_args) },
|
||||
None => quote_spanned! { name.span() => fc_to_builder(#name) },
|
||||
let mut tokens = if let Some(props) = manual_props.as_ref() {
|
||||
quote! { let mut __manual_props = #props; }
|
||||
} else {
|
||||
quote! { fc_to_builder(#name #generics) }
|
||||
};
|
||||
for field in &self.fields {
|
||||
toks.append_all(quote! {#field})
|
||||
|
||||
for (name, value) in self.make_field_idents() {
|
||||
if manual_props.is_some() {
|
||||
tokens.append_all(quote! { __manual_props.#name = #value; })
|
||||
} else {
|
||||
tokens.append_all(quote! { .#name(#value) })
|
||||
}
|
||||
}
|
||||
|
||||
if !self.children.is_empty() {
|
||||
let renderer = TemplateRenderer::as_tokens(&self.children, None);
|
||||
toks.append_all(quote_spanned! { name.span() => .children( #renderer ) });
|
||||
let children = &self.children;
|
||||
if manual_props.is_some() {
|
||||
tokens.append_all(quote! { __manual_props.children = { #children }; })
|
||||
} else {
|
||||
tokens.append_all(quote! { .children( { #children } ) })
|
||||
}
|
||||
}
|
||||
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());
|
||||
if manual_props.is_some() {
|
||||
tokens.append_all(quote! { __manual_props })
|
||||
} else {
|
||||
tokens.append_all(quote! { .build() })
|
||||
}
|
||||
|
||||
tokens
|
||||
}
|
||||
}
|
||||
|
||||
// the struct's fields info
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
||||
pub struct ComponentField {
|
||||
pub name: Ident,
|
||||
pub content: ContentField,
|
||||
}
|
||||
fn manual_props(&self) -> Option<&Expr> {
|
||||
self.spreads.first().map(|spread| &spread.expr)
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
||||
pub enum ContentField {
|
||||
Shorthand(Ident),
|
||||
ManExpr(Expr),
|
||||
Formatted(IfmtInput),
|
||||
}
|
||||
fn make_field_idents(&self) -> Vec<(TokenStream2, TokenStream2)> {
|
||||
self.fields
|
||||
.iter()
|
||||
.filter_map(|attr| {
|
||||
let Attribute { name, value, .. } = attr;
|
||||
|
||||
impl ContentField {
|
||||
fn new(input: ParseStream) -> Result<Self> {
|
||||
if input.peek(LitStr) {
|
||||
let forked = input.fork();
|
||||
let t: LitStr = forked.parse()?;
|
||||
|
||||
// the string literal must either be the end of the input or a followed by a comma
|
||||
let res =
|
||||
match (forked.is_empty() || forked.peek(Token![,])) && is_literal_foramtted(&t) {
|
||||
true => ContentField::Formatted(input.parse()?),
|
||||
false => ContentField::ManExpr(input.parse()?),
|
||||
let attr = match name {
|
||||
AttributeName::BuiltIn(k) => {
|
||||
if k == "key" {
|
||||
return None;
|
||||
}
|
||||
quote! { #k }
|
||||
}
|
||||
AttributeName::Custom(_) => return None,
|
||||
AttributeName::Spread(_) => return None,
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
Ok(ContentField::ManExpr(input.parse()?))
|
||||
Some((attr, value.to_token_stream()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ContentField {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
match self {
|
||||
ContentField::Shorthand(i) => tokens.append_all(quote! { #i }),
|
||||
ContentField::ManExpr(e) => e.to_tokens(tokens),
|
||||
ContentField::Formatted(s) => tokens.append_all(quote! {
|
||||
#s
|
||||
}),
|
||||
fn fn_name(&self) -> Ident {
|
||||
self.name.segments.last().unwrap().ident.clone()
|
||||
}
|
||||
|
||||
fn empty(name: syn::Path, generics: Option<AngleBracketedGenericArguments>) -> Self {
|
||||
let mut diagnostics = Diagnostics::new();
|
||||
diagnostics.push(
|
||||
name.span()
|
||||
.error("Components must have a body")
|
||||
.help("Components must have a body, for example `Component {}`"),
|
||||
);
|
||||
Component {
|
||||
name,
|
||||
generics,
|
||||
brace: token::Brace::default(),
|
||||
fields: vec![],
|
||||
spreads: vec![],
|
||||
children: TemplateBody::new(vec![]),
|
||||
dyn_idx: DynIdx::default(),
|
||||
diagnostics,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for ComponentField {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let name = Ident::parse_any(input)?;
|
||||
|
||||
// if the next token is not a colon, then it's a shorthand field
|
||||
if input.parse::<Token![:]>().is_err() {
|
||||
return Ok(Self {
|
||||
content: ContentField::Shorthand(name.clone()),
|
||||
name,
|
||||
});
|
||||
};
|
||||
|
||||
let content = ContentField::new(input)?;
|
||||
|
||||
if input.peek(LitStr) || input.peek(Ident) {
|
||||
missing_trailing_comma!(content.span());
|
||||
}
|
||||
|
||||
Ok(Self { name, content })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ComponentField {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let ComponentField { name, content, .. } = self;
|
||||
tokens.append_all(quote! { .#name(#content) })
|
||||
}
|
||||
}
|
||||
|
||||
fn is_literal_foramtted(lit: &LitStr) -> bool {
|
||||
let s = lit.value();
|
||||
let mut chars = s.chars();
|
||||
|
||||
while let Some(next) = chars.next() {
|
||||
if next == '{' {
|
||||
let nen = chars.next();
|
||||
if nen != Some('{') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Normalize the generics of a path
|
||||
///
|
||||
/// Ensure there's a `::` after the last segment if there are generics
|
||||
fn normalize_path(name: &mut syn::Path) -> Option<AngleBracketedGenericArguments> {
|
||||
let seg = name.segments.last_mut()?;
|
||||
match seg.arguments.clone() {
|
||||
|
||||
let mut generics = match seg.arguments.clone() {
|
||||
PathArguments::AngleBracketed(args) => {
|
||||
seg.arguments = PathArguments::None;
|
||||
Some(args)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(generics) = generics.as_mut() {
|
||||
use syn::Token;
|
||||
generics.colon2_token = Some(Token));
|
||||
}
|
||||
|
||||
generics
|
||||
}
|
||||
|
||||
impl ComponentField {
|
||||
pub fn can_be_shorthand(&self) -> bool {
|
||||
// If it's a shorthand...
|
||||
if matches!(self.content, ContentField::Shorthand(_)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If it's in the form of attr: attr, return true
|
||||
if let ContentField::ManExpr(Expr::Path(path)) = &self.content {
|
||||
if path.path.segments.len() == 1 && path.path.segments[0].ident == self.name {
|
||||
return true;
|
||||
/// Ensure we can parse a component
|
||||
#[test]
|
||||
fn parses() {
|
||||
let input = quote! {
|
||||
MyComponent {
|
||||
key: "value {something}",
|
||||
prop: "value",
|
||||
..props,
|
||||
div {
|
||||
"Hello, world!"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
false
|
||||
}
|
||||
let component: Component = syn::parse2(input).unwrap();
|
||||
|
||||
dbg!(component);
|
||||
|
||||
let input_without_manual_props = quote! {
|
||||
MyComponent {
|
||||
key: "value {something}",
|
||||
prop: "value",
|
||||
div { "Hello, world!" }
|
||||
}
|
||||
};
|
||||
|
||||
let component: Component = syn::parse2(input_without_manual_props).unwrap();
|
||||
dbg!(component);
|
||||
}
|
||||
|
||||
/// Ensure we reject invalid forms
|
||||
///
|
||||
/// Maybe want to snapshot the errors?
|
||||
#[test]
|
||||
fn rejects() {
|
||||
let input = quote! {
|
||||
myComponent {
|
||||
key: "value",
|
||||
prop: "value",
|
||||
prop: "other",
|
||||
..props,
|
||||
..other_props,
|
||||
div {
|
||||
"Hello, world!"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let component: Component = syn::parse2(input).unwrap();
|
||||
dbg!(component.diagnostics);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_tokens_properly() {
|
||||
let input = quote! {
|
||||
MyComponent {
|
||||
key: "value {something}",
|
||||
prop: "value",
|
||||
prop: "value",
|
||||
prop: "value",
|
||||
prop: "value",
|
||||
prop: 123,
|
||||
..props,
|
||||
div { "Hello, world!" }
|
||||
}
|
||||
};
|
||||
|
||||
let component: Component = syn::parse2(input).unwrap();
|
||||
println!("{}", component.to_token_stream());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_tokens_no_manual_props() {
|
||||
let input_without_manual_props = quote! {
|
||||
MyComponent {
|
||||
key: "value {something}",
|
||||
named: "value {something}",
|
||||
prop: "value",
|
||||
count: 1,
|
||||
div { "Hello, world!" }
|
||||
}
|
||||
};
|
||||
let component: Component = syn::parse2(input_without_manual_props).unwrap();
|
||||
println!("{}", component.to_token_stream().pretty_unparse());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generics_params() {
|
||||
let input_without_children = quote! {
|
||||
Outlet::<R> {}
|
||||
};
|
||||
let component: CallBody = syn::parse2(input_without_children).unwrap();
|
||||
println!("{}", component.to_token_stream().pretty_unparse());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generics_no_fish() {
|
||||
let name = quote! { Outlet<R> };
|
||||
let mut p = syn::parse2::<syn::Path>(name).unwrap();
|
||||
let generics = normalize_path(&mut p);
|
||||
assert!(generics.is_some());
|
||||
|
||||
let input_without_children = quote! {
|
||||
div {
|
||||
Component<Generic> {}
|
||||
}
|
||||
};
|
||||
let component: BodyNode = syn::parse2(input_without_children).unwrap();
|
||||
println!("{}", component.to_token_stream().pretty_unparse());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fmt_passes_properly() {
|
||||
let input = quote! {
|
||||
Link { to: Route::List, class: "pure-button", "Go back" }
|
||||
};
|
||||
|
||||
let component: Component = syn::parse2(input).unwrap();
|
||||
|
||||
println!("{}", component.to_token_stream().pretty_unparse());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_components() {
|
||||
let input = quote::quote! {
|
||||
some::cool::Component
|
||||
};
|
||||
|
||||
let _parsed: Component = syn::parse2(input).unwrap();
|
||||
|
||||
let input = quote::quote! {
|
||||
some::cool::C
|
||||
};
|
||||
|
||||
let _parsed: syn::Path = syn::parse2(input).unwrap();
|
||||
}
|
||||
@@ -1,431 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::*;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
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
|
||||
#[derive(Default, Debug)]
|
||||
pub struct DynamicContext<'a> {
|
||||
pub dynamic_nodes: Vec<&'a BodyNode>,
|
||||
pub dynamic_attributes: Vec<Vec<&'a AttributeType>>,
|
||||
pub current_path: Vec<u8>,
|
||||
pub node_paths: Vec<Vec<u8>>,
|
||||
pub attr_paths: Vec<Vec<u8>>,
|
||||
|
||||
/// Mapping variables used to map the old template to the new template
|
||||
///
|
||||
/// This tracks whether or not we're tracking some nodes or attributes
|
||||
/// If we're tracking, then we'll attempt to use the old mapping
|
||||
is_tracking: bool,
|
||||
|
||||
/// The mapping of node to its index in the dynamic_nodes list
|
||||
/// We use the fact that BodyNode is Hash/PartialEq to track the nodes when we run into them
|
||||
node_to_idx: HashMap<BodyNode, Vec<usize>>,
|
||||
last_element_idx: usize,
|
||||
|
||||
/// The mapping of attribute to its index in the dynamic_attributes list
|
||||
/// We use the fact that AttributeType is Hash/PartialEq to track the attributes when we run into them
|
||||
attribute_to_idx: HashMap<AttributeType, Vec<usize>>,
|
||||
last_attribute_idx: usize,
|
||||
}
|
||||
|
||||
impl<'a> DynamicContext<'a> {
|
||||
pub fn new_with_old(template: Option<CallBody>) -> Self {
|
||||
let mut new = Self::default();
|
||||
|
||||
if let Some(call) = template {
|
||||
for node in call.roots {
|
||||
new.track_node(node);
|
||||
}
|
||||
new.is_tracking = true;
|
||||
}
|
||||
|
||||
new
|
||||
}
|
||||
|
||||
/// Populate the dynamic context with our own roots
|
||||
///
|
||||
/// This will call update_node on each root, attempting to build us a list of TemplateNodes that
|
||||
/// we can render out.
|
||||
///
|
||||
/// These TemplateNodes are the same one used in Dioxus core! We just serialize them out and then
|
||||
/// they'll get picked up after codegen for compilation. Cool stuff.
|
||||
///
|
||||
/// If updating fails (IE the root is a dynamic node that has changed), then we return None.
|
||||
#[cfg(feature = "hot_reload")]
|
||||
pub fn populate_by_updating<Ctx: HotReloadingContext>(
|
||||
&mut self,
|
||||
roots: &'a [BodyNode],
|
||||
) -> Option<Vec<TemplateNode>> {
|
||||
// Create a list of new roots that we'll spit out
|
||||
let mut roots_ = Vec::new();
|
||||
|
||||
// Populate the dynamic context with our own roots
|
||||
for (idx, root) in roots.iter().enumerate() {
|
||||
self.current_path.push(idx as u8);
|
||||
roots_.push(self.update_node::<Ctx>(root)?);
|
||||
self.current_path.pop();
|
||||
}
|
||||
|
||||
Some(roots_)
|
||||
}
|
||||
|
||||
/// Render a portion of an rsx callbody to a TemplateNode call
|
||||
///
|
||||
/// We're assembling the templatenodes
|
||||
pub fn render_static_node(&mut self, root: &'a BodyNode) -> TokenStream2 {
|
||||
match root {
|
||||
BodyNode::Element(el) => self.render_static_element(el),
|
||||
|
||||
BodyNode::Text(text) if text.is_static() => {
|
||||
let text = text.to_static().unwrap();
|
||||
quote! { dioxus_core::TemplateNode::Text { text: #text } }
|
||||
}
|
||||
|
||||
BodyNode::ForLoop(for_loop) => self.render_for_loop(root, for_loop),
|
||||
|
||||
BodyNode::RawExpr(_)
|
||||
| BodyNode::Text(_)
|
||||
| BodyNode::IfChain(_)
|
||||
| BodyNode::Component(_) => self.render_dynamic_node(root),
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a for loop to a token stream
|
||||
///
|
||||
/// This is basically just rendering a dynamic node, but with some extra bookkepping to track the
|
||||
/// contents of the for loop in case we want to hot reload it
|
||||
fn render_for_loop(&mut self, root: &'a BodyNode, _for_loop: &ForLoop) -> TokenStream2 {
|
||||
self.render_dynamic_node(root)
|
||||
}
|
||||
|
||||
fn render_dynamic_node(&mut self, root: &'a BodyNode) -> TokenStream2 {
|
||||
let ct = self.dynamic_nodes.len();
|
||||
self.dynamic_nodes.push(root);
|
||||
self.node_paths.push(self.current_path.clone());
|
||||
quote! { dioxus_core::TemplateNode::Dynamic { id: #ct } }
|
||||
}
|
||||
|
||||
fn render_static_element(&mut self, el: &'a Element) -> TokenStream2 {
|
||||
let el_name = &el.name;
|
||||
let ns = el_name.namespace();
|
||||
let span = el_name.span();
|
||||
|
||||
let static_attrs = el
|
||||
.merged_attributes
|
||||
.iter()
|
||||
.map(|attr| self.render_merged_attributes(attr, el_name))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let children = el
|
||||
.children
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, root)| self.render_children_nodes(idx, root))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let el_name = el_name.tag_name();
|
||||
let completion_hints = el.completion_hints();
|
||||
let errors = el.errors();
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_children_nodes(&mut self, idx: usize, root: &'a BodyNode) -> TokenStream2 {
|
||||
self.current_path.push(idx as u8);
|
||||
let out = self.render_static_node(root);
|
||||
self.current_path.pop();
|
||||
out
|
||||
}
|
||||
|
||||
/// Render the attributes of an element
|
||||
fn render_merged_attributes(
|
||||
&mut self,
|
||||
attr: &'a AttributeType,
|
||||
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, 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
|
||||
_ => self.render_dynamic_attr(attr),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_static_attr(
|
||||
value: &IfmtInput,
|
||||
name: &ElementAttrName,
|
||||
el_name: &ElementName,
|
||||
) -> TokenStream2 {
|
||||
let value = value.to_static().unwrap();
|
||||
|
||||
let ns = match name {
|
||||
ElementAttrName::BuiltIn(name) => quote! { #el_name::#name.1 },
|
||||
ElementAttrName::Custom(_) => quote!(None),
|
||||
};
|
||||
|
||||
let name = match (el_name, name) {
|
||||
(ElementName::Ident(_), ElementAttrName::BuiltIn(_)) => quote! { #el_name::#name.0 },
|
||||
_ => {
|
||||
//hmmmm I think we could just totokens this, but the to_string might be inserting quotes
|
||||
let as_string = name.to_string();
|
||||
quote! { #as_string }
|
||||
}
|
||||
};
|
||||
|
||||
quote! {
|
||||
dioxus_core::TemplateAttribute::Static {
|
||||
name: #name,
|
||||
namespace: #ns,
|
||||
value: #value,
|
||||
|
||||
// todo: we don't diff these so we never apply the volatile flag
|
||||
// volatile: dioxus_elements::#el_name::#name.2,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// If the attr is dynamic, we save it to the tracked attributes list
|
||||
/// This will let us use this context at a later point in time to update the template
|
||||
fn render_dynamic_attr(&mut self, attr: &'a AttributeType) -> TokenStream2 {
|
||||
let ct = self.dynamic_attributes.len();
|
||||
|
||||
self.dynamic_attributes.push(vec![attr]);
|
||||
self.attr_paths.push(self.current_path.clone());
|
||||
|
||||
quote! { dioxus_core::TemplateAttribute::Dynamic { id: #ct }, }
|
||||
}
|
||||
|
||||
#[cfg(feature = "hot_reload")]
|
||||
pub fn update_node<Ctx: HotReloadingContext>(
|
||||
&mut self,
|
||||
root: &'a BodyNode,
|
||||
) -> Option<TemplateNode> {
|
||||
match root {
|
||||
// The user is moving a static node around in the template
|
||||
// Check this is a bit more complex but we can likely handle it
|
||||
BodyNode::Element(el) => self.update_element::<Ctx>(el),
|
||||
|
||||
BodyNode::Text(text) if text.is_static() => {
|
||||
let text = text.source.as_ref().unwrap();
|
||||
let text = intern(text.value().as_str());
|
||||
Some(TemplateNode::Text { text })
|
||||
}
|
||||
|
||||
// The user is moving a dynamic node around in the template
|
||||
// We *might* be able to handle it, but you never really know
|
||||
BodyNode::RawExpr(_)
|
||||
| BodyNode::Text(_)
|
||||
| BodyNode::ForLoop(_)
|
||||
| BodyNode::IfChain(_)
|
||||
| BodyNode::Component(_) => self.update_dynamic_node(root),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to update a dynamic node in the template
|
||||
///
|
||||
/// If the change between the old and new template results in a mapping that doesn't exist, then we need to bail out.
|
||||
/// Basically if we *had* a mapping of `[0, 1]` and the new template is `[1, 2]`, then we need to bail out, since
|
||||
/// the new mapping doesn't exist in the original.
|
||||
#[cfg(feature = "hot_reload")]
|
||||
fn update_dynamic_node(&mut self, root: &'a BodyNode) -> Option<TemplateNode> {
|
||||
let idx = match self.has_tracked_nodes() {
|
||||
// Bail out if the mapping doesn't exist
|
||||
// The user put it new code in the template, and that code is not hotreloadable
|
||||
true => self.tracked_node_idx(root)?,
|
||||
false => self.dynamic_nodes.len(),
|
||||
};
|
||||
|
||||
// Put the node in the dynamic nodes list
|
||||
self.dynamic_nodes.push(root);
|
||||
|
||||
// Fill in as many paths as we need - might have to fill in more since the old tempate shrunk some and let some paths be empty
|
||||
if self.node_paths.len() <= idx {
|
||||
self.node_paths.resize_with(idx + 1, Vec::new);
|
||||
}
|
||||
|
||||
// And then set the path of this node to the current path (which we're hitting during traversal)
|
||||
self.node_paths[idx].clone_from(&self.current_path);
|
||||
|
||||
Some(match root {
|
||||
BodyNode::Text(_) => TemplateNode::Dynamic { id: idx },
|
||||
_ => TemplateNode::Dynamic { id: idx },
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "hot_reload")]
|
||||
fn update_element<Ctx: HotReloadingContext>(
|
||||
&mut self,
|
||||
el: &'a Element,
|
||||
) -> Option<TemplateNode> {
|
||||
let rust_name = el.name.to_string();
|
||||
|
||||
let mut static_attr_array = Vec::new();
|
||||
|
||||
for attr in &el.merged_attributes {
|
||||
let template_attr = match attr.as_static_str_literal() {
|
||||
// For static attributes, we don't need to pull in any mapping or anything
|
||||
// We can just build them directly
|
||||
Some((name, value)) => Self::make_static_attribute::<Ctx>(value, name, &rust_name),
|
||||
|
||||
// For dynamic attributes, we need to check the mapping to see if that mapping exists
|
||||
// todo: one day we could generate new dynamic attributes on the fly if they're a literal,
|
||||
// or something sufficiently serializable
|
||||
// (ie `checked`` being a bool and bools being interpretable)
|
||||
//
|
||||
// For now, just give up if that attribute doesn't exist in the mapping
|
||||
None => {
|
||||
let id = self.update_dynamic_attribute(attr)?;
|
||||
TemplateAttribute::Dynamic { id }
|
||||
}
|
||||
};
|
||||
|
||||
static_attr_array.push(template_attr);
|
||||
}
|
||||
|
||||
let children = self.populate_by_updating::<Ctx>(el.children.as_slice())?;
|
||||
|
||||
let (tag, namespace) =
|
||||
Ctx::map_element(&rust_name).unwrap_or((intern(rust_name.as_str()), None));
|
||||
|
||||
Some(TemplateNode::Element {
|
||||
tag,
|
||||
namespace,
|
||||
attrs: intern(static_attr_array.into_boxed_slice()),
|
||||
children: intern(children.as_slice()),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "hot_reload")]
|
||||
fn update_dynamic_attribute(&mut self, attr: &'a AttributeType) -> Option<usize> {
|
||||
let idx = match self.has_tracked_nodes() {
|
||||
true => self.tracked_attribute_idx(attr)?,
|
||||
false => self.dynamic_attributes.len(),
|
||||
};
|
||||
|
||||
self.dynamic_attributes.push(vec![attr]);
|
||||
if self.attr_paths.len() <= idx {
|
||||
self.attr_paths.resize_with(idx + 1, Vec::new);
|
||||
}
|
||||
|
||||
self.attr_paths[idx].clone_from(&self.current_path);
|
||||
|
||||
Some(idx)
|
||||
}
|
||||
|
||||
#[cfg(feature = "hot_reload")]
|
||||
fn make_static_attribute<Ctx: HotReloadingContext>(
|
||||
value: &IfmtInput,
|
||||
name: &ElementAttrName,
|
||||
element_name_rust: &str,
|
||||
) -> TemplateAttribute {
|
||||
let value = value.source.as_ref().unwrap();
|
||||
let attribute_name_rust = name.to_string();
|
||||
let (name, namespace) = Ctx::map_attribute(element_name_rust, &attribute_name_rust)
|
||||
.unwrap_or((intern(attribute_name_rust.as_str()), None));
|
||||
|
||||
let static_attr = TemplateAttribute::Static {
|
||||
name,
|
||||
namespace,
|
||||
value: intern(value.value().as_str()),
|
||||
};
|
||||
|
||||
static_attr
|
||||
}
|
||||
|
||||
#[cfg(feature = "hot_reload")]
|
||||
/// Check if we're tracking any nodes
|
||||
///
|
||||
/// If we're tracking, then we'll attempt to use the old mapping
|
||||
fn has_tracked_nodes(&self) -> bool {
|
||||
self.is_tracking
|
||||
}
|
||||
|
||||
/// Track a BodyNode
|
||||
///
|
||||
/// This will save the any dynamic nodes that we find.
|
||||
/// We need to be careful around saving if/for/components since we want to hotreload their contents
|
||||
/// provided that their rust portions haven't changed.
|
||||
pub(crate) fn track_node(&mut self, node: BodyNode) {
|
||||
match node {
|
||||
// If the node is a static element, we just want to merge its attributes into the dynamic mapping
|
||||
BodyNode::Element(el) => self.track_element(el),
|
||||
|
||||
// We skip static nodes since they'll get written out by the template during the diffing phase
|
||||
BodyNode::Text(text) if text.is_static() => {}
|
||||
|
||||
BodyNode::RawExpr(_)
|
||||
| BodyNode::Text(_)
|
||||
| BodyNode::ForLoop(_)
|
||||
| BodyNode::IfChain(_)
|
||||
| BodyNode::Component(_) => {
|
||||
self.track_dynamic_node(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn track_element(&mut self, el: Element) {
|
||||
for attr in el.merged_attributes {
|
||||
// If the attribute is a static string literal, we don't need to insert it since the attribute
|
||||
// will be written out during the diffing phase (since it's static)
|
||||
if !attr.is_static_str_literal() {
|
||||
self.track_attribute(attr);
|
||||
}
|
||||
}
|
||||
|
||||
for child in el.children {
|
||||
self.track_node(child);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn track_attribute(&mut self, attr: AttributeType) -> usize {
|
||||
let idx = self.last_attribute_idx;
|
||||
self.last_attribute_idx += 1;
|
||||
self.attribute_to_idx.entry(attr).or_default().push(idx);
|
||||
idx
|
||||
}
|
||||
|
||||
pub(crate) fn track_dynamic_node(&mut self, node: BodyNode) -> usize {
|
||||
let idx = self.last_element_idx;
|
||||
self.last_element_idx += 1;
|
||||
self.node_to_idx.entry(node).or_default().push(idx);
|
||||
idx
|
||||
}
|
||||
|
||||
#[cfg(feature = "hot_reload")]
|
||||
pub(crate) fn tracked_attribute_idx(&mut self, attr: &AttributeType) -> Option<usize> {
|
||||
self.attribute_to_idx
|
||||
.get_mut(attr)
|
||||
.and_then(|idxs| idxs.pop())
|
||||
}
|
||||
|
||||
#[cfg(feature = "hot_reload")]
|
||||
pub(crate) fn tracked_node_idx(&mut self, node: &BodyNode) -> Option<usize> {
|
||||
self.node_to_idx.get_mut(node).and_then(|idxs| idxs.pop())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use proc_macro2_diagnostics::Diagnostic;
|
||||
use quote::ToTokens;
|
||||
|
||||
/// A collection of diagnostics
|
||||
///
|
||||
/// This is a wrapper type since we want it to be transparent in terms of PartialEq and Eq.
|
||||
/// This also lets us choose the expansion strategy for the diagnostics.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Diagnostics {
|
||||
pub diagnostics: Vec<Diagnostic>,
|
||||
}
|
||||
|
||||
impl Diagnostics {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
diagnostics: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, diagnostic: Diagnostic) {
|
||||
self.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, diagnostics: Vec<Diagnostic>) {
|
||||
self.diagnostics.extend(diagnostics);
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.diagnostics.is_empty()
|
||||
}
|
||||
|
||||
pub fn into_diagnostics(self) -> Vec<Diagnostic> {
|
||||
self.diagnostics
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.diagnostics.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Diagnostics {
|
||||
fn eq(&self, _other: &Self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Diagnostics {}
|
||||
|
||||
impl ToTokens for Diagnostics {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
for diagnostic in &self.diagnostics {
|
||||
tokens.extend(diagnostic.clone().emit_as_expr_tokens());
|
||||
}
|
||||
}
|
||||
}
|
||||
+503
-403
@@ -1,389 +1,298 @@
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use crate::errors::missing_trailing_comma;
|
||||
|
||||
use self::util::try_parse_braces;
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::innerlude::*;
|
||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||
use quote::quote;
|
||||
use proc_macro2_diagnostics::SpanDiagnosticExt;
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use syn::{
|
||||
ext::IdentExt, punctuated::Punctuated, spanned::Spanned, token::Brace, Expr, Ident, LitStr,
|
||||
Token,
|
||||
parse::{Parse, ParseStream},
|
||||
punctuated::Punctuated,
|
||||
spanned::Spanned,
|
||||
token::Brace,
|
||||
Ident, LitStr, Result, Token,
|
||||
};
|
||||
|
||||
// =======================================
|
||||
// Parse the VNode::Element type
|
||||
// =======================================
|
||||
#[derive(Clone, Debug)]
|
||||
/// Parse the VNode::Element type
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub struct Element {
|
||||
/// div { } -> div
|
||||
pub name: ElementName,
|
||||
pub key: Option<IfmtInput>,
|
||||
pub attributes: Vec<AttributeType>,
|
||||
pub merged_attributes: Vec<AttributeType>,
|
||||
|
||||
/// The actual attributes that were parsed
|
||||
pub raw_attributes: Vec<Attribute>,
|
||||
|
||||
/// The attributes after merging - basically the formatted version of the combined attributes
|
||||
/// where possible.
|
||||
///
|
||||
/// These are the actual attributes that get rendered out
|
||||
pub merged_attributes: Vec<Attribute>,
|
||||
|
||||
/// The `...` spread attributes.
|
||||
pub spreads: Vec<Spread>,
|
||||
|
||||
// /// Elements can have multiple, unlike components which can only have one
|
||||
// pub spreads: Vec<Spread>,
|
||||
/// The children of the element
|
||||
pub children: Vec<BodyNode>,
|
||||
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
|
||||
}
|
||||
}
|
||||
/// the brace of the `div { }`
|
||||
pub brace: 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 {
|
||||
/// Create a new element with the given name, attributes and children
|
||||
pub fn new(
|
||||
key: Option<IfmtInput>,
|
||||
name: ElementName,
|
||||
attributes: Vec<AttributeType>,
|
||||
children: Vec<BodyNode>,
|
||||
brace: syn::token::Brace,
|
||||
) -> Self {
|
||||
// Deduplicate any attributes that can be combined
|
||||
// For example, if there are two `class` attributes, combine them into one
|
||||
let mut merged_attributes: Vec<AttributeType> = Vec::new();
|
||||
for attr in &attributes {
|
||||
let attr_index = merged_attributes
|
||||
.iter()
|
||||
.position(|a| a.matches_attr_name(attr));
|
||||
|
||||
if let Some(old_attr_index) = attr_index {
|
||||
let old_attr = &mut merged_attributes[old_attr_index];
|
||||
|
||||
if let Some(combined) = old_attr.try_combine(attr) {
|
||||
*old_attr = combined;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
merged_attributes.push(attr.clone());
|
||||
}
|
||||
|
||||
Self {
|
||||
name,
|
||||
key,
|
||||
attributes,
|
||||
merged_attributes,
|
||||
children,
|
||||
brace: Some(brace),
|
||||
errors: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 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
|
||||
// start parsing bodynodes
|
||||
// "def": 456,
|
||||
// abc: 123,
|
||||
loop {
|
||||
if content.peek(Token![..]) {
|
||||
content.parse::<Token![..]>()?;
|
||||
let expr = content.parse::<Expr>()?;
|
||||
let span = expr.span();
|
||||
attributes.push(attribute::AttributeType::Spread(expr));
|
||||
|
||||
if content.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
if content.parse::<Token![,]>().is_err() {
|
||||
accumulate_or_return_error!(missing_trailing_comma(span));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse the raw literal fields
|
||||
// "def": 456,
|
||||
if content.peek(LitStr) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
|
||||
let name = content.parse::<LitStr>()?;
|
||||
let ident = name.clone();
|
||||
|
||||
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 !followed_by_comma {
|
||||
accumulate_or_return_error!(missing_trailing_comma(ident.span()));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse
|
||||
// abc: 123,
|
||||
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![:]>()?;
|
||||
|
||||
// The span of the content to be parsed,
|
||||
// for example the `hi` part of `class: "hi"`.
|
||||
let span = content.span();
|
||||
|
||||
if name_str == "key" {
|
||||
let _key: IfmtInput = content.parse()?;
|
||||
|
||||
if _key.is_static() {
|
||||
invalid_key!(_key);
|
||||
}
|
||||
|
||||
key = Some(_key);
|
||||
} else {
|
||||
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::built_in(&name),
|
||||
value,
|
||||
},
|
||||
followed_by_comma: content.peek(Token![,]),
|
||||
}));
|
||||
}
|
||||
|
||||
if content.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
if content.parse::<Token![,]>().is_err() {
|
||||
accumulate_or_return_error!(missing_trailing_comma(span));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse shorthand fields
|
||||
if peek_any_ident(&content)
|
||||
&& !content.peek2(Brace)
|
||||
&& !content.peek2(Token![:])
|
||||
&& !content.peek2(Token![-])
|
||||
{
|
||||
let name = Ident::parse_any(&content)?;
|
||||
let name_ = name.clone();
|
||||
|
||||
// If the shorthand field is children, these are actually children!
|
||||
if name == "children" {
|
||||
return Err(syn::Error::new(
|
||||
name.span(),
|
||||
r#"Shorthand element children are not supported.
|
||||
To pass children into elements, wrap them in curly braces.
|
||||
Like so:
|
||||
div { {children} }
|
||||
|
||||
"#,
|
||||
));
|
||||
};
|
||||
|
||||
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::built_in(&name),
|
||||
value,
|
||||
},
|
||||
followed_by_comma,
|
||||
}));
|
||||
|
||||
if content.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
if !followed_by_comma {
|
||||
accumulate_or_return_error!(missing_trailing_comma(name_.span()));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
while !content.is_empty() {
|
||||
if ((content.peek(Ident) || content.peek(LitStr)) && content.peek2(Token![:]))
|
||||
&& !content.peek3(Token![:])
|
||||
{
|
||||
attr_after_element!(content.span());
|
||||
}
|
||||
|
||||
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![,]) {
|
||||
let _ = content.parse::<Token![,]>();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
/// A list of diagnostics that were generated during parsing. This element might be a valid rsx_block
|
||||
/// but not technically a valid element - these diagnostics tell us what's wrong and then are used
|
||||
/// when rendering
|
||||
pub diagnostics: Diagnostics,
|
||||
}
|
||||
|
||||
impl Parse for Element {
|
||||
fn parse(stream: ParseStream) -> Result<Self> {
|
||||
Self::parse_with_options(stream, true)
|
||||
let name = stream.parse::<ElementName>()?;
|
||||
|
||||
let RsxBlock {
|
||||
attributes: mut fields,
|
||||
children,
|
||||
brace,
|
||||
spreads,
|
||||
diagnostics,
|
||||
} = stream.parse::<RsxBlock>()?;
|
||||
|
||||
// Make sure these attributes have an el_name set for completions and Template generation
|
||||
for attr in fields.iter_mut() {
|
||||
attr.el_name = Some(name.clone());
|
||||
}
|
||||
|
||||
let mut element = Element {
|
||||
name,
|
||||
raw_attributes: fields,
|
||||
children,
|
||||
brace,
|
||||
diagnostics,
|
||||
spreads: spreads.clone(),
|
||||
merged_attributes: Vec::new(),
|
||||
};
|
||||
|
||||
element.merge_attributes();
|
||||
|
||||
for spread in spreads.iter() {
|
||||
element.merged_attributes.push(Attribute {
|
||||
name: AttributeName::Spread(spread.dots),
|
||||
colon: None,
|
||||
value: AttributeValue::AttrExpr(PartialExpr::from_expr(&spread.expr)),
|
||||
comma: spread.comma,
|
||||
dyn_idx: spread.dyn_idx.clone(),
|
||||
el_name: None,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(element)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Element {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let el = self;
|
||||
let el_name = &el.name;
|
||||
|
||||
let ns = |name| match el_name {
|
||||
ElementName::Ident(i) => quote! { dioxus_elements::#i::#name },
|
||||
ElementName::Custom(_) => quote! { None },
|
||||
};
|
||||
|
||||
let static_attrs = el
|
||||
.merged_attributes
|
||||
.iter()
|
||||
.map(|attr| {
|
||||
// Rendering static attributes requires a bit more work than just a dynamic attrs
|
||||
// Early return for dynamic attributes
|
||||
let Some((name, value)) = attr.as_static_str_literal() else {
|
||||
let id = attr.dyn_idx.get();
|
||||
return quote! { dioxus_core::TemplateAttribute::Dynamic { id: #id } };
|
||||
};
|
||||
|
||||
let ns = match name {
|
||||
AttributeName::BuiltIn(name) => ns(quote!(#name.1)),
|
||||
AttributeName::Custom(_) => quote!(None),
|
||||
AttributeName::Spread(_) => {
|
||||
unreachable!("spread attributes should not be static")
|
||||
}
|
||||
};
|
||||
|
||||
let name = match (el_name, name) {
|
||||
(ElementName::Ident(_), AttributeName::BuiltIn(_)) => {
|
||||
quote! { dioxus_elements::#el_name::#name.0 }
|
||||
}
|
||||
//hmmmm I think we could just totokens this, but the to_string might be inserting quotes
|
||||
_ => {
|
||||
let as_string = name.to_string();
|
||||
quote! { #as_string }
|
||||
}
|
||||
};
|
||||
|
||||
let value = value.to_static().unwrap();
|
||||
|
||||
quote! {
|
||||
dioxus_core::TemplateAttribute::Static {
|
||||
name: #name,
|
||||
namespace: #ns,
|
||||
value: #value,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Render either the child
|
||||
let children = el.children.iter().map(|c| match c {
|
||||
BodyNode::Element(el) => quote! { #el },
|
||||
BodyNode::Text(text) if text.is_static() => {
|
||||
let text = text.input.to_static().unwrap();
|
||||
quote! { dioxus_core::TemplateNode::Text { text: #text } }
|
||||
}
|
||||
BodyNode::Text(text) => {
|
||||
let id = text.dyn_idx.get();
|
||||
quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
|
||||
}
|
||||
BodyNode::ForLoop(floop) => {
|
||||
let id = floop.dyn_idx.get();
|
||||
quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
|
||||
}
|
||||
BodyNode::RawExpr(exp) => {
|
||||
let id = exp.dyn_idx.get();
|
||||
quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
|
||||
}
|
||||
BodyNode::Component(exp) => {
|
||||
let id = exp.dyn_idx.get();
|
||||
quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
|
||||
}
|
||||
BodyNode::IfChain(exp) => {
|
||||
let id = exp.dyn_idx.get();
|
||||
quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
|
||||
}
|
||||
});
|
||||
|
||||
let ns = ns(quote!(NAME_SPACE));
|
||||
let el_name = el_name.tag_name();
|
||||
let diagnostics = &el.diagnostics;
|
||||
|
||||
// todo: generate less code if there's no diagnostics by not including the curlies
|
||||
tokens.append_all(quote! {
|
||||
{
|
||||
#diagnostics
|
||||
|
||||
dioxus_core::TemplateNode::Element {
|
||||
tag: #el_name,
|
||||
namespace: #ns,
|
||||
attrs: &[ #(#static_attrs),* ],
|
||||
children: &[ #(#children),* ],
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Element {
|
||||
/// Collapses ifmt attributes into a single dynamic attribute using a space or `;` as a delimiter
|
||||
///
|
||||
/// ```ignore,
|
||||
/// div {
|
||||
/// class: "abc-def",
|
||||
/// class: if some_expr { "abc" },
|
||||
/// }
|
||||
/// ```
|
||||
fn merge_attributes(&mut self) {
|
||||
let mut attrs: Vec<&Attribute> = vec![];
|
||||
|
||||
for attr in &self.raw_attributes {
|
||||
if attrs.iter().any(|old_attr| old_attr.name == attr.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
attrs.push(attr);
|
||||
}
|
||||
|
||||
for attr in attrs {
|
||||
if attr.name.to_string() == "key" {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect all the attributes with the same name
|
||||
let matching_attrs = self
|
||||
.raw_attributes
|
||||
.iter()
|
||||
.filter(|a| a.name == attr.name)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// if there's only one attribute with this name, then we don't need to merge anything
|
||||
if matching_attrs.len() == 1 {
|
||||
self.merged_attributes.push(attr.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
// If there are multiple attributes with the same name, then we need to merge them
|
||||
// This will be done by creating an ifmt attribute that combines all the segments
|
||||
// We might want to throw a diagnostic of trying to merge things together that might not
|
||||
// make a whole lot of sense - like merging two exprs together
|
||||
let mut out = IfmtInput::new(attr.span());
|
||||
|
||||
for (idx, matching_attr) in matching_attrs.iter().enumerate() {
|
||||
// If this is the first attribute, then we don't need to add a delimiter
|
||||
if idx != 0 {
|
||||
// FIXME: I don't want to special case anything - but our delimiter is special cased to a space
|
||||
// We really don't want to special case anything in the macro, but the hope here is that
|
||||
// multiline strings can be merged with a space
|
||||
out.push_raw_str(" ".to_string());
|
||||
}
|
||||
|
||||
// Merge raw literals into the output
|
||||
if let AttributeValue::AttrLiteral(lit) = &matching_attr.value {
|
||||
if let HotLiteralType::Fmted(new) = &lit.value {
|
||||
out.push_ifmt(new.clone());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Merge `if cond { "abc" }` into the output
|
||||
if let AttributeValue::AttrOptionalExpr { condition, value } = &matching_attr.value
|
||||
{
|
||||
if let AttributeValue::AttrLiteral(lit) = value.as_ref() {
|
||||
if let HotLiteralType::Fmted(new) = &lit.value {
|
||||
out.push_condition(condition.clone(), new.clone());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unwind in case there's a test or two that cares about this weird state
|
||||
_ = out.segments.pop();
|
||||
self.diagnostics.push(matching_attr.span().error("Cannot merge non-fmt literals").help(
|
||||
"Only formatted strings can be merged together. If you want to merge literals, you can use a format string.",
|
||||
));
|
||||
}
|
||||
|
||||
let out_lit = HotLiteral {
|
||||
value: HotLiteralType::Fmted(out),
|
||||
hr_idx: Default::default(),
|
||||
};
|
||||
|
||||
self.merged_attributes.push(Attribute {
|
||||
name: attr.name.clone(),
|
||||
value: AttributeValue::AttrLiteral(out_lit),
|
||||
colon: attr.colon,
|
||||
dyn_idx: attr.dyn_idx.clone(),
|
||||
comma: matching_attrs.last().unwrap().comma,
|
||||
el_name: attr.el_name.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn key(&self) -> Option<&IfmtInput> {
|
||||
for attr in &self.raw_attributes {
|
||||
if let AttributeName::BuiltIn(name) = &attr.name {
|
||||
if name == "key" {
|
||||
return attr.ifmt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +302,33 @@ pub enum ElementName {
|
||||
Custom(LitStr),
|
||||
}
|
||||
|
||||
impl ToTokens for ElementName {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
match self {
|
||||
ElementName::Ident(i) => tokens.append_all(quote! { #i }),
|
||||
ElementName::Custom(s) => s.to_tokens(tokens),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for ElementName {
|
||||
fn parse(stream: ParseStream) -> Result<Self> {
|
||||
let raw = Punctuated::<Ident, Token![-]>::parse_separated_nonempty(stream)?;
|
||||
if raw.len() == 1 {
|
||||
Ok(ElementName::Ident(raw.into_iter().next().unwrap()))
|
||||
} else {
|
||||
let span = raw.span();
|
||||
let tag = raw
|
||||
.into_iter()
|
||||
.map(|ident| ident.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("-");
|
||||
let tag = LitStr::new(&tag, span);
|
||||
Ok(ElementName::Custom(tag))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ElementName {
|
||||
pub(crate) fn tag_name(&self) -> TokenStream2 {
|
||||
match self {
|
||||
@@ -401,15 +337,6 @@ impl ElementName {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn namespace(&self) -> TokenStream2 {
|
||||
match self {
|
||||
ElementName::Ident(i) => quote! { dioxus_elements::elements::#i::NAME_SPACE },
|
||||
ElementName::Custom(_) => quote! { None },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ElementName {
|
||||
pub fn span(&self) -> Span {
|
||||
match self {
|
||||
ElementName::Ident(i) => i.span(),
|
||||
@@ -436,29 +363,202 @@ impl Display for ElementName {
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for ElementName {
|
||||
fn parse(stream: ParseStream) -> Result<Self> {
|
||||
let raw = Punctuated::<Ident, Token![-]>::parse_separated_nonempty(stream)?;
|
||||
if raw.len() == 1 {
|
||||
Ok(ElementName::Ident(raw.into_iter().next().unwrap()))
|
||||
} else {
|
||||
let span = raw.span();
|
||||
let tag = raw
|
||||
.into_iter()
|
||||
.map(|ident| ident.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("-");
|
||||
let tag = LitStr::new(&tag, span);
|
||||
Ok(ElementName::Custom(tag))
|
||||
#[test]
|
||||
fn parses_name() {
|
||||
let _parsed: ElementName = syn::parse2(quote::quote! { div }).unwrap();
|
||||
let _parsed: ElementName = syn::parse2(quote::quote! { some-cool-element }).unwrap();
|
||||
|
||||
let _parsed: Element = syn::parse2(quote::quote! { div {} }).unwrap();
|
||||
let _parsed: Element = syn::parse2(quote::quote! { some-cool-element {} }).unwrap();
|
||||
|
||||
let parsed: Element = syn::parse2(quote::quote! {
|
||||
some-cool-div {
|
||||
id: "hi",
|
||||
id: "hi {abc}",
|
||||
id: "hi {def}",
|
||||
class: 123,
|
||||
something: bool,
|
||||
data_attr: "data",
|
||||
data_attr: "data2",
|
||||
data_attr: "data3",
|
||||
exp: { some_expr },
|
||||
something: {cool},
|
||||
something: bool,
|
||||
something: 123,
|
||||
onclick: move |_| {
|
||||
println!("hello world");
|
||||
},
|
||||
"some-attr": "hello world",
|
||||
onclick: move |_| {},
|
||||
class: "hello world",
|
||||
id: "my-id",
|
||||
data_attr: "data",
|
||||
data_attr: "data2",
|
||||
data_attr: "data3",
|
||||
"somte_attr3": "hello world",
|
||||
something: {cool},
|
||||
something: bool,
|
||||
something: 123,
|
||||
onclick: move |_| {
|
||||
println!("hello world");
|
||||
},
|
||||
..attrs1,
|
||||
..attrs2,
|
||||
..attrs3
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
dbg!(parsed);
|
||||
}
|
||||
|
||||
impl ToTokens for ElementName {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
match self {
|
||||
ElementName::Ident(i) => tokens.append_all(quote! { dioxus_elements::elements::#i }),
|
||||
ElementName::Custom(s) => tokens.append_all(quote! { #s }),
|
||||
#[test]
|
||||
fn parses_variety() {
|
||||
let input = quote::quote! {
|
||||
div {
|
||||
class: "hello world",
|
||||
id: "my-id",
|
||||
data_attr: "data",
|
||||
data_attr: "data2",
|
||||
data_attr: "data3",
|
||||
"somte_attr3": "hello world",
|
||||
something: {cool},
|
||||
something: bool,
|
||||
something: 123,
|
||||
onclick: move |_| {
|
||||
println!("hello world");
|
||||
},
|
||||
..attrs,
|
||||
..attrs2,
|
||||
..attrs3
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let parsed: Element = syn::parse2(input).unwrap();
|
||||
dbg!(parsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_tokens_properly() {
|
||||
let input = quote::quote! {
|
||||
div {
|
||||
class: "hello world",
|
||||
class2: "hello {world}",
|
||||
class3: "goodbye {world}",
|
||||
class4: "goodbye world",
|
||||
"something": "cool {blah}",
|
||||
"something2": "cooler",
|
||||
div {
|
||||
div {
|
||||
h1 { class: "h1 col" }
|
||||
h2 { class: "h2 col" }
|
||||
h3 { class: "h3 col" }
|
||||
div {}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let parsed: Element = syn::parse2(input).unwrap();
|
||||
println!("{}", parsed.to_token_stream().pretty_unparse());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_tokens_with_diagnostic() {
|
||||
let input = quote::quote! {
|
||||
div {
|
||||
class: "hello world",
|
||||
id: "my-id",
|
||||
..attrs,
|
||||
div {
|
||||
..attrs,
|
||||
class: "hello world",
|
||||
id: "my-id",
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let parsed: Element = syn::parse2(input).unwrap();
|
||||
println!("{}", parsed.to_token_stream().pretty_unparse());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merges_attributes() {
|
||||
let input = quote::quote! {
|
||||
div {
|
||||
class: "hello world",
|
||||
class: if count > 3 { "abc {def}" }
|
||||
}
|
||||
};
|
||||
|
||||
let parsed: Element = syn::parse2(input).unwrap();
|
||||
assert_eq!(parsed.merged_attributes.len(), 1);
|
||||
assert_eq!(
|
||||
parsed.merged_attributes[0].name.to_string(),
|
||||
"class".to_string()
|
||||
);
|
||||
|
||||
let attr = &parsed.merged_attributes[0].value;
|
||||
|
||||
println!("{}", attr.to_token_stream().pretty_unparse());
|
||||
|
||||
let _attr = match attr {
|
||||
AttributeValue::AttrLiteral(lit) => lit,
|
||||
_ => panic!("expected literal"),
|
||||
};
|
||||
}
|
||||
|
||||
/// There are a number of cases where merging attributes doesn't make sense
|
||||
/// - merging two expressions together
|
||||
/// - merging two literals together
|
||||
/// - merging a literal and an expression together
|
||||
/// etc
|
||||
///
|
||||
/// We really only want to merge formatted things together
|
||||
///
|
||||
/// IE
|
||||
/// class: "hello world ",
|
||||
/// class: if some_expr { "abc" }
|
||||
///
|
||||
/// Some open questions - should the delimiter be explicit?
|
||||
#[test]
|
||||
fn merging_weird_fails() {
|
||||
let input = quote::quote! {
|
||||
div {
|
||||
class: "hello world",
|
||||
class: if some_expr { 123 },
|
||||
|
||||
style: "color: red;",
|
||||
style: "color: blue;",
|
||||
|
||||
width: "1px",
|
||||
width: 1,
|
||||
width: false,
|
||||
contenteditable: true,
|
||||
}
|
||||
};
|
||||
|
||||
let parsed: Element = syn::parse2(input).unwrap();
|
||||
|
||||
assert_eq!(parsed.merged_attributes.len(), 4);
|
||||
assert_eq!(parsed.diagnostics.len(), 3);
|
||||
|
||||
// style should not generate a diagnostic
|
||||
assert!(!parsed
|
||||
.diagnostics
|
||||
.diagnostics
|
||||
.into_iter()
|
||||
.any(|f| f.emit_as_item_tokens().to_string().contains("style")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnositcs() {
|
||||
let input = quote::quote! {
|
||||
p {
|
||||
class: "foo bar"
|
||||
"Hello world"
|
||||
}
|
||||
};
|
||||
|
||||
let _parsed: Element = syn::parse2(input).unwrap();
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// An error produced when interperting the rsx
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum Error {
|
||||
ParseError(ParseError),
|
||||
RecompileRequiredError(RecompileReason),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum RecompileReason {
|
||||
Variable(String),
|
||||
Expression(String),
|
||||
Component(String),
|
||||
Listener(String),
|
||||
Attribute(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct CodeLocation {
|
||||
pub line: u32,
|
||||
pub column: u32,
|
||||
pub file_path: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct ParseError {
|
||||
pub message: String,
|
||||
pub location: CodeLocation,
|
||||
}
|
||||
|
||||
impl ParseError {
|
||||
pub fn new(error: syn::Error, mut location: CodeLocation) -> Self {
|
||||
let message = error.to_string();
|
||||
let syn_call_site = error.span().start();
|
||||
location.line += syn_call_site.line as u32;
|
||||
if syn_call_site.line == 0 {
|
||||
location.column += syn_call_site.column as u32;
|
||||
} else {
|
||||
location.column = syn_call_site.column as u32;
|
||||
}
|
||||
location.column += 1;
|
||||
ParseError { message, location }
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::ParseError(error) => writeln!(
|
||||
f,
|
||||
"parse error:\n--> at {}:{}:{}\n\t{:?}",
|
||||
error.location.file_path, error.location.line, error.location.column, error.message
|
||||
),
|
||||
Error::RecompileRequiredError(reason) => {
|
||||
writeln!(f, "recompile required: {:?}", reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
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(crate::errors::missing_trailing_comma($span));
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! attr_after_element {
|
||||
($span:expr) => {
|
||||
return Err(syn::Error::new($span, "expected element\n = help move the attribute above all the children and text elements"));
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! component_path_cannot_have_arguments {
|
||||
($span:expr) => {
|
||||
return Err(Error::new(
|
||||
$span,
|
||||
"expected a path without arguments\n = try remove the path arguments",
|
||||
));
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! invalid_component_path {
|
||||
($span:expr) => {
|
||||
return Err(Error::new($span, "Invalid component path syntax"));
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! invalid_key {
|
||||
($_key:ident) => {
|
||||
let val = $_key.to_static().unwrap();
|
||||
return Err(syn::Error::new(
|
||||
$_key.span(),
|
||||
format!("Element keys must be a dynamic value. Considering using `key: {{{val}}}` instead.\nStatic keys will result in every element using the same key which will cause rendering issues or panics."),
|
||||
));
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use crate::{DynIdx, PartialExpr};
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use syn::parse::Parse;
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub struct ExprNode {
|
||||
pub expr: PartialExpr,
|
||||
pub dyn_idx: DynIdx,
|
||||
}
|
||||
|
||||
impl ExprNode {
|
||||
pub fn span(&self) -> proc_macro2::Span {
|
||||
self.expr.span()
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for ExprNode {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
Ok(Self {
|
||||
expr: input.parse()?,
|
||||
dyn_idx: DynIdx::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ExprNode {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
let expr = &self.expr;
|
||||
tokens.append_all(quote! {
|
||||
{ let ___nodes = (#expr).into_dyn_node(); ___nodes }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_commas() {
|
||||
use crate::PrettyUnparse;
|
||||
let input = quote! {
|
||||
div {
|
||||
{label("Hello, world!")},
|
||||
}
|
||||
};
|
||||
|
||||
let _expr: crate::BodyNode = syn::parse2(input).unwrap();
|
||||
println!("{}", _expr.to_token_stream().pretty_unparse());
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
use super::*;
|
||||
use location::DynIdx;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use syn::{braced, Expr, Pat};
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub struct ForLoop {
|
||||
pub for_token: Token![for],
|
||||
pub pat: Pat,
|
||||
pub in_token: Token![in],
|
||||
pub expr: Box<Expr>,
|
||||
pub body: TemplateBody,
|
||||
pub dyn_idx: DynIdx,
|
||||
}
|
||||
|
||||
impl Parse for ForLoop {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
// todo: better partial parsing
|
||||
// A bit stolen from `ExprForLoop` in the `syn` crate
|
||||
let for_token = input.parse()?;
|
||||
let pat = input.call(Pat::parse_single)?;
|
||||
let in_token = input.parse()?;
|
||||
let expr = input.call(Expr::parse_without_eager_brace)?;
|
||||
|
||||
let content;
|
||||
let _brace = braced!(content in input);
|
||||
let body = content.parse()?;
|
||||
|
||||
Ok(Self {
|
||||
for_token,
|
||||
pat,
|
||||
in_token,
|
||||
expr: Box::new(expr),
|
||||
body,
|
||||
dyn_idx: DynIdx::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ForLoop {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let ForLoop {
|
||||
pat, expr, body, ..
|
||||
} = self;
|
||||
|
||||
// the temporary is important so we create a lifetime binding
|
||||
tokens.append_all(quote! {
|
||||
{
|
||||
let ___nodes = (#expr).into_iter().map(|#pat| { #body }).into_dyn_node();
|
||||
___nodes
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_for_loop() {
|
||||
let toks = quote! {
|
||||
for item in 0..10 {
|
||||
div { "cool-{item}" }
|
||||
div { "cool-{item}" }
|
||||
div { "cool-{item}" }
|
||||
}
|
||||
};
|
||||
|
||||
let for_loop: ForLoop = syn::parse2(toks).unwrap();
|
||||
assert!(for_loop.body.roots.len() == 3);
|
||||
}
|
||||
@@ -1,667 +1,145 @@
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::ToTokens;
|
||||
use syn::{Expr, File, Item, Macro, Stmt, TraitItem};
|
||||
//! Compare two files and find any rsx calls that have changed
|
||||
//!
|
||||
//! This is used to determine if a hotreload is needed.
|
||||
//! We use a special syn visitor to find all the rsx! calls in the file and then compare them to see
|
||||
//! if they are the same. This visitor will actually remove the rsx! calls and replace them with a
|
||||
//! dummy rsx! call. The final file type is thus mutated in place, leaving the original file idents
|
||||
//! in place. We then compare the two files to see if they are the same. We're able to differentiate
|
||||
//! between rust code changes and rsx code changes with much less code than the previous manual diff
|
||||
//! approach.
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DiffResult {
|
||||
/// Non-rsx was changed in the file
|
||||
CodeChanged(NotreloadableReason),
|
||||
|
||||
/// Rsx was changed in the file
|
||||
///
|
||||
/// Contains a list of macro invocations that were changed
|
||||
RsxChanged { rsx_calls: Vec<ChangedRsx> },
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum NotreloadableReason {
|
||||
RootMismatch,
|
||||
|
||||
RsxMismatch,
|
||||
}
|
||||
use syn::visit_mut::VisitMut;
|
||||
use syn::{File, Macro};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ChangedRsx {
|
||||
/// The macro that was changed
|
||||
/// The old macro - the original RSX from the original file
|
||||
pub old: Macro,
|
||||
|
||||
/// The new tokens for the macro
|
||||
pub new: TokenStream,
|
||||
/// The new macro
|
||||
pub new: Macro,
|
||||
}
|
||||
|
||||
/// Find any rsx calls in the given file and return a list of all the rsx calls that have changed.
|
||||
pub fn diff_rsx(new: &File, old: &File) -> DiffResult {
|
||||
let mut rsx_calls = Vec::new();
|
||||
///
|
||||
/// Takes in the two files, clones them, removes the rsx! contents and prunes any doc comments.
|
||||
/// Then it compares the two files to see if they are different - if they are, the code changed.
|
||||
/// Otherwise, the code is the same and we can move on to handling the changed rsx
|
||||
///
|
||||
/// Returns `None` if the files are the same and `Some` if they are different
|
||||
/// If there are no rsx! calls in the files, the vec will be empty.
|
||||
pub fn diff_rsx(new: &File, old: &File) -> Option<Vec<ChangedRsx>> {
|
||||
// Make a clone of these files in place so we don't have to worry about mutating the original
|
||||
let mut old = old.clone();
|
||||
let mut new = new.clone();
|
||||
|
||||
if new.items.len() != old.items.len() {
|
||||
tracing::trace!(
|
||||
"found not hot reload-able change {:#?} != {:#?}",
|
||||
new.items
|
||||
.iter()
|
||||
.map(|i| i.to_token_stream().to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
old.items
|
||||
.iter()
|
||||
.map(|i| i.to_token_stream().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
return DiffResult::CodeChanged(NotreloadableReason::RootMismatch);
|
||||
// Collect all the rsx! macros from the old file - modifying the files in place
|
||||
let old_macros = collect_from_file(&mut old);
|
||||
let new_macros = collect_from_file(&mut new);
|
||||
|
||||
// If the number of rsx! macros is different, then it's not hotreloadable
|
||||
if old_macros.len() != new_macros.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
for (new, old) in new.items.iter().zip(old.items.iter()) {
|
||||
if find_rsx_item(new, old, &mut rsx_calls) {
|
||||
tracing::trace!(
|
||||
"found not hot reload-able change {:#?} != {:#?}",
|
||||
new.to_token_stream().to_string(),
|
||||
old.to_token_stream().to_string()
|
||||
);
|
||||
|
||||
return DiffResult::CodeChanged(NotreloadableReason::RsxMismatch);
|
||||
}
|
||||
// If the files are not the same, then it's not hotreloadable
|
||||
if old != new {
|
||||
return None;
|
||||
}
|
||||
|
||||
tracing::trace!("found hot reload-able changes {:#?}", rsx_calls);
|
||||
DiffResult::RsxChanged { rsx_calls }
|
||||
Some(
|
||||
old_macros
|
||||
.into_iter()
|
||||
.zip(new_macros)
|
||||
.map(|(old, new)| ChangedRsx { old, new })
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn find_rsx_item(new: &Item, old: &Item, rsx_calls: &mut Vec<ChangedRsx>) -> bool {
|
||||
match (new, old) {
|
||||
(Item::Const(new_item), Item::Const(old_item)) => {
|
||||
find_rsx_expr(&new_item.expr, &old_item.expr, rsx_calls)
|
||||
|| new_item.attrs != old_item.attrs
|
||||
|| new_item.vis != old_item.vis
|
||||
|| new_item.const_token != old_item.const_token
|
||||
|| new_item.ident != old_item.ident
|
||||
|| new_item.colon_token != old_item.colon_token
|
||||
|| new_item.ty != old_item.ty
|
||||
|| new_item.eq_token != old_item.eq_token
|
||||
|| new_item.semi_token != old_item.semi_token
|
||||
}
|
||||
(Item::Enum(new_item), Item::Enum(old_item)) => {
|
||||
if new_item.variants.len() != old_item.variants.len() {
|
||||
return true;
|
||||
}
|
||||
for (new_variant, old_variant) in new_item.variants.iter().zip(old_item.variants.iter())
|
||||
{
|
||||
match (&new_variant.discriminant, &old_variant.discriminant) {
|
||||
(Some((new_eq, new_expr)), Some((old_eq, old_expr))) => {
|
||||
if find_rsx_expr(new_expr, old_expr, rsx_calls) || new_eq != old_eq {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
(None, None) => (),
|
||||
_ => return true,
|
||||
}
|
||||
if new_variant.attrs != old_variant.attrs
|
||||
|| new_variant.ident != old_variant.ident
|
||||
|| new_variant.fields != old_variant.fields
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
new_item.attrs != old_item.attrs
|
||||
|| new_item.vis != old_item.vis
|
||||
|| new_item.enum_token != old_item.enum_token
|
||||
|| new_item.ident != old_item.ident
|
||||
|| new_item.generics != old_item.generics
|
||||
|| new_item.brace_token != old_item.brace_token
|
||||
}
|
||||
(Item::ExternCrate(new_item), Item::ExternCrate(old_item)) => old_item != new_item,
|
||||
(Item::Fn(new_item), Item::Fn(old_item)) => {
|
||||
find_rsx_block(&new_item.block, &old_item.block, rsx_calls)
|
||||
|| new_item.attrs != old_item.attrs
|
||||
|| new_item.vis != old_item.vis
|
||||
|| new_item.sig != old_item.sig
|
||||
}
|
||||
(Item::ForeignMod(new_item), Item::ForeignMod(old_item)) => old_item != new_item,
|
||||
(Item::Impl(new_item), Item::Impl(old_item)) => {
|
||||
if new_item.items.len() != old_item.items.len() {
|
||||
return true;
|
||||
}
|
||||
for (new_item, old_item) in new_item.items.iter().zip(old_item.items.iter()) {
|
||||
if match (new_item, old_item) {
|
||||
(syn::ImplItem::Const(new_item), syn::ImplItem::Const(old_item)) => {
|
||||
find_rsx_expr(&new_item.expr, &old_item.expr, rsx_calls)
|
||||
}
|
||||
(syn::ImplItem::Fn(new_item), syn::ImplItem::Fn(old_item)) => {
|
||||
find_rsx_block(&new_item.block, &old_item.block, rsx_calls)
|
||||
}
|
||||
(syn::ImplItem::Type(new_item), syn::ImplItem::Type(old_item)) => {
|
||||
old_item != new_item
|
||||
}
|
||||
(syn::ImplItem::Macro(new_item), syn::ImplItem::Macro(old_item)) => {
|
||||
old_item != new_item
|
||||
}
|
||||
(syn::ImplItem::Verbatim(stream), syn::ImplItem::Verbatim(stream2)) => {
|
||||
stream.to_string() != stream2.to_string()
|
||||
}
|
||||
_ => true,
|
||||
} {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
new_item.attrs != old_item.attrs
|
||||
|| new_item.defaultness != old_item.defaultness
|
||||
|| new_item.unsafety != old_item.unsafety
|
||||
|| new_item.impl_token != old_item.impl_token
|
||||
|| new_item.generics != old_item.generics
|
||||
|| new_item.trait_ != old_item.trait_
|
||||
|| new_item.self_ty != old_item.self_ty
|
||||
|| new_item.brace_token != old_item.brace_token
|
||||
}
|
||||
(Item::Macro(new_item), Item::Macro(old_item)) => {
|
||||
find_rsx_macro(&new_item.mac, &old_item.mac, rsx_calls)
|
||||
|| new_item.attrs != old_item.attrs
|
||||
|| new_item.semi_token != old_item.semi_token
|
||||
|| new_item.ident != old_item.ident
|
||||
}
|
||||
(Item::Mod(new_item), Item::Mod(old_item)) => {
|
||||
match (&new_item.content, &old_item.content) {
|
||||
(Some((_, new_items)), Some((_, old_items))) => {
|
||||
if new_items.len() != old_items.len() {
|
||||
return true;
|
||||
}
|
||||
for (new_item, old_item) in new_items.iter().zip(old_items.iter()) {
|
||||
if find_rsx_item(new_item, old_item, rsx_calls) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
new_item.attrs != old_item.attrs
|
||||
|| new_item.vis != old_item.vis
|
||||
|| new_item.mod_token != old_item.mod_token
|
||||
|| new_item.ident != old_item.ident
|
||||
|| new_item.semi != old_item.semi
|
||||
}
|
||||
(None, None) => {
|
||||
new_item.attrs != old_item.attrs
|
||||
|| new_item.vis != old_item.vis
|
||||
|| new_item.mod_token != old_item.mod_token
|
||||
|| new_item.ident != old_item.ident
|
||||
|| new_item.semi != old_item.semi
|
||||
}
|
||||
_ => true,
|
||||
pub fn collect_from_file(file: &mut File) -> Vec<Macro> {
|
||||
struct MacroCollector(Vec<Macro>);
|
||||
impl VisitMut for MacroCollector {
|
||||
/// Take out the rsx! macros, leaving a default in their place
|
||||
fn visit_macro_mut(&mut self, dest: &mut syn::Macro) {
|
||||
let name = &dest.path.segments.last().map(|i| i.ident.to_string());
|
||||
if let Some("rsx" | "render") = name.as_deref() {
|
||||
let mut default: syn::Macro = syn::parse_quote! { rsx! {} };
|
||||
std::mem::swap(dest, &mut default);
|
||||
self.0.push(default)
|
||||
}
|
||||
}
|
||||
(Item::Static(new_item), Item::Static(old_item)) => {
|
||||
find_rsx_expr(&new_item.expr, &old_item.expr, rsx_calls)
|
||||
|| new_item.attrs != old_item.attrs
|
||||
|| new_item.vis != old_item.vis
|
||||
|| new_item.static_token != old_item.static_token
|
||||
|| new_item.mutability != old_item.mutability
|
||||
|| new_item.ident != old_item.ident
|
||||
|| new_item.colon_token != old_item.colon_token
|
||||
|| new_item.ty != old_item.ty
|
||||
|| new_item.eq_token != old_item.eq_token
|
||||
|| new_item.semi_token != old_item.semi_token
|
||||
}
|
||||
(Item::Struct(new_item), Item::Struct(old_item)) => old_item != new_item,
|
||||
(Item::Trait(new_item), Item::Trait(old_item)) => {
|
||||
find_rsx_trait(new_item, old_item, rsx_calls)
|
||||
}
|
||||
(Item::TraitAlias(new_item), Item::TraitAlias(old_item)) => old_item != new_item,
|
||||
(Item::Type(new_item), Item::Type(old_item)) => old_item != new_item,
|
||||
(Item::Union(new_item), Item::Union(old_item)) => old_item != new_item,
|
||||
(Item::Use(new_item), Item::Use(old_item)) => old_item != new_item,
|
||||
(Item::Verbatim(_), Item::Verbatim(_)) => false,
|
||||
|
||||
_ => true,
|
||||
/// Ignore doc comments by swapping them out with a default
|
||||
fn visit_attribute_mut(&mut self, i: &mut syn::Attribute) {
|
||||
if i.path().is_ident("doc") {
|
||||
*i = syn::parse_quote! { #[doc = ""] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut macros = MacroCollector(vec![]);
|
||||
macros.visit_file_mut(file);
|
||||
macros.0
|
||||
}
|
||||
|
||||
fn find_rsx_trait(
|
||||
new_item: &syn::ItemTrait,
|
||||
old_item: &syn::ItemTrait,
|
||||
rsx_calls: &mut Vec<ChangedRsx>,
|
||||
) -> bool {
|
||||
if new_item.items.len() != old_item.items.len() {
|
||||
return true;
|
||||
}
|
||||
for (new_item, old_item) in new_item.items.iter().zip(old_item.items.iter()) {
|
||||
if match (new_item, old_item) {
|
||||
(TraitItem::Const(new_item), TraitItem::Const(old_item)) => {
|
||||
if let (Some((_, new_expr)), Some((_, old_expr))) =
|
||||
(&new_item.default, &old_item.default)
|
||||
{
|
||||
find_rsx_expr(new_expr, old_expr, rsx_calls)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
(TraitItem::Fn(new_item), TraitItem::Fn(old_item)) => {
|
||||
match (&new_item.default, &old_item.default) {
|
||||
(Some(new_block), Some(old_block)) => {
|
||||
find_rsx_block(new_block, old_block, rsx_calls)
|
||||
}
|
||||
(None, None) => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
(TraitItem::Type(new_item), TraitItem::Type(old_item)) => old_item != new_item,
|
||||
(TraitItem::Macro(new_item), TraitItem::Macro(old_item)) => old_item != new_item,
|
||||
(TraitItem::Verbatim(stream), TraitItem::Verbatim(stream2)) => {
|
||||
stream.to_string() != stream2.to_string()
|
||||
}
|
||||
_ => true,
|
||||
} {
|
||||
return true;
|
||||
#[test]
|
||||
fn changing_files() {
|
||||
let old = r#"
|
||||
use dioxus::prelude::*;
|
||||
|
||||
/// some comment
|
||||
pub fn CoolChild() -> Element {
|
||||
let a = 123;
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
{some_expr()}
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
|
||||
new_item.attrs != old_item.attrs
|
||||
|| new_item.vis != old_item.vis
|
||||
|| new_item.unsafety != old_item.unsafety
|
||||
|| new_item.auto_token != old_item.auto_token
|
||||
|| new_item.ident != old_item.ident
|
||||
|| new_item.generics != old_item.generics
|
||||
|| new_item.colon_token != old_item.colon_token
|
||||
|| new_item.supertraits != old_item.supertraits
|
||||
|| new_item.brace_token != old_item.brace_token
|
||||
}
|
||||
let new = r#"
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn find_rsx_block(
|
||||
new_block: &syn::Block,
|
||||
old_block: &syn::Block,
|
||||
rsx_calls: &mut Vec<ChangedRsx>,
|
||||
) -> bool {
|
||||
if new_block.stmts.len() != old_block.stmts.len() {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (new_stmt, old_stmt) in new_block.stmts.iter().zip(old_block.stmts.iter()) {
|
||||
if find_rsx_stmt(new_stmt, old_stmt, rsx_calls) {
|
||||
return true;
|
||||
/// some comment
|
||||
pub fn CoolChild() -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
{some_expr()}
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
|
||||
new_block.brace_token != old_block.brace_token
|
||||
}
|
||||
let same = r#"
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn find_rsx_stmt(new_stmt: &Stmt, old_stmt: &Stmt, rsx_calls: &mut Vec<ChangedRsx>) -> bool {
|
||||
match (new_stmt, old_stmt) {
|
||||
(Stmt::Local(new_local), Stmt::Local(old_local)) => {
|
||||
(match (&new_local.init, &old_local.init) {
|
||||
(Some(new_local), Some(old_local)) => {
|
||||
find_rsx_expr(&new_local.expr, &old_local.expr, rsx_calls)
|
||||
|| new_local.diverge != old_local.diverge
|
||||
}
|
||||
(None, None) => false,
|
||||
_ => true,
|
||||
} || new_local.attrs != old_local.attrs
|
||||
|| new_local.let_token != old_local.let_token
|
||||
|| new_local.pat != old_local.pat
|
||||
|| new_local.semi_token != old_local.semi_token)
|
||||
}
|
||||
(Stmt::Item(new_item), Stmt::Item(old_item)) => {
|
||||
find_rsx_item(new_item, old_item, rsx_calls)
|
||||
}
|
||||
(Stmt::Expr(new_expr, _), Stmt::Expr(old_expr, _)) => {
|
||||
find_rsx_expr(new_expr, old_expr, rsx_calls)
|
||||
}
|
||||
(Stmt::Macro(new_macro), Stmt::Macro(old_macro)) => {
|
||||
find_rsx_macro(&new_macro.mac, &old_macro.mac, rsx_calls)
|
||||
|| new_macro.attrs != old_macro.attrs
|
||||
|| new_macro.semi_token != old_macro.semi_token
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
/// some comment!!!!!
|
||||
pub fn CoolChild() -> Element {
|
||||
let a = 123;
|
||||
|
||||
fn find_rsx_expr(new_expr: &Expr, old_expr: &Expr, rsx_calls: &mut Vec<ChangedRsx>) -> bool {
|
||||
match (new_expr, old_expr) {
|
||||
(Expr::Array(new_expr), Expr::Array(old_expr)) => {
|
||||
if new_expr.elems.len() != old_expr.elems.len() {
|
||||
return true;
|
||||
}
|
||||
for (new_el, old_el) in new_expr.elems.iter().zip(old_expr.elems.iter()) {
|
||||
if find_rsx_expr(new_el, old_el, rsx_calls) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
new_expr.attrs != old_expr.attrs || new_expr.bracket_token != old_expr.bracket_token
|
||||
}
|
||||
(Expr::Assign(new_expr), Expr::Assign(old_expr)) => {
|
||||
find_rsx_expr(&new_expr.left, &old_expr.left, rsx_calls)
|
||||
|| find_rsx_expr(&new_expr.right, &old_expr.right, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.eq_token != old_expr.eq_token
|
||||
}
|
||||
(Expr::Async(new_expr), Expr::Async(old_expr)) => {
|
||||
find_rsx_block(&new_expr.block, &old_expr.block, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.async_token != old_expr.async_token
|
||||
|| new_expr.capture != old_expr.capture
|
||||
}
|
||||
(Expr::Await(new_expr), Expr::Await(old_expr)) => {
|
||||
find_rsx_expr(&new_expr.base, &old_expr.base, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.dot_token != old_expr.dot_token
|
||||
|| new_expr.await_token != old_expr.await_token
|
||||
}
|
||||
(Expr::Binary(new_expr), Expr::Binary(old_expr)) => {
|
||||
find_rsx_expr(&new_expr.left, &old_expr.left, rsx_calls)
|
||||
|| find_rsx_expr(&new_expr.right, &old_expr.right, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.op != old_expr.op
|
||||
}
|
||||
(Expr::Block(new_expr), Expr::Block(old_expr)) => {
|
||||
find_rsx_block(&new_expr.block, &old_expr.block, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.label != old_expr.label
|
||||
}
|
||||
(Expr::Break(new_expr), Expr::Break(old_expr)) => match (&new_expr.expr, &old_expr.expr) {
|
||||
(Some(new_inner), Some(old_inner)) => {
|
||||
find_rsx_expr(new_inner, old_inner, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.break_token != old_expr.break_token
|
||||
|| new_expr.label != old_expr.label
|
||||
}
|
||||
(None, None) => {
|
||||
new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.break_token != old_expr.break_token
|
||||
|| new_expr.label != old_expr.label
|
||||
}
|
||||
_ => true,
|
||||
},
|
||||
(Expr::Call(new_expr), Expr::Call(old_expr)) => {
|
||||
find_rsx_expr(&new_expr.func, &old_expr.func, rsx_calls);
|
||||
if new_expr.args.len() != old_expr.args.len() {
|
||||
return true;
|
||||
}
|
||||
for (new_arg, old_arg) in new_expr.args.iter().zip(old_expr.args.iter()) {
|
||||
if find_rsx_expr(new_arg, old_arg, rsx_calls) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
new_expr.attrs != old_expr.attrs || new_expr.paren_token != old_expr.paren_token
|
||||
}
|
||||
(Expr::Cast(new_expr), Expr::Cast(old_expr)) => {
|
||||
find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.as_token != old_expr.as_token
|
||||
|| new_expr.ty != old_expr.ty
|
||||
}
|
||||
(Expr::Closure(new_expr), Expr::Closure(old_expr)) => {
|
||||
find_rsx_expr(&new_expr.body, &old_expr.body, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.movability != old_expr.movability
|
||||
|| new_expr.asyncness != old_expr.asyncness
|
||||
|| new_expr.capture != old_expr.capture
|
||||
|| new_expr.or1_token != old_expr.or1_token
|
||||
|| new_expr.inputs != old_expr.inputs
|
||||
|| new_expr.or2_token != old_expr.or2_token
|
||||
|| new_expr.output != old_expr.output
|
||||
}
|
||||
(Expr::Const(new_expr), Expr::Const(old_expr)) => {
|
||||
find_rsx_block(&new_expr.block, &old_expr.block, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.const_token != old_expr.const_token
|
||||
}
|
||||
(Expr::Continue(new_expr), Expr::Continue(old_expr)) => old_expr != new_expr,
|
||||
(Expr::Field(new_expr), Expr::Field(old_expr)) => {
|
||||
find_rsx_expr(&new_expr.base, &old_expr.base, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.dot_token != old_expr.dot_token
|
||||
|| new_expr.member != old_expr.member
|
||||
}
|
||||
(Expr::ForLoop(new_expr), Expr::ForLoop(old_expr)) => {
|
||||
find_rsx_block(&new_expr.body, &old_expr.body, rsx_calls)
|
||||
|| find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.label != old_expr.label
|
||||
|| new_expr.for_token != old_expr.for_token
|
||||
|| new_expr.pat != old_expr.pat
|
||||
|| new_expr.in_token != old_expr.in_token
|
||||
}
|
||||
(Expr::Group(new_expr), Expr::Group(old_expr)) => {
|
||||
find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls)
|
||||
}
|
||||
(Expr::If(new_expr), Expr::If(old_expr)) => {
|
||||
if find_rsx_expr(&new_expr.cond, &old_expr.cond, rsx_calls)
|
||||
|| find_rsx_block(&new_expr.then_branch, &old_expr.then_branch, rsx_calls)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
match (&new_expr.else_branch, &old_expr.else_branch) {
|
||||
(Some((new_tok, new_else)), Some((old_tok, old_else))) => {
|
||||
find_rsx_expr(new_else, old_else, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.if_token != old_expr.if_token
|
||||
|| new_expr.cond != old_expr.cond
|
||||
|| new_tok != old_tok
|
||||
}
|
||||
(None, None) => {
|
||||
new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.if_token != old_expr.if_token
|
||||
|| new_expr.cond != old_expr.cond
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
(Expr::Index(new_expr), Expr::Index(old_expr)) => {
|
||||
find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls)
|
||||
|| find_rsx_expr(&new_expr.index, &old_expr.index, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.bracket_token != old_expr.bracket_token
|
||||
}
|
||||
(Expr::Infer(new_expr), Expr::Infer(old_expr)) => new_expr != old_expr,
|
||||
(Expr::Let(new_expr), Expr::Let(old_expr)) => {
|
||||
find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.let_token != old_expr.let_token
|
||||
|| new_expr.pat != old_expr.pat
|
||||
|| new_expr.eq_token != old_expr.eq_token
|
||||
rsx! {
|
||||
div {
|
||||
{some_expr()}
|
||||
}
|
||||
(Expr::Lit(new_expr), Expr::Lit(old_expr)) => old_expr != new_expr,
|
||||
(Expr::Loop(new_expr), Expr::Loop(old_expr)) => {
|
||||
find_rsx_block(&new_expr.body, &old_expr.body, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.label != old_expr.label
|
||||
|| new_expr.loop_token != old_expr.loop_token
|
||||
}
|
||||
(Expr::Macro(new_expr), Expr::Macro(old_expr)) => {
|
||||
find_rsx_macro(&new_expr.mac, &old_expr.mac, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
}
|
||||
(Expr::Match(new_expr), Expr::Match(old_expr)) => {
|
||||
if find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls) {
|
||||
return true;
|
||||
}
|
||||
for (new_arm, old_arm) in new_expr.arms.iter().zip(old_expr.arms.iter()) {
|
||||
match (&new_arm.guard, &old_arm.guard) {
|
||||
(Some((new_tok, new_expr)), Some((old_tok, old_expr))) => {
|
||||
if find_rsx_expr(new_expr, old_expr, rsx_calls) || new_tok != old_tok {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
(None, None) => (),
|
||||
_ => return true,
|
||||
}
|
||||
if find_rsx_expr(&new_arm.body, &old_arm.body, rsx_calls)
|
||||
|| new_arm.attrs != old_arm.attrs
|
||||
|| new_arm.pat != old_arm.pat
|
||||
|| new_arm.fat_arrow_token != old_arm.fat_arrow_token
|
||||
|| new_arm.comma != old_arm.comma
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.match_token != old_expr.match_token
|
||||
|| new_expr.brace_token != old_expr.brace_token
|
||||
}
|
||||
(Expr::MethodCall(new_expr), Expr::MethodCall(old_expr)) => {
|
||||
if find_rsx_expr(&new_expr.receiver, &old_expr.receiver, rsx_calls) {
|
||||
return true;
|
||||
}
|
||||
for (new_arg, old_arg) in new_expr.args.iter().zip(old_expr.args.iter()) {
|
||||
if find_rsx_expr(new_arg, old_arg, rsx_calls) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.dot_token != old_expr.dot_token
|
||||
|| new_expr.method != old_expr.method
|
||||
|| new_expr.turbofish != old_expr.turbofish
|
||||
|| new_expr.paren_token != old_expr.paren_token
|
||||
}
|
||||
(Expr::Paren(new_expr), Expr::Paren(old_expr)) => {
|
||||
find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.paren_token != old_expr.paren_token
|
||||
}
|
||||
(Expr::Path(new_expr), Expr::Path(old_expr)) => old_expr != new_expr,
|
||||
(Expr::Range(new_expr), Expr::Range(old_expr)) => {
|
||||
match (&new_expr.start, &old_expr.start) {
|
||||
(Some(new_expr), Some(old_expr)) => {
|
||||
if find_rsx_expr(new_expr, old_expr, rsx_calls) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
(None, None) => (),
|
||||
_ => return true,
|
||||
}
|
||||
match (&new_expr.end, &old_expr.end) {
|
||||
(Some(new_inner), Some(old_inner)) => {
|
||||
find_rsx_expr(new_inner, old_inner, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.limits != old_expr.limits
|
||||
}
|
||||
(None, None) => {
|
||||
new_expr.attrs != old_expr.attrs || new_expr.limits != old_expr.limits
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
(Expr::Reference(new_expr), Expr::Reference(old_expr)) => {
|
||||
find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.and_token != old_expr.and_token
|
||||
|| new_expr.mutability != old_expr.mutability
|
||||
}
|
||||
(Expr::Repeat(new_expr), Expr::Repeat(old_expr)) => {
|
||||
find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls)
|
||||
|| find_rsx_expr(&new_expr.len, &old_expr.len, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.bracket_token != old_expr.bracket_token
|
||||
|| new_expr.semi_token != old_expr.semi_token
|
||||
}
|
||||
(Expr::Return(new_expr), Expr::Return(old_expr)) => {
|
||||
match (&new_expr.expr, &old_expr.expr) {
|
||||
(Some(new_inner), Some(old_inner)) => {
|
||||
find_rsx_expr(new_inner, old_inner, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.return_token != old_expr.return_token
|
||||
}
|
||||
(None, None) => {
|
||||
new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.return_token != old_expr.return_token
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
(Expr::Struct(new_expr), Expr::Struct(old_expr)) => {
|
||||
match (&new_expr.rest, &old_expr.rest) {
|
||||
(Some(new_expr), Some(old_expr)) => {
|
||||
if find_rsx_expr(new_expr, old_expr, rsx_calls) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
(None, None) => (),
|
||||
_ => return true,
|
||||
}
|
||||
for (new_field, old_field) in new_expr.fields.iter().zip(old_expr.fields.iter()) {
|
||||
if find_rsx_expr(&new_field.expr, &old_field.expr, rsx_calls)
|
||||
|| new_field.attrs != old_field.attrs
|
||||
|| new_field.member != old_field.member
|
||||
|| new_field.colon_token != old_field.colon_token
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.path != old_expr.path
|
||||
|| new_expr.brace_token != old_expr.brace_token
|
||||
|| new_expr.dot2_token != old_expr.dot2_token
|
||||
}
|
||||
(Expr::Try(new_expr), Expr::Try(old_expr)) => {
|
||||
find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.question_token != old_expr.question_token
|
||||
}
|
||||
(Expr::TryBlock(new_expr), Expr::TryBlock(old_expr)) => {
|
||||
find_rsx_block(&new_expr.block, &old_expr.block, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.try_token != old_expr.try_token
|
||||
}
|
||||
(Expr::Tuple(new_expr), Expr::Tuple(old_expr)) => {
|
||||
for (new_el, old_el) in new_expr.elems.iter().zip(old_expr.elems.iter()) {
|
||||
if find_rsx_expr(new_el, old_el, rsx_calls) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
new_expr.attrs != old_expr.attrs || new_expr.paren_token != old_expr.paren_token
|
||||
}
|
||||
(Expr::Unary(new_expr), Expr::Unary(old_expr)) => {
|
||||
find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.op != old_expr.op
|
||||
}
|
||||
(Expr::Unsafe(new_expr), Expr::Unsafe(old_expr)) => {
|
||||
find_rsx_block(&new_expr.block, &old_expr.block, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.unsafe_token != old_expr.unsafe_token
|
||||
}
|
||||
(Expr::While(new_expr), Expr::While(old_expr)) => {
|
||||
find_rsx_expr(&new_expr.cond, &old_expr.cond, rsx_calls)
|
||||
|| find_rsx_block(&new_expr.body, &old_expr.body, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.label != old_expr.label
|
||||
|| new_expr.while_token != old_expr.while_token
|
||||
}
|
||||
(Expr::Yield(new_expr), Expr::Yield(old_expr)) => match (&new_expr.expr, &old_expr.expr) {
|
||||
(Some(new_inner), Some(old_inner)) => {
|
||||
find_rsx_expr(new_inner, old_inner, rsx_calls)
|
||||
|| new_expr.attrs != old_expr.attrs
|
||||
|| new_expr.yield_token != old_expr.yield_token
|
||||
}
|
||||
(None, None) => {
|
||||
new_expr.attrs != old_expr.attrs || new_expr.yield_token != old_expr.yield_token
|
||||
}
|
||||
_ => true,
|
||||
},
|
||||
(Expr::Verbatim(stream), Expr::Verbatim(stream2)) => {
|
||||
stream.to_string() != stream2.to_string()
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
|
||||
fn find_rsx_macro(
|
||||
new_mac: &syn::Macro,
|
||||
old_mac: &syn::Macro,
|
||||
rsx_calls: &mut Vec<ChangedRsx>,
|
||||
) -> bool {
|
||||
if matches!(
|
||||
new_mac
|
||||
.path
|
||||
.get_ident()
|
||||
.map(|ident| ident.to_string())
|
||||
.as_deref(),
|
||||
Some("rsx" | "render")
|
||||
) && matches!(
|
||||
old_mac
|
||||
.path
|
||||
.get_ident()
|
||||
.map(|ident| ident.to_string())
|
||||
.as_deref(),
|
||||
Some("rsx" | "render")
|
||||
) {
|
||||
rsx_calls.push(ChangedRsx {
|
||||
old: old_mac.clone(),
|
||||
new: new_mac.tokens.clone(),
|
||||
});
|
||||
false
|
||||
} else {
|
||||
new_mac != old_mac
|
||||
}
|
||||
let old = syn::parse_file(old).unwrap();
|
||||
let new = syn::parse_file(new).unwrap();
|
||||
let same = syn::parse_file(same).unwrap();
|
||||
|
||||
assert!(
|
||||
diff_rsx(&old, &new).is_none(),
|
||||
"Files with different expressions should not be hotreloadable"
|
||||
);
|
||||
|
||||
assert!(
|
||||
diff_rsx(&new, &new).is_some(),
|
||||
"The same file should be reloadable with itself"
|
||||
);
|
||||
|
||||
assert!(
|
||||
diff_rsx(&old, &same).is_some(),
|
||||
"Files with changed comments should be hotreloadable"
|
||||
);
|
||||
}
|
||||
@@ -1,399 +0,0 @@
|
||||
use super::{
|
||||
hot_reload_diff::{diff_rsx, DiffResult},
|
||||
ChangedRsx,
|
||||
};
|
||||
use crate::{CallBody, HotReloadingContext};
|
||||
use dioxus_core::{
|
||||
prelude::{TemplateAttribute, TemplateNode},
|
||||
Template,
|
||||
};
|
||||
use krates::cm::MetadataCommand;
|
||||
use krates::Cmd;
|
||||
pub use proc_macro2::TokenStream;
|
||||
pub use std::collections::HashMap;
|
||||
pub use std::sync::Mutex;
|
||||
pub use std::time::SystemTime;
|
||||
use std::{collections::HashSet, ffi::OsStr, marker::PhantomData, path::PathBuf};
|
||||
pub use std::{fs, io, path::Path};
|
||||
pub use std::{fs::File, io::Read};
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
pub enum UpdateResult {
|
||||
UpdatedRsx(Vec<Template>),
|
||||
|
||||
NeedsRebuild,
|
||||
}
|
||||
|
||||
/// The result of building a FileMap
|
||||
pub struct FileMapBuildResult<Ctx: HotReloadingContext> {
|
||||
/// The FileMap that was built
|
||||
pub map: FileMap<Ctx>,
|
||||
|
||||
/// Any errors that occurred while building the FileMap that were not fatal
|
||||
pub errors: Vec<io::Error>,
|
||||
}
|
||||
|
||||
pub struct FileMap<Ctx: HotReloadingContext> {
|
||||
pub map: HashMap<PathBuf, CachedSynFile>,
|
||||
|
||||
in_workspace: HashMap<PathBuf, Option<PathBuf>>,
|
||||
|
||||
phantom: PhantomData<Ctx>,
|
||||
}
|
||||
|
||||
/// A cached file that has been parsed
|
||||
///
|
||||
/// We store the templates found in this file
|
||||
pub struct CachedSynFile {
|
||||
pub raw: String,
|
||||
pub path: PathBuf,
|
||||
pub templates: HashMap<&'static str, Template>,
|
||||
pub tracked_assets: HashSet<PathBuf>,
|
||||
}
|
||||
|
||||
impl<Ctx: HotReloadingContext> FileMap<Ctx> {
|
||||
/// Create a new FileMap from a crate directory
|
||||
///
|
||||
/// TODO: this should be created with a gitignore filter
|
||||
pub fn create(path: PathBuf) -> io::Result<FileMapBuildResult<Ctx>> {
|
||||
Self::create_with_filter(path, |p| {
|
||||
// skip some stuff we know is large by default
|
||||
p.file_name() == Some(OsStr::new("target"))
|
||||
|| p.file_name() == Some(OsStr::new("node_modules"))
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new FileMap from a crate directory
|
||||
pub fn create_with_filter(
|
||||
crate_dir: PathBuf,
|
||||
mut filter: impl FnMut(&Path) -> bool,
|
||||
) -> io::Result<FileMapBuildResult<Ctx>> {
|
||||
let FileMapSearchResult { map, errors } = find_rs_files(crate_dir.clone(), &mut filter);
|
||||
|
||||
let mut map = Self {
|
||||
map,
|
||||
in_workspace: HashMap::new(),
|
||||
phantom: PhantomData,
|
||||
};
|
||||
|
||||
map.load_assets(crate_dir.as_path());
|
||||
|
||||
Ok(FileMapBuildResult { errors, map })
|
||||
}
|
||||
|
||||
/// Start watching assets for changes
|
||||
///
|
||||
/// This just diffs every file against itself and populates the tracked assets as it goes
|
||||
pub fn load_assets(&mut self, crate_dir: &Path) {
|
||||
let keys = self.map.keys().cloned().collect::<Vec<_>>();
|
||||
for file in keys {
|
||||
_ = self.update_rsx(file.as_path(), crate_dir);
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to update the rsx in a file
|
||||
pub fn update_rsx(
|
||||
&mut self,
|
||||
file_path: &Path,
|
||||
crate_dir: &Path,
|
||||
) -> Result<UpdateResult, HotreloadError> {
|
||||
let mut file = File::open(file_path)?;
|
||||
let mut src = String::new();
|
||||
file.read_to_string(&mut src)?;
|
||||
|
||||
// If we can't parse the contents we want to pass it off to the build system to tell the user that there's a syntax error
|
||||
let syntax = syn::parse_file(&src).map_err(|_err| HotreloadError::Parse)?;
|
||||
|
||||
let in_workspace = self.child_in_workspace(crate_dir)?;
|
||||
|
||||
// Get the cached file if it exists, otherwise try to create it
|
||||
let Some(old_cached) = self.map.get_mut(file_path) else {
|
||||
// if this is a new file, rebuild the project
|
||||
let FileMapBuildResult { map, mut errors } =
|
||||
FileMap::<Ctx>::create(crate_dir.to_path_buf())?;
|
||||
|
||||
if let Some(err) = errors.pop() {
|
||||
return Err(HotreloadError::Failure(err));
|
||||
}
|
||||
|
||||
// merge the new map into the old map
|
||||
self.map.extend(map.map);
|
||||
|
||||
return Ok(UpdateResult::NeedsRebuild);
|
||||
};
|
||||
|
||||
// If the cached file is not a valid rsx file, rebuild the project, forcing errors
|
||||
// TODO: in theory the error is simply in the RsxCallbody. We could attempt to parse it using partial expansion
|
||||
// And collect out its errors instead of giving up to a full rebuild
|
||||
let old = syn::parse_file(&old_cached.raw).map_err(|_e| HotreloadError::Parse)?;
|
||||
|
||||
let instances = match diff_rsx(&syntax, &old) {
|
||||
// If the changes were just some rsx, we can just update the template
|
||||
//
|
||||
// However... if the changes involved code in the rsx itself, this should actually be a CodeChanged
|
||||
DiffResult::RsxChanged {
|
||||
rsx_calls: instances,
|
||||
} => instances,
|
||||
|
||||
// If the changes were some code, we should insert the file into the map and rebuild
|
||||
// todo: not sure we even need to put the cached file into the map, but whatever
|
||||
DiffResult::CodeChanged(_) => {
|
||||
let cached_file = CachedSynFile {
|
||||
raw: src.clone(),
|
||||
path: file_path.to_path_buf(),
|
||||
templates: HashMap::new(),
|
||||
tracked_assets: HashSet::new(),
|
||||
};
|
||||
|
||||
self.map.insert(file_path.to_path_buf(), cached_file);
|
||||
return Ok(UpdateResult::NeedsRebuild);
|
||||
}
|
||||
};
|
||||
|
||||
let mut messages: Vec<Template> = Vec::new();
|
||||
|
||||
for calls in instances.into_iter() {
|
||||
let ChangedRsx { old, new } = calls;
|
||||
|
||||
let old_start = old.span().start();
|
||||
|
||||
let old_parsed = syn::parse2::<CallBody>(old.tokens);
|
||||
let new_parsed = syn::parse2::<CallBody>(new);
|
||||
let (Ok(old_call_body), Ok(new_call_body)) = (old_parsed, new_parsed) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// if the file!() macro is invoked in a workspace, the path is relative to the workspace root, otherwise it's relative to the crate root
|
||||
// we need to check if the file is in a workspace or not and strip the prefix accordingly
|
||||
let prefix = match in_workspace {
|
||||
Some(ref workspace) => workspace,
|
||||
_ => crate_dir,
|
||||
};
|
||||
|
||||
let Ok(file) = file_path.strip_prefix(prefix) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// We leak the template since templates are a compiletime value
|
||||
// This is not ideal, but also not a huge deal for hot reloading
|
||||
// TODO: we could consider arena allocating the templates and dropping them when the connection is closed
|
||||
let leaked_location = Box::leak(template_location(old_start, file).into_boxed_str());
|
||||
|
||||
// Retuns Some(template) if the template is hotreloadable
|
||||
// dynamic changes are not hot reloadable and force a rebuild
|
||||
let hotreloadable_template =
|
||||
new_call_body.update_template::<Ctx>(Some(old_call_body), leaked_location);
|
||||
|
||||
// if the template is not hotreloadable, we need to do a full rebuild
|
||||
let Some(template) = hotreloadable_template else {
|
||||
return Ok(UpdateResult::NeedsRebuild);
|
||||
};
|
||||
|
||||
// dioxus cannot handle empty templates...
|
||||
// todo: I think it can? or we just skip them nowa
|
||||
if template.roots.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if the template is the same, don't send it
|
||||
if let Some(old_template) = old_cached.templates.get(template.name) {
|
||||
if old_template == &template {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// update the cached file
|
||||
old_cached.templates.insert(template.name, template);
|
||||
|
||||
// Track any new assets
|
||||
old_cached
|
||||
.tracked_assets
|
||||
.extend(Self::populate_assets(template));
|
||||
|
||||
messages.push(template);
|
||||
}
|
||||
|
||||
Ok(UpdateResult::UpdatedRsx(messages))
|
||||
}
|
||||
|
||||
fn populate_assets(template: Template) -> HashSet<PathBuf> {
|
||||
fn collect_assetlike_attrs(node: &TemplateNode, asset_urls: &mut HashSet<PathBuf>) {
|
||||
if let TemplateNode::Element {
|
||||
attrs, children, ..
|
||||
} = node
|
||||
{
|
||||
for attr in attrs.iter() {
|
||||
if let TemplateAttribute::Static { name, value, .. } = attr {
|
||||
if *name == "src" || *name == "href" {
|
||||
asset_urls.insert(PathBuf::from(*value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for child in children.iter() {
|
||||
collect_assetlike_attrs(child, asset_urls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut asset_urls = HashSet::new();
|
||||
|
||||
for node in template.roots {
|
||||
collect_assetlike_attrs(node, &mut asset_urls);
|
||||
}
|
||||
|
||||
asset_urls
|
||||
}
|
||||
|
||||
/// add the template to an existing file in the filemap if it exists
|
||||
/// create a new file if it doesn't exist
|
||||
pub fn insert(&mut self, path: PathBuf, template: Template) {
|
||||
let tracked_assets = Self::populate_assets(template);
|
||||
|
||||
if self.map.contains_key(&path) {
|
||||
let entry = self.map.get_mut(&path).unwrap();
|
||||
entry.tracked_assets.extend(tracked_assets);
|
||||
entry.templates.insert(template.name, template);
|
||||
} else {
|
||||
self.map.insert(
|
||||
path.clone(),
|
||||
CachedSynFile {
|
||||
raw: String::new(),
|
||||
path,
|
||||
tracked_assets,
|
||||
templates: HashMap::from([(template.name, template)]),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tracked_assets(&self) -> HashSet<PathBuf> {
|
||||
self.map
|
||||
.values()
|
||||
.flat_map(|file| file.tracked_assets.iter().cloned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn is_tracking_asset(&self, path: &PathBuf) -> Option<&CachedSynFile> {
|
||||
self.map
|
||||
.values()
|
||||
.find(|file| file.tracked_assets.contains(path))
|
||||
}
|
||||
|
||||
fn child_in_workspace(&mut self, crate_dir: &Path) -> io::Result<Option<PathBuf>> {
|
||||
if let Some(in_workspace) = self.in_workspace.get(crate_dir) {
|
||||
return Ok(in_workspace.clone());
|
||||
}
|
||||
|
||||
let mut cmd = Cmd::new();
|
||||
let manafest_path = crate_dir.join("Cargo.toml");
|
||||
cmd.manifest_path(&manafest_path);
|
||||
let cmd: MetadataCommand = cmd.into();
|
||||
let metadata = cmd
|
||||
.exec()
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
|
||||
|
||||
let in_workspace = metadata.workspace_root != crate_dir;
|
||||
let workspace_path = in_workspace.then(|| metadata.workspace_root.into());
|
||||
self.in_workspace
|
||||
.insert(crate_dir.to_path_buf(), workspace_path.clone());
|
||||
Ok(workspace_path)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn template_location(old_start: proc_macro2::LineColumn, file: &Path) -> String {
|
||||
let line = old_start.line;
|
||||
let column = old_start.column + 1;
|
||||
|
||||
// Always ensure the path components are separated by `/`.
|
||||
let path = file
|
||||
.components()
|
||||
.map(|c| c.as_os_str().to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
.join("/");
|
||||
|
||||
path
|
||||
+ ":"
|
||||
+ &line.to_string()
|
||||
+ ":"
|
||||
+ &column.to_string()
|
||||
// the byte index doesn't matter, but dioxus needs it
|
||||
+ ":0"
|
||||
}
|
||||
|
||||
struct FileMapSearchResult {
|
||||
map: HashMap<PathBuf, CachedSynFile>,
|
||||
errors: Vec<io::Error>,
|
||||
}
|
||||
|
||||
// todo: we could just steal the mod logic from rustc itself
|
||||
fn find_rs_files(root: PathBuf, filter: &mut impl FnMut(&Path) -> bool) -> FileMapSearchResult {
|
||||
let mut files = HashMap::new();
|
||||
let mut errors = Vec::new();
|
||||
|
||||
if root.is_dir() {
|
||||
let read_dir = match fs::read_dir(root) {
|
||||
Ok(read_dir) => read_dir,
|
||||
Err(err) => {
|
||||
errors.push(err);
|
||||
return FileMapSearchResult { map: files, errors };
|
||||
}
|
||||
};
|
||||
for entry in read_dir.flatten() {
|
||||
let path = entry.path();
|
||||
if !filter(&path) {
|
||||
let FileMapSearchResult {
|
||||
map,
|
||||
errors: child_errors,
|
||||
} = find_rs_files(path, filter);
|
||||
errors.extend(child_errors);
|
||||
files.extend(map);
|
||||
}
|
||||
}
|
||||
} else if root.extension().and_then(|s| s.to_str()) == Some("rs") {
|
||||
if let Ok(mut file) = File::open(root.clone()) {
|
||||
let mut src = String::new();
|
||||
match file.read_to_string(&mut src) {
|
||||
Ok(_) => {
|
||||
let cached_file = CachedSynFile {
|
||||
raw: src.clone(),
|
||||
path: root.clone(),
|
||||
templates: HashMap::new(),
|
||||
tracked_assets: HashSet::new(),
|
||||
};
|
||||
|
||||
// track assets while we're here
|
||||
files.insert(root, cached_file);
|
||||
}
|
||||
Err(err) => {
|
||||
errors.push(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileMapSearchResult { map: files, errors }
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HotreloadError {
|
||||
Failure(io::Error),
|
||||
Parse,
|
||||
NoPreviousBuild,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for HotreloadError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Failure(err) => write!(f, "Failed to parse file: {}", err),
|
||||
Self::Parse => write!(f, "Failed to parse file"),
|
||||
Self::NoPreviousBuild => write!(f, "No previous build found"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for HotreloadError {
|
||||
fn from(err: io::Error) -> Self {
|
||||
HotreloadError::Failure(err)
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,3 @@ pub use hot_reload_diff::*;
|
||||
mod hot_reloading_context;
|
||||
#[cfg(feature = "hot_reload_traits")]
|
||||
pub use hot_reloading_context::*;
|
||||
|
||||
#[cfg(feature = "hot_reload")]
|
||||
mod hot_reloading_file_map;
|
||||
#[cfg(feature = "hot_reload")]
|
||||
pub use hot_reloading_file_map::*;
|
||||
@@ -0,0 +1,429 @@
|
||||
#![cfg(feature = "hot_reload")]
|
||||
|
||||
//! This module contains hotreloading logic for rsx.
|
||||
//!
|
||||
//! There's a few details that I wish we could've gotten right but we can revisit later:
|
||||
//!
|
||||
//! - Empty rsx! blocks are written as `None` - it would be nice to be able to hot reload them
|
||||
//!
|
||||
//! - The byte index of the template is not the same as the byte index of the original template
|
||||
//! this forces us to make up IDs on the fly. We should just find an ID naming scheme, but that
|
||||
//! struggles when you have nested rsx! calls since file:line:col is the same for all expanded rsx!
|
||||
//!
|
||||
//! - There's lots of linear scans
|
||||
//!
|
||||
//! - Expanding an if chain is not possible - only its contents can be hot reloaded
|
||||
//!
|
||||
//! - Components that don't start with children can't be hotreloaded - IE going from `Comp {}` to `Comp { "foo" }`
|
||||
//! is not possible. We could in theory allow this by seeding all Components with a `children` field.
|
||||
//!
|
||||
//! - Cross-templates hot reloading is not possible - multiple templates don't share the dynamic nodes.
|
||||
//! This would require changes in core to work, I imagine.
|
||||
//!
|
||||
//! Future work
|
||||
//!
|
||||
//! - We've proven that binary patching is feasible but has a longer path to stabilization for all platforms.
|
||||
//! Binary patching is pretty quick, actually, and *might* remove the need to literal hotreloading.
|
||||
//! However, you could imagine a scenario where literal hotreloading would be useful without the
|
||||
//! compiler in the loop. Ideally we can slash most of this code once patching is stable.
|
||||
//!
|
||||
//! - We could also allow adding arbitrary nodes/attributes at runtime. The template system doesn't
|
||||
//! quite support that, unfortunately, since the number of dynamic nodes and attributes is baked into
|
||||
//! the template, but if that changed we'd be okay.
|
||||
|
||||
use crate::{innerlude::*, scoring::score_dynamic_node};
|
||||
use crate::{scoring::score_attribute, HotReloadingContext};
|
||||
use dioxus_core::{internal::HotReloadLiteral, Template};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// The mapping of a node relative to the root of its containing template
|
||||
///
|
||||
/// IE [0, 1] would be the location of the h3 node in this template:
|
||||
/// ```rust, ignore
|
||||
/// rsx! {
|
||||
/// div {
|
||||
/// h1 { "title" }
|
||||
/// h3 { class: "{class}", "Hi" }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
type NodePath = Vec<u8>;
|
||||
|
||||
/// The mapping of an attribute relative to the root of its containing template
|
||||
/// Order doesn't matter for attributes, you can render them in any order on a given node.a
|
||||
///
|
||||
/// IE [0, 1] would be the location of the `class` attribute on this template:
|
||||
/// ```rust, ignore
|
||||
/// rsx! {
|
||||
/// div {
|
||||
/// h1 { "title" }
|
||||
/// h3 { class: "{class}", "Hi" }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
type AttributePath = Vec<u8>;
|
||||
|
||||
/// A result of hot reloading
|
||||
///
|
||||
/// This contains information about what has changed so the hotreloader can apply the right changes
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct HotReloadedTemplate {
|
||||
/// List of inner templates that changed (nested blocks like for/if/component bodies)
|
||||
pub templates: Vec<Template>,
|
||||
|
||||
/// Previously changed lits that we're going to use to invalidate the old literals
|
||||
pub prev_lits: HashMap<String, HotReloadLiteral>,
|
||||
|
||||
/// A map of Signal IDs to the new literals
|
||||
/// Eventually we'll want to move this to a more efficient data structure to have one signal per rsx! call
|
||||
pub changed_lits: HashMap<String, HotReloadLiteral>,
|
||||
|
||||
// The location of the original call
|
||||
// This should be in the form of `file:line:col:0` - 0 since this will be the base template
|
||||
pub location: &'static str,
|
||||
}
|
||||
|
||||
impl HotReloadedTemplate {
|
||||
/// Calculate the hotreload diff between two callbodies
|
||||
pub fn new<Ctx: HotReloadingContext>(
|
||||
old: &CallBody,
|
||||
new: &CallBody,
|
||||
location: &'static str,
|
||||
old_lits: HashMap<String, HotReloadLiteral>,
|
||||
) -> Option<Self> {
|
||||
let mut s = Self {
|
||||
templates: Default::default(),
|
||||
changed_lits: Default::default(),
|
||||
prev_lits: old_lits,
|
||||
location,
|
||||
};
|
||||
|
||||
s.hotreload_body::<Ctx>(&old.body, &new.body)?;
|
||||
|
||||
Some(s)
|
||||
}
|
||||
|
||||
/// Walk the dynamic contexts and do our best to find hotreloadable changes between the two
|
||||
/// sets of dynamic nodes/attributes. If there's a change we can't hotreload, we'll return None
|
||||
///
|
||||
/// Otherwise, we pump out the list of templates that need to be updated. The templates will be
|
||||
/// re-ordered such that the node paths will be adjusted to match the new template for every
|
||||
/// existing dynamic node.
|
||||
///
|
||||
/// ```ignore
|
||||
/// old:
|
||||
/// [[0], [1], [2]]
|
||||
/// rsx! {
|
||||
/// "{one}"
|
||||
/// "{two}"
|
||||
/// "{three}"
|
||||
/// }
|
||||
///
|
||||
/// new:
|
||||
/// [[0], [2], [1, 1]]
|
||||
/// rsx! {
|
||||
/// "{one}"
|
||||
/// div { "{three}" }
|
||||
/// "{two}"
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Generally we can't hotreload a node if:
|
||||
/// - We add a truly dynaamic node (except maybe text nodes - but even then.. only if we've seen them before)
|
||||
///
|
||||
/// If a dynamic node is removed, we don't necessarily need to kill hotreload - just unmounting it should be enough
|
||||
/// If the dynamic node is re-added, we want to be able to find it again.
|
||||
///
|
||||
/// This encourages the hotreloader to hot onto DynamicContexts directly instead of the CallBody since
|
||||
/// you can preserve more information about the nodes as they've changed over time.
|
||||
pub fn hotreload_body<Ctx: HotReloadingContext>(
|
||||
&mut self,
|
||||
old: &TemplateBody,
|
||||
new: &TemplateBody,
|
||||
) -> Option<()> {
|
||||
// Quickly run through dynamic attributes first attempting to invalidate them
|
||||
// Move over old IDs onto the new template
|
||||
let new_attribute_paths = self.hotreload_attributes(old, new)?;
|
||||
|
||||
// Now we can run through the dynamic nodes and see if we can hot reload them
|
||||
// Move over old IDs onto the new template
|
||||
let new_node_paths = self.hotreload_dynamic_nodes::<Ctx>(old, new)?;
|
||||
|
||||
// Now render the new template out. We've proven that it's a close enough match to the old template
|
||||
//
|
||||
// The paths will be different but the dynamic indexes will be the same
|
||||
let template = new.to_template_with_custom_paths::<Ctx>(
|
||||
intern(self.make_location(old.template_idx.get()).leak()),
|
||||
new_node_paths,
|
||||
new_attribute_paths,
|
||||
);
|
||||
|
||||
self.templates.push(template);
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Take two dynamic contexts and return a mapping of dynamic attributes from the original to the new.
|
||||
///
|
||||
/// IE if we shuffle attributes around we should be able to hot reload them.
|
||||
/// Same thing with dropping dynamic attributes.
|
||||
///
|
||||
/// Does not apply with moving the dynamic contents from one attribute to another.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// rsx! {
|
||||
/// div { id: "{id}", class: "{class}", "Hi" }
|
||||
/// }
|
||||
///
|
||||
/// rsx! {
|
||||
/// div { class: "{class}", id: "{id}", "Hi" }
|
||||
/// }
|
||||
/// ```
|
||||
fn hotreload_attributes(
|
||||
&mut self,
|
||||
old: &TemplateBody,
|
||||
new: &TemplateBody,
|
||||
) -> Option<Vec<AttributePath>> {
|
||||
// Build a stack of old attributes so we can pop them off as we find matches in the new attributes
|
||||
//
|
||||
// Note that we might have duplicate attributes! We use a stack just to make sure we don't lose them
|
||||
// Also note that we use a vec + remove, but the idea is that in most cases we're removing from the end
|
||||
// which is an O(1) operation. We could use a linked list or a queue, but I don't want any
|
||||
// more complexity than necessary here since this can complex.
|
||||
let mut old_attrs = PopVec::new(old.dynamic_attributes());
|
||||
|
||||
// Now we can run through the dynamic nodes and see if we can hot reload them
|
||||
// Here we create the new attribute paths for the final template - we'll fill them in as we find matches
|
||||
let mut attr_paths = vec![vec![]; old.attr_paths.len()];
|
||||
|
||||
// Note that we walk the new attributes - we can remove segments from formatted text so
|
||||
// all `new` is a subset of `old`.
|
||||
for new_attr in new.dynamic_attributes() {
|
||||
// We're going to score the attributes based on their names and values
|
||||
// This ensures that we can handle the majority of cases where the attributes are shuffled around
|
||||
// or their contents have been stripped down
|
||||
//
|
||||
// A higher score is better - 0 is a mismatch, usize::MAX is a perfect match
|
||||
// As we find matches, the complexity of the search should reduce, making this quadratic
|
||||
// a little less painful
|
||||
let (old_idx, score) =
|
||||
old_attrs.highest_score(move |old_attr| score_attribute(old_attr, new_attr))?;
|
||||
|
||||
// Remove it from the stack so we don't match it again
|
||||
let old_attr = old_attrs.remove(old_idx).unwrap();
|
||||
|
||||
// This old node will now need to take on the new path
|
||||
attr_paths[old_attr.dyn_idx.get()] = new.attr_paths[new_attr.dyn_idx.get()].clone().0;
|
||||
|
||||
// Now move over the idx of the old to the new
|
||||
//
|
||||
// We're going to reuse the new CallBody to render the new template, so we have to make sure
|
||||
// stuff like IDs are ported over properly
|
||||
//
|
||||
// it's a little dumb to modify the new one in place, but it us avoid a lot of complexity
|
||||
// we should change the semantics of these methods to take the new one mutably, making it
|
||||
// clear that we're going to modify it in place and use it render
|
||||
new_attr.dyn_idx.set(old_attr.dyn_idx.get());
|
||||
|
||||
// While we're here, if it's a literal and not a perfect score, it's a mismatch and we need to
|
||||
// hotreload the literal
|
||||
self.hotreload_attribute(old_attr, new_attr, score)?;
|
||||
}
|
||||
|
||||
Some(attr_paths)
|
||||
}
|
||||
|
||||
fn hotreload_dynamic_nodes<Ctx: HotReloadingContext>(
|
||||
&mut self,
|
||||
old: &TemplateBody,
|
||||
new: &TemplateBody,
|
||||
) -> Option<Vec<NodePath>> {
|
||||
use BodyNode::*;
|
||||
let mut old_nodes = PopVec::new(old.dynamic_nodes());
|
||||
|
||||
let mut node_paths = vec![vec![]; old.node_paths.len()];
|
||||
|
||||
for new_node in new.dynamic_nodes() {
|
||||
// Find the best match for the new node - this is done by comparing the dynamic contents of the various nodes to
|
||||
// find the best fit.
|
||||
//
|
||||
// We do this since two components/textnodes/attributes *might* be similar in terms of dynamic contents
|
||||
// but not be the same node.
|
||||
let (old_idx, score) =
|
||||
old_nodes.highest_score(move |old_node| score_dynamic_node(old_node, new_node))?;
|
||||
|
||||
// Remove it from the stack so we don't match it again - this is O(1)
|
||||
let old_node = old_nodes.remove(old_idx)?;
|
||||
|
||||
// This old node will now need to take on the new path in the new template
|
||||
node_paths[old_node.get_dyn_idx()].clone_from(&new.node_paths[new_node.get_dyn_idx()]);
|
||||
|
||||
// But we also need to make sure the new node is taking on the old node's ID
|
||||
new_node.set_dyn_idx(old_node.get_dyn_idx());
|
||||
|
||||
// Make sure we descend into the children, and then record any changed literals
|
||||
match (old_node, new_node) {
|
||||
// If the contents of the text changed, then we need to hotreload the text node
|
||||
(Text(a), Text(b)) if score != usize::MAX => {
|
||||
self.hotreload_text_node(a, b)?;
|
||||
}
|
||||
|
||||
// We want to attempt to hotreload the component literals and the children
|
||||
(Component(a), Component(b)) => {
|
||||
self.hotreload_component_fields(a, b)?;
|
||||
self.hotreload_body::<Ctx>(&a.children, &b.children)?;
|
||||
}
|
||||
|
||||
// We don't reload the exprs or condition - just the bodies
|
||||
(ForLoop(a), ForLoop(b)) => {
|
||||
self.hotreload_body::<Ctx>(&a.body, &b.body)?;
|
||||
}
|
||||
|
||||
// Ensure the if chains are the same and then hotreload the bodies
|
||||
// We don't handle new chains or "elses" just yet - but feasibly we could allow
|
||||
// for an `else` chain to be added/removed.
|
||||
//
|
||||
// Our ifchain parser would need to be better to support this.
|
||||
(IfChain(a), IfChain(b)) => {
|
||||
self.hotreload_ifchain::<Ctx>(a, b)?;
|
||||
}
|
||||
|
||||
// Just assert we never get these cases - attributes are handled separately
|
||||
(Element(_), Element(_)) => unreachable!("Elements are not dynamic nodes"),
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Some(node_paths)
|
||||
}
|
||||
|
||||
fn hotreload_text_node(&mut self, a: &TextNode, b: &TextNode) -> Option<()> {
|
||||
let idx = a.hr_idx.get();
|
||||
let location = self.make_location(idx);
|
||||
let segments = IfmtInput::fmt_segments(&a.input, &b.input)?;
|
||||
self.changed_lits
|
||||
.insert(location.to_string(), HotReloadLiteral::Fmted(segments));
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn hotreload_component_fields(&mut self, a: &Component, b: &Component) -> Option<()> {
|
||||
// make sure both are the same length
|
||||
if a.fields.len() != b.fields.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut left_fields = a.fields.iter().collect::<Vec<_>>();
|
||||
left_fields.sort_by(|a, b| a.name.to_string().cmp(&b.name.to_string()));
|
||||
|
||||
let mut right_fields = b.fields.iter().collect::<Vec<_>>();
|
||||
right_fields.sort_by(|a, b| a.name.to_string().cmp(&b.name.to_string()));
|
||||
|
||||
// Walk the attributes looking for literals
|
||||
// Those will have plumbing in the hotreloading code
|
||||
// All others just get diffed via tokensa
|
||||
for (old_attr, new_attr) in left_fields.iter().zip(right_fields.iter()) {
|
||||
self.hotreload_attribute(old_attr, new_attr, score_attribute(old_attr, new_attr))?;
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn hotreload_attribute(
|
||||
&mut self,
|
||||
old_attr: &Attribute,
|
||||
new_attr: &Attribute,
|
||||
score: usize,
|
||||
) -> Option<()> {
|
||||
// If the score is 0, the name didn't match or the values didn't match
|
||||
// A score of usize::MAX means the attributes are the same
|
||||
if score == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// If it's a perfect match, we don't need to do anything special
|
||||
// ... well actually if it's a lit we need to invalidate the old lit
|
||||
// this is because a lit going from true -> false -> true doesn't count as a change from the diffing perspective
|
||||
if score == usize::MAX {
|
||||
// if score == usize::MAX && new_attr.as_lit().is_none() {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
// Prep the new literal
|
||||
let location = self.make_location(old_attr.as_lit().unwrap().hr_idx.get());
|
||||
|
||||
// If we have a perfect match and no old lit exists, then this didn't change
|
||||
if score == usize::MAX && self.prev_lits.remove(&location).is_none() {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
let out = match &new_attr.as_lit().unwrap().value {
|
||||
HotLiteralType::Float(f) => HotReloadLiteral::Float(f.base10_parse().unwrap()),
|
||||
HotLiteralType::Int(f) => HotReloadLiteral::Int(f.base10_parse().unwrap()),
|
||||
HotLiteralType::Bool(f) => HotReloadLiteral::Bool(f.value),
|
||||
HotLiteralType::Fmted(new) => HotReloadLiteral::Fmted(
|
||||
IfmtInput::fmt_segments(old_attr.ifmt().unwrap(), new)
|
||||
.expect("Fmt segments to generate"),
|
||||
),
|
||||
};
|
||||
|
||||
self.changed_lits.insert(location, out);
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn make_location(&self, idx: usize) -> String {
|
||||
format!("{}:{}", self.location.trim_end_matches(":0"), idx)
|
||||
}
|
||||
|
||||
/// Hot reload an if chain
|
||||
fn hotreload_ifchain<Ctx: HotReloadingContext>(
|
||||
&mut self,
|
||||
a: &IfChain,
|
||||
b: &IfChain,
|
||||
) -> Option<bool> {
|
||||
let matches = a.cond == b.cond;
|
||||
|
||||
if matches {
|
||||
let (mut elif_a, mut elif_b) = (Some(a), Some(b));
|
||||
|
||||
loop {
|
||||
// No point in continuing if we've hit the end of the chain
|
||||
if elif_a.is_none() && elif_b.is_none() {
|
||||
break;
|
||||
}
|
||||
|
||||
// We assume both exist branches exist
|
||||
let (a, b) = (elif_a.take()?, elif_b.take()?);
|
||||
|
||||
// Write the `then` branch
|
||||
self.hotreload_body::<Ctx>(&a.then_branch, &b.then_branch)?;
|
||||
|
||||
// If there's an elseif branch, we set that as the next branch
|
||||
// Otherwise we continue to the else branch - which we assume both branches have
|
||||
if let (Some(left), Some(right)) =
|
||||
(a.else_if_branch.as_ref(), b.else_if_branch.as_ref())
|
||||
{
|
||||
elif_a = Some(left.as_ref());
|
||||
elif_b = Some(right.as_ref());
|
||||
continue;
|
||||
}
|
||||
|
||||
// No else branches, that's fine
|
||||
if a.else_branch.is_none() && b.else_branch.is_none() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Write out the else branch and then we're done
|
||||
let (left, right) = (a.else_branch.as_ref()?, b.else_branch.as_ref()?);
|
||||
self.hotreload_body::<Ctx>(left, right)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Some(matches)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
#[cfg(feature = "hot_reload")]
|
||||
use dioxus_core::TemplateNode;
|
||||
|
||||
use crate::location::DynIdx;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::quote;
|
||||
use quote::{ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
Expr, ExprIf, Result, Token,
|
||||
};
|
||||
|
||||
use crate::TemplateBody;
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub struct IfChain {
|
||||
pub if_token: Token![if],
|
||||
pub cond: Box<Expr>,
|
||||
pub then_branch: TemplateBody,
|
||||
pub else_if_branch: Option<Box<IfChain>>,
|
||||
pub else_branch: Option<TemplateBody>,
|
||||
pub dyn_idx: DynIdx,
|
||||
}
|
||||
|
||||
impl IfChain {
|
||||
pub fn for_each_branch(&self, f: &mut impl FnMut(&TemplateBody)) {
|
||||
f(&self.then_branch);
|
||||
|
||||
if let Some(else_if) = &self.else_if_branch {
|
||||
else_if.for_each_branch(f);
|
||||
}
|
||||
|
||||
if let Some(else_branch) = &self.else_branch {
|
||||
f(else_branch);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hot_reload")]
|
||||
pub fn to_template_node(&self) -> TemplateNode {
|
||||
TemplateNode::Dynamic {
|
||||
id: self.dyn_idx.get(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for IfChain {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let if_token: Token![if] = input.parse()?;
|
||||
|
||||
// stolen from ExprIf
|
||||
let cond = Box::new(input.call(Expr::parse_without_eager_brace)?);
|
||||
|
||||
let content;
|
||||
syn::braced!(content in input);
|
||||
|
||||
let then_branch = content.parse()?;
|
||||
|
||||
let mut else_branch = None;
|
||||
let mut else_if_branch = None;
|
||||
|
||||
// if the next token is `else`, set the else branch as the next if chain
|
||||
if input.peek(Token![else]) {
|
||||
input.parse::<Token![else]>()?;
|
||||
if input.peek(Token![if]) {
|
||||
else_if_branch = Some(Box::new(input.parse::<IfChain>()?));
|
||||
} else {
|
||||
let content;
|
||||
syn::braced!(content in input);
|
||||
else_branch = Some(content.parse()?);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
cond,
|
||||
if_token,
|
||||
then_branch,
|
||||
else_if_branch,
|
||||
else_branch,
|
||||
dyn_idx: DynIdx::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for IfChain {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let mut body = TokenStream2::new();
|
||||
let mut terminated = false;
|
||||
|
||||
let mut elif = Some(self);
|
||||
|
||||
while let Some(chain) = elif {
|
||||
let IfChain {
|
||||
if_token,
|
||||
cond,
|
||||
then_branch,
|
||||
else_if_branch,
|
||||
else_branch,
|
||||
..
|
||||
} = chain;
|
||||
|
||||
body.append_all(quote! {
|
||||
#if_token #cond {
|
||||
{ #then_branch }
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(next) = else_if_branch {
|
||||
body.append_all(quote! {
|
||||
else
|
||||
});
|
||||
elif = Some(next);
|
||||
} else if let Some(else_branch) = else_branch {
|
||||
body.append_all(quote! {
|
||||
else {
|
||||
{#else_branch}
|
||||
}
|
||||
});
|
||||
terminated = true;
|
||||
break;
|
||||
} else {
|
||||
elif = None;
|
||||
}
|
||||
}
|
||||
|
||||
if !terminated {
|
||||
body.append_all(quote! {
|
||||
else { dioxus_core::VNode::empty() }
|
||||
});
|
||||
}
|
||||
|
||||
tokens.append_all(quote! {
|
||||
{
|
||||
let ___nodes = (#body).into_dyn_node();
|
||||
___nodes
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_if_chain_terminated(chain: &ExprIf) -> bool {
|
||||
let mut current = chain;
|
||||
loop {
|
||||
if let Some((_, else_block)) = ¤t.else_branch {
|
||||
if let Expr::If(else_if) = else_block.as_ref() {
|
||||
current = else_if;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_if_chain() {
|
||||
let input = quote! {
|
||||
if true {
|
||||
"one"
|
||||
} else if false {
|
||||
"two"
|
||||
} else {
|
||||
"three"
|
||||
}
|
||||
};
|
||||
|
||||
let _chain: IfChain = syn::parse2(input).unwrap();
|
||||
}
|
||||
+230
-59
@@ -1,49 +1,64 @@
|
||||
use std::str::FromStr;
|
||||
#[cfg(feature = "hot_reload")]
|
||||
use dioxus_core::internal::{FmtSegment, FmtedSegments};
|
||||
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
|
||||
use quote::{quote, quote_spanned, ToTokens, TokenStreamExt};
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
*,
|
||||
};
|
||||
|
||||
pub fn format_args_f_impl(input: IfmtInput) -> Result<TokenStream> {
|
||||
Ok(input.into_token_stream())
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // dumb compiler does not see the struct being used...
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)]
|
||||
/// A hot-reloadable formatted string, boolean, number or other literal
|
||||
///
|
||||
/// This wraps LitStr with some extra goodies like inline expressions and hot-reloading.
|
||||
/// Originally this was intended to provide named inline string interpolation but eventually Rust
|
||||
/// actualy shipped this!
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
|
||||
pub struct IfmtInput {
|
||||
pub source: Option<LitStr>,
|
||||
pub source: LitStr,
|
||||
pub segments: Vec<Segment>,
|
||||
}
|
||||
|
||||
impl IfmtInput {
|
||||
pub fn new_static(input: &str) -> Self {
|
||||
pub fn new(span: Span) -> Self {
|
||||
Self {
|
||||
source: None,
|
||||
segments: vec![Segment::Literal(input.to_string())],
|
||||
source: LitStr::new("", span),
|
||||
segments: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn join(mut self, other: Self, separator: &str) -> Self {
|
||||
if !self.segments.is_empty() {
|
||||
self.segments.push(Segment::Literal(separator.to_string()));
|
||||
}
|
||||
pub fn new_litstr(source: LitStr) -> Self {
|
||||
let segments = Self::from_raw(&source.value()).unwrap();
|
||||
Self { segments, source }
|
||||
}
|
||||
|
||||
pub fn span(&self) -> Span {
|
||||
self.source.span()
|
||||
}
|
||||
|
||||
pub fn push_raw_str(&mut self, other: String) {
|
||||
self.segments.push(Segment::Literal(other.to_string()))
|
||||
}
|
||||
|
||||
pub fn push_ifmt(&mut self, other: IfmtInput) {
|
||||
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
|
||||
}
|
||||
|
||||
pub fn push_condition(&mut self, condition: Expr, contents: IfmtInput) {
|
||||
let desugared = quote! {
|
||||
{
|
||||
let _cond = if #condition { #contents.to_string() } else { String::new() };
|
||||
_cond
|
||||
}
|
||||
};
|
||||
|
||||
let parsed = syn::parse2::<Expr>(desugared).unwrap();
|
||||
|
||||
self.segments.push(Segment::Formatted(FormattedSegment {
|
||||
format_args: String::new(),
|
||||
segment: FormattedSegmentType::Expr(Box::new(parsed)),
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn push_expr(&mut self, expr: Expr) {
|
||||
@@ -53,24 +68,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 {
|
||||
self.segments
|
||||
.iter()
|
||||
.all(|seg| matches!(seg, Segment::Literal(_)))
|
||||
}
|
||||
}
|
||||
|
||||
impl IfmtInput {
|
||||
pub fn to_static(&self) -> Option<String> {
|
||||
self.segments
|
||||
.iter()
|
||||
@@ -83,6 +86,82 @@ impl IfmtInput {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn dynamic_segments(&self) -> Vec<&FormattedSegment> {
|
||||
self.segments
|
||||
.iter()
|
||||
.filter_map(|seg| match seg {
|
||||
Segment::Formatted(seg) => Some(seg),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn dynamic_seg_frequency_map(&self) -> HashMap<&FormattedSegment, usize> {
|
||||
let mut map = HashMap::new();
|
||||
for seg in self.dynamic_segments() {
|
||||
*map.entry(seg).or_insert(0) += 1;
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
#[cfg(feature = "hot_reload")]
|
||||
pub fn fmt_segments(old: &Self, new: &Self) -> Option<FmtedSegments> {
|
||||
use crate::intern;
|
||||
|
||||
// Make sure all the dynamic segments of b show up in a
|
||||
for segment in new.segments.iter() {
|
||||
if segment.is_formatted() && !old.segments.contains(segment) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all the formatted segments from the original
|
||||
let mut out = vec![];
|
||||
|
||||
// the original list of formatted segments
|
||||
let mut fmted = old
|
||||
.segments
|
||||
.iter()
|
||||
.flat_map(|f| match f {
|
||||
crate::Segment::Literal(_) => None,
|
||||
crate::Segment::Formatted(f) => Some(f),
|
||||
})
|
||||
.cloned()
|
||||
.map(Some)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for segment in new.segments.iter() {
|
||||
match segment {
|
||||
crate::Segment::Literal(lit) => {
|
||||
// create a &'static str by leaking the string
|
||||
let lit = intern(lit.clone().into_boxed_str());
|
||||
out.push(FmtSegment::Literal { value: lit });
|
||||
}
|
||||
crate::Segment::Formatted(fmt) => {
|
||||
// Find the formatted segment in the original
|
||||
// Set it to None when we find it so we don't re-render it on accident
|
||||
let idx = fmted
|
||||
.iter_mut()
|
||||
.position(|_s| {
|
||||
if let Some(s) = _s {
|
||||
if s == fmt {
|
||||
*_s = None;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
out.push(FmtSegment::Dynamic { id: idx });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(FmtedSegments::new(out))
|
||||
}
|
||||
|
||||
fn is_simple_expr(&self) -> bool {
|
||||
self.segments.iter().all(|seg| match seg {
|
||||
Segment::Literal(_) => true,
|
||||
@@ -126,12 +205,14 @@ impl IfmtInput {
|
||||
}
|
||||
single_dynamic
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for IfmtInput {
|
||||
type Err = syn::Error;
|
||||
/// print the original source string - this handles escapes and stuff for us
|
||||
pub fn to_string_with_quotes(&self) -> String {
|
||||
self.source.to_token_stream().to_string()
|
||||
}
|
||||
|
||||
fn from_str(input: &str) -> Result<Self> {
|
||||
/// Parse the source into segments
|
||||
fn from_raw(input: &str) -> Result<Vec<Segment>> {
|
||||
let mut chars = input.chars().peekable();
|
||||
let mut segments = Vec::new();
|
||||
let mut current_literal = String::new();
|
||||
@@ -190,13 +271,12 @@ impl FromStr for IfmtInput {
|
||||
current_literal.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
if !current_literal.is_empty() {
|
||||
segments.push(Segment::Literal(current_literal));
|
||||
}
|
||||
Ok(Self {
|
||||
segments,
|
||||
source: None,
|
||||
})
|
||||
|
||||
Ok(segments)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,10 +316,7 @@ impl ToTokens for IfmtInput {
|
||||
}
|
||||
}
|
||||
|
||||
let span = match self.source.as_ref() {
|
||||
Some(source) => source.span(),
|
||||
None => Span::call_site(),
|
||||
};
|
||||
let span = self.span();
|
||||
|
||||
let positional_args = self.segments.iter().filter_map(|seg| {
|
||||
if let Segment::Formatted(FormattedSegment { segment, .. }) = seg {
|
||||
@@ -271,10 +348,20 @@ pub enum Segment {
|
||||
Formatted(FormattedSegment),
|
||||
}
|
||||
|
||||
impl Segment {
|
||||
pub fn is_literal(&self) -> bool {
|
||||
matches!(self, Segment::Literal(_))
|
||||
}
|
||||
|
||||
pub fn is_formatted(&self) -> bool {
|
||||
matches!(self, Segment::Formatted(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
|
||||
pub struct FormattedSegment {
|
||||
format_args: String,
|
||||
segment: FormattedSegmentType,
|
||||
pub format_args: String,
|
||||
pub segment: FormattedSegmentType,
|
||||
}
|
||||
|
||||
impl ToTokens for FormattedSegment {
|
||||
@@ -320,12 +407,96 @@ impl ToTokens for FormattedSegmentType {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for IfmtInput {
|
||||
type Err = syn::Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self> {
|
||||
let segments = IfmtInput::from_raw(input)?;
|
||||
Ok(Self {
|
||||
source: LitStr::new(input, Span::call_site()),
|
||||
segments,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for IfmtInput {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let input: LitStr = input.parse()?;
|
||||
let input_str = input.value();
|
||||
let mut ifmt = IfmtInput::from_str(&input_str)?;
|
||||
ifmt.source = Some(input);
|
||||
Ok(ifmt)
|
||||
let source: LitStr = input.parse()?;
|
||||
let segments = IfmtInput::from_raw(&source.value())?;
|
||||
Ok(Self { source, segments })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::PrettyUnparse;
|
||||
|
||||
#[test]
|
||||
fn raw_tokens() {
|
||||
let input = syn::parse2::<IfmtInput>(quote! { r#"hello world"# }).unwrap();
|
||||
println!("{}", input.to_token_stream().pretty_unparse());
|
||||
assert_eq!(input.source.value(), "hello world");
|
||||
assert_eq!(input.to_string_with_quotes(), "r#\"hello world\"#");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn segments_parse() {
|
||||
let input = "blah {abc} {def}".parse::<IfmtInput>().unwrap();
|
||||
assert_eq!(
|
||||
input.segments,
|
||||
vec![
|
||||
Segment::Literal("blah ".to_string()),
|
||||
Segment::Formatted(FormattedSegment {
|
||||
format_args: String::new(),
|
||||
segment: FormattedSegmentType::Ident(Ident::new("abc", Span::call_site()))
|
||||
}),
|
||||
Segment::Literal(" ".to_string()),
|
||||
Segment::Formatted(FormattedSegment {
|
||||
format_args: String::new(),
|
||||
segment: FormattedSegmentType::Ident(Ident::new("def", Span::call_site()))
|
||||
}),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn printing_raw() {
|
||||
let input = syn::parse2::<IfmtInput>(quote! { "hello {world}" }).unwrap();
|
||||
println!("{}", input.to_string_with_quotes());
|
||||
|
||||
let input = syn::parse2::<IfmtInput>(quote! { "hello {world} {world} {world}" }).unwrap();
|
||||
println!("{}", input.to_string_with_quotes());
|
||||
|
||||
let input = syn::parse2::<IfmtInput>(quote! { "hello {world} {world} {world()}" }).unwrap();
|
||||
println!("{}", input.to_string_with_quotes());
|
||||
|
||||
let input =
|
||||
syn::parse2::<IfmtInput>(quote! { r#"hello {world} {world} {world()}"# }).unwrap();
|
||||
println!("{}", input.to_string_with_quotes());
|
||||
assert!(!input.is_static());
|
||||
|
||||
let input = syn::parse2::<IfmtInput>(quote! { r#"hello"# }).unwrap();
|
||||
println!("{}", input.to_string_with_quotes());
|
||||
assert!(input.is_static());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pushing_conditional() {
|
||||
let mut input = syn::parse2::<IfmtInput>(quote! { "hello " }).unwrap();
|
||||
|
||||
input.push_condition(
|
||||
parse_quote! { true },
|
||||
syn::parse2::<IfmtInput>(quote! { "world" }).unwrap(),
|
||||
);
|
||||
println!("{}", input.to_token_stream().pretty_unparse());
|
||||
dbg!(input.segments);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fmt_segments() {
|
||||
let left = syn::parse2::<IfmtInput>(quote! { "thing {abc}" }).unwrap();
|
||||
let right = syn::parse2::<IfmtInput>(quote! { "thing" }).unwrap();
|
||||
let _segments = IfmtInput::fmt_segments(&left, &right).unwrap();
|
||||
}
|
||||
}
|
||||
+45
-158
@@ -1,7 +1,7 @@
|
||||
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
|
||||
//! Parse the root tokens in the rsx!{} macro
|
||||
//! Parse the root tokens in the rsx!{ } macro
|
||||
//! =========================================
|
||||
//!
|
||||
//! This parsing path emerges directly from the macro call, with `RsxRender` being the primary entrance into parsing.
|
||||
@@ -49,185 +49,72 @@
|
||||
//! dioxus_elements::elements::di
|
||||
//! ```
|
||||
|
||||
#[macro_use]
|
||||
mod errors;
|
||||
mod attribute;
|
||||
mod component;
|
||||
mod element;
|
||||
mod ifmt;
|
||||
mod location;
|
||||
mod forloop;
|
||||
mod ifchain;
|
||||
mod node;
|
||||
mod raw_expr;
|
||||
mod rsx_block;
|
||||
mod rsx_call;
|
||||
mod template_body;
|
||||
mod text_node;
|
||||
|
||||
mod diagnostics;
|
||||
mod expr_node;
|
||||
pub mod hotreload;
|
||||
mod ifmt;
|
||||
mod literal;
|
||||
mod location;
|
||||
mod partial_closure;
|
||||
mod reload_stack;
|
||||
mod scoring;
|
||||
mod util;
|
||||
|
||||
pub(crate) mod context;
|
||||
pub(crate) mod renderer;
|
||||
mod sub_templates;
|
||||
|
||||
// Re-export the namespaces into each other
|
||||
pub use attribute::*;
|
||||
pub use component::*;
|
||||
pub use context::DynamicContext;
|
||||
pub use element::*;
|
||||
pub use diagnostics::Diagnostics;
|
||||
pub use ifmt::*;
|
||||
pub use node::*;
|
||||
pub use partial_closure::PartialClosure;
|
||||
pub use rsx_call::*;
|
||||
pub use template_body::TemplateBody;
|
||||
|
||||
pub use util::PrettyUnparse;
|
||||
|
||||
pub mod hot_reload;
|
||||
|
||||
#[cfg(feature = "hot_reload")]
|
||||
use dioxus_core::{Template, TemplateAttribute, TemplateNode};
|
||||
#[cfg(feature = "hot_reload_traits")]
|
||||
pub use hot_reload::HotReloadingContext;
|
||||
#[cfg(feature = "hot_reload")]
|
||||
use internment::Intern;
|
||||
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use renderer::TemplateRenderer;
|
||||
use std::{fmt::Debug, hash::Hash};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
Result, Token,
|
||||
};
|
||||
|
||||
/// The Callbody is the contents of the rsx! macro
|
||||
///
|
||||
/// It is a list of BodyNodes, which are the different parts of the template.
|
||||
/// The Callbody contains no information about how the template will be rendered, only information about the parsed tokens.
|
||||
///
|
||||
/// Every callbody should be valid, so you can use it to build a template.
|
||||
/// To generate the code used to render the template, use the ToTokens impl on the Callbody, or with the `render_with_location` method.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct CallBody {
|
||||
pub roots: Vec<BodyNode>,
|
||||
}
|
||||
pub use innerlude::*;
|
||||
pub(crate) mod innerlude {
|
||||
pub use crate::attribute::*;
|
||||
pub use crate::component::*;
|
||||
pub use crate::element::*;
|
||||
pub use crate::expr_node::*;
|
||||
pub use crate::forloop::*;
|
||||
pub use crate::ifchain::*;
|
||||
pub use crate::location::*;
|
||||
pub use crate::node::*;
|
||||
pub use crate::raw_expr::*;
|
||||
pub use crate::rsx_block::*;
|
||||
pub use crate::rsx_call::*;
|
||||
pub use crate::template_body::*;
|
||||
pub use crate::text_node::*;
|
||||
|
||||
impl CallBody {
|
||||
/// Render the template with a manually set file location. This should be used when multiple rsx! calls are used in the same macro
|
||||
pub fn render_with_location(&self, location: String) -> TokenStream2 {
|
||||
// Empty templates just are placeholders for "none"
|
||||
if self.roots.is_empty() {
|
||||
return quote! { None };
|
||||
}
|
||||
pub use crate::diagnostics::*;
|
||||
pub use crate::ifmt::*;
|
||||
pub use crate::literal::*;
|
||||
pub use crate::reload_stack::*;
|
||||
pub use crate::util::*;
|
||||
|
||||
let body = TemplateRenderer::as_tokens(&self.roots, Some(location));
|
||||
|
||||
quote! { { #body } }
|
||||
}
|
||||
|
||||
/// This will try to create a new template from the current body and the previous body. This will return None if the
|
||||
/// rsx has some dynamic part that has changed.
|
||||
///
|
||||
/// The previous_location is the location of the previous template at the time the template was originally compiled.
|
||||
/// It's up to you the implementor to trace the template location back to the original source code. Generally you
|
||||
/// can simply just match the location from the syn::File type to the template map living in the renderer.
|
||||
///
|
||||
/// When you implement hotreloading, you're likely just going to parse the source code into the Syn::File type, which
|
||||
/// should make retrieving the template location easy.
|
||||
///
|
||||
/// ## Note:
|
||||
///
|
||||
/// - This function intentionally leaks memory to create a static template.
|
||||
/// - Keeping the template static allows us to simplify the core of dioxus and leaking memory in dev mode is less of an issue.
|
||||
///
|
||||
/// ## Longer note about sub templates:
|
||||
///
|
||||
/// Sub templates when expanded in rustc use the same file/lin/col information as the parent template. This can
|
||||
/// be annoying when you're trying to get a location for a sub template and it's pretending that it's its parent.
|
||||
/// The new implementation of this aggregates all subtemplates into the TemplateRenderer and then assigns them
|
||||
/// unique IDs based on the byte index of the template, working around this issue.
|
||||
///
|
||||
/// ## TODO:
|
||||
///
|
||||
/// A longer term goal would be to provide some sort of diagnostics to the user as to why the template was not
|
||||
/// updated, giving them an option to revert to the previous template as to not require a full rebuild.
|
||||
#[cfg(feature = "hot_reload")]
|
||||
pub fn update_template<Ctx: HotReloadingContext>(
|
||||
&self,
|
||||
old: Option<CallBody>,
|
||||
location: &'static str,
|
||||
) -> Option<Template> {
|
||||
// Create a context that will be used to update the template
|
||||
let mut context = DynamicContext::new_with_old(old);
|
||||
|
||||
// Force the template node to generate us TemplateNodes, and fill in the location information
|
||||
let roots = context.populate_by_updating::<Ctx>(&self.roots)?;
|
||||
|
||||
// We've received the dioxus-core TemplateNodess, and need to assemble them into a Template
|
||||
// We could just use them directly, but we want to intern them to do our best to avoid
|
||||
// egregious memory leaks. We're sitll leaking memory, but at least we can blame it on
|
||||
// the `Intern` crate and not just the fact that we call Box::leak.
|
||||
//
|
||||
// We should also note that order of these nodes could be all scrambeled
|
||||
Some(Template {
|
||||
name: location,
|
||||
roots: intern(roots.as_slice()),
|
||||
node_paths: intern(
|
||||
context
|
||||
.node_paths
|
||||
.into_iter()
|
||||
.map(|path| intern(path.as_slice()))
|
||||
.collect::<Vec<_>>()
|
||||
.as_slice(),
|
||||
),
|
||||
attr_paths: intern(
|
||||
context
|
||||
.attr_paths
|
||||
.into_iter()
|
||||
.map(|path| intern(path.as_slice()))
|
||||
.collect::<Vec<_>>()
|
||||
.as_slice(),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
/// 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 = BodyNode::parse_with_options(input, partial_completions)?;
|
||||
|
||||
if input.peek(Token![,]) {
|
||||
let _ = input.parse::<Token![,]>();
|
||||
}
|
||||
|
||||
roots.push(node);
|
||||
}
|
||||
|
||||
Ok(CallBody { roots })
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
match self.roots.is_empty() {
|
||||
true => out_tokens
|
||||
.append_all(quote! { dioxus_core::prelude::Element::Ok(dioxus_core::prelude::VNode::placeholder()) }),
|
||||
false => {
|
||||
let body = TemplateRenderer::as_tokens(&self.roots, None);
|
||||
out_tokens.append_all(quote! { { #body } })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hot_reload")]
|
||||
// interns a object into a static object, resusing the value if it already exists
|
||||
pub(crate) fn intern<T: Eq + Hash + Send + Sync + ?Sized + 'static>(
|
||||
s: impl Into<Intern<T>>,
|
||||
) -> &'static T {
|
||||
s.into().as_ref()
|
||||
pub use crate::hotreload::*;
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
use proc_macro2::Span;
|
||||
use quote::ToTokens;
|
||||
use quote::{quote, TokenStreamExt};
|
||||
use std::fmt::Display;
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
Lit, LitBool, LitFloat, LitInt, LitStr,
|
||||
};
|
||||
|
||||
use crate::{location::DynIdx, IfmtInput, Segment};
|
||||
|
||||
/// A literal value in the rsx! macro
|
||||
///
|
||||
/// These get hotreloading super powers, making them a bit more complex than a normal literal.
|
||||
/// In debug mode we need to generate a bunch of extra code to support hotreloading.
|
||||
///
|
||||
/// Eventually we want to remove this notion of hot literals since we're generating different code
|
||||
/// in debug than in release, which is harder to maintain and can lead to bugs.
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
||||
pub struct HotLiteral {
|
||||
pub value: HotLiteralType,
|
||||
pub hr_idx: DynIdx,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
||||
pub enum HotLiteralType {
|
||||
/// A *formatted* string literal
|
||||
/// We know this will generate a String, not an &'static str
|
||||
///
|
||||
/// The raw str type will generate a &'static str, but we need to distinguish the two for component props
|
||||
///
|
||||
/// "hello {world}"
|
||||
Fmted(IfmtInput),
|
||||
|
||||
/// A float literal
|
||||
///
|
||||
/// 1.0
|
||||
Float(LitFloat),
|
||||
|
||||
/// An int literal
|
||||
///
|
||||
/// 1
|
||||
Int(LitInt),
|
||||
|
||||
/// A bool literal
|
||||
///
|
||||
/// true
|
||||
Bool(LitBool),
|
||||
}
|
||||
|
||||
impl Parse for HotLiteral {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let raw = input.parse::<Lit>()?;
|
||||
|
||||
let value = match raw.clone() {
|
||||
Lit::Int(a) => HotLiteralType::Int(a),
|
||||
Lit::Bool(a) => HotLiteralType::Bool(a),
|
||||
Lit::Float(a) => HotLiteralType::Float(a),
|
||||
Lit::Str(a) => HotLiteralType::Fmted(IfmtInput::new_litstr(a)),
|
||||
_ => {
|
||||
return Err(syn::Error::new(
|
||||
raw.span(),
|
||||
"Only string, int, float, and bool literals are supported",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(HotLiteral {
|
||||
value,
|
||||
hr_idx: DynIdx::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for HotLiteral {
|
||||
fn to_tokens(&self, out: &mut proc_macro2::TokenStream) {
|
||||
let val = match &self.value {
|
||||
HotLiteralType::Fmted(fmt) if fmt.is_static() => {
|
||||
let o = fmt.to_static().unwrap().to_token_stream();
|
||||
quote! { #o }
|
||||
}
|
||||
|
||||
HotLiteralType::Fmted(fmt) => {
|
||||
let mut idx = 0_usize;
|
||||
let segments = fmt.segments.iter().map(|s| match s {
|
||||
Segment::Literal(lit) => quote! {
|
||||
dioxus_core::internal::FmtSegment::Literal { value: #lit }
|
||||
},
|
||||
Segment::Formatted(_fmt) => {
|
||||
// increment idx for the dynamic segment so we maintain the mapping
|
||||
let _idx = idx;
|
||||
idx += 1;
|
||||
quote! {
|
||||
dioxus_core::internal::FmtSegment::Dynamic { id: #_idx }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// The static segments with idxs for locations
|
||||
quote! {
|
||||
dioxus_core::internal::FmtedSegments::new( vec![ #(#segments),* ], )
|
||||
}
|
||||
}
|
||||
HotLiteralType::Float(a) => quote! { #a },
|
||||
HotLiteralType::Int(a) => quote! { #a },
|
||||
HotLiteralType::Bool(a) => quote! { #a },
|
||||
};
|
||||
|
||||
let mapped = match &self.value {
|
||||
HotLiteralType::Fmted(f) if f.is_static() => quote! { .clone() as &'static str},
|
||||
|
||||
HotLiteralType::Fmted(segments) => {
|
||||
let rendered_segments = segments.segments.iter().filter_map(|s| match s {
|
||||
Segment::Literal(_lit) => None,
|
||||
Segment::Formatted(fmt) => {
|
||||
// just render as a format_args! call
|
||||
Some(quote! { #fmt.to_string() })
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
.render_with(vec![ #(#rendered_segments),* ])
|
||||
}
|
||||
}
|
||||
HotLiteralType::Float(_) => quote! { .clone() },
|
||||
HotLiteralType::Int(_) => quote! { .clone() },
|
||||
HotLiteralType::Bool(_) => quote! { .clone() },
|
||||
};
|
||||
|
||||
let as_lit = match &self.value {
|
||||
HotLiteralType::Fmted(f) if f.is_static() => {
|
||||
let r = f.to_static().unwrap();
|
||||
quote! { #r }
|
||||
}
|
||||
HotLiteralType::Fmted(f) => f.to_token_stream(),
|
||||
HotLiteralType::Float(f) => f.to_token_stream(),
|
||||
HotLiteralType::Int(f) => f.to_token_stream(),
|
||||
HotLiteralType::Bool(f) => f.to_token_stream(),
|
||||
};
|
||||
|
||||
let map_lit = match &self.value {
|
||||
HotLiteralType::Fmted(f) if f.is_static() => quote! { .clone() },
|
||||
HotLiteralType::Fmted(_) => quote! { .to_string() },
|
||||
HotLiteralType::Float(_) => quote! { .clone() },
|
||||
HotLiteralType::Int(_) => quote! { .clone() },
|
||||
HotLiteralType::Bool(_) => quote! { .clone() },
|
||||
};
|
||||
|
||||
let hr_idx = self.hr_idx.get().to_string();
|
||||
|
||||
out.append_all(quote! {
|
||||
{
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
// in debug we still want these tokens to turn into fmt args such that RA can line
|
||||
// them up, giving us rename powersa
|
||||
_ = #as_lit;
|
||||
|
||||
// The key is important here - we're creating a new GlobalSignal each call to this/
|
||||
// But the key is what's keeping it stable
|
||||
GlobalSignal::with_key(
|
||||
|| #val, {
|
||||
concat!(
|
||||
file!(),
|
||||
":",
|
||||
line!(),
|
||||
":",
|
||||
column!(),
|
||||
":",
|
||||
#hr_idx
|
||||
)
|
||||
})
|
||||
.maybe_with_rt(|s| s #mapped)
|
||||
}
|
||||
|
||||
// just render the literal directly
|
||||
#[cfg(not(debug_assertions))]
|
||||
{ #as_lit #map_lit }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HotLiteralType {
|
||||
fn span(&self) -> Span {
|
||||
match self {
|
||||
HotLiteralType::Fmted(f) => f.span(),
|
||||
HotLiteralType::Float(f) => f.span(),
|
||||
HotLiteralType::Int(f) => f.span(),
|
||||
HotLiteralType::Bool(f) => f.span(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HotLiteral {
|
||||
// We can only handle a few types of literals - the rest need to be expressions
|
||||
// todo on adding more of course - they're not hard to support, just work
|
||||
pub fn peek(input: ParseStream) -> bool {
|
||||
if input.peek(Lit) {
|
||||
let lit = input.fork().parse::<Lit>().unwrap();
|
||||
|
||||
matches!(
|
||||
lit,
|
||||
Lit::Str(_) | Lit::Int(_) | Lit::Float(_) | Lit::Bool(_)
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_static(&self) -> bool {
|
||||
match &self.value {
|
||||
HotLiteralType::Fmted(fmt) => fmt.is_static(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn span(&self) -> Span {
|
||||
self.value.span()
|
||||
}
|
||||
|
||||
pub fn from_raw_text(text: &str) -> Self {
|
||||
HotLiteral {
|
||||
value: crate::HotLiteralType::Fmted(IfmtInput {
|
||||
source: LitStr::new(text, Span::call_site()),
|
||||
segments: vec![],
|
||||
}),
|
||||
hr_idx: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for HotLiteral {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match &self.value {
|
||||
HotLiteralType::Fmted(l) => l.to_string_with_quotes().fmt(f),
|
||||
HotLiteralType::Float(l) => l.fmt(f),
|
||||
HotLiteralType::Int(l) => l.fmt(f),
|
||||
HotLiteralType::Bool(l) => l.value().fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::PrettyUnparse;
|
||||
|
||||
#[test]
|
||||
fn parses_lits() {
|
||||
let _ = syn::parse2::<HotLiteral>(quote! { "hello" }).unwrap();
|
||||
let _ = syn::parse2::<HotLiteral>(quote! { "hello {world}" }).unwrap();
|
||||
let _ = syn::parse2::<HotLiteral>(quote! { 1 }).unwrap();
|
||||
let _ = syn::parse2::<HotLiteral>(quote! { 1.0 }).unwrap();
|
||||
let _ = syn::parse2::<HotLiteral>(quote! { false }).unwrap();
|
||||
let _ = syn::parse2::<HotLiteral>(quote! { true }).unwrap();
|
||||
|
||||
// Refuses the other unsupported types - we could add them if we wanted to
|
||||
assert!(syn::parse2::<HotLiteral>(quote! { b"123" }).is_err());
|
||||
assert!(syn::parse2::<HotLiteral>(quote! { 'a' }).is_err());
|
||||
|
||||
let lit = syn::parse2::<HotLiteral>(quote! { "hello" }).unwrap();
|
||||
assert!(matches!(lit.value, HotLiteralType::Fmted(_)));
|
||||
|
||||
let lit = syn::parse2::<HotLiteral>(quote! { "hello {world}" }).unwrap();
|
||||
assert!(matches!(lit.value, HotLiteralType::Fmted(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outputs_a_signal() {
|
||||
// Should output a type of f64 which we convert into whatever the expected type is via "into"
|
||||
// todo: hmmmmmmmmmmmm might not always work
|
||||
let lit = syn::parse2::<HotLiteral>(quote! { 1.0 }).unwrap();
|
||||
println!("{}", lit.to_token_stream().pretty_unparse());
|
||||
|
||||
let lit = syn::parse2::<HotLiteral>(quote! { "hi" }).unwrap();
|
||||
println!("{}", lit.to_token_stream().pretty_unparse());
|
||||
|
||||
let lit = syn::parse2::<HotLiteral>(quote! { "hi {world}" }).unwrap();
|
||||
println!("{}", lit.to_token_stream().pretty_unparse());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_str_becomes_str() {
|
||||
let lit = syn::parse2::<HotLiteral>(quote! { "hello" }).unwrap();
|
||||
let HotLiteralType::Fmted(segments) = &lit.value else {
|
||||
panic!("expected a formatted string");
|
||||
};
|
||||
assert!(segments.is_static());
|
||||
assert_eq!(r##""hello""##, segments.to_string_with_quotes());
|
||||
println!("{}", lit.to_token_stream().pretty_unparse());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formatted_prints_as_formatted() {
|
||||
let lit = syn::parse2::<HotLiteral>(quote! { "hello {world}" }).unwrap();
|
||||
let HotLiteralType::Fmted(segments) = &lit.value else {
|
||||
panic!("expected a formatted string");
|
||||
};
|
||||
assert!(!segments.is_static());
|
||||
assert_eq!(r##""hello {world}""##, segments.to_string_with_quotes());
|
||||
println!("{}", lit.to_token_stream().pretty_unparse());
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,42 @@
|
||||
/// Information about the location of the call to a component
|
||||
use std::{cell::Cell, hash::Hash};
|
||||
|
||||
/// A simple idx in the code that can be used to track back to the original source location
|
||||
///
|
||||
/// This will be filled in when the dynamiccontext is built, filling in the file:line:column:id format
|
||||
/// Used in two places:
|
||||
/// - In the `CallBody` to track the location of hotreloadable literals
|
||||
/// - In the `Body` to track the ID of each template
|
||||
///
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, Default)]
|
||||
pub struct CallerLocation {
|
||||
inner: Option<String>,
|
||||
/// We need an ID system, unfortunately, to properly disambiguate between different templates since
|
||||
/// rustc assigns them all the same line!() and column!() information. Before, we hashed spans but
|
||||
/// that has collision issues and is eventually relied on specifics of proc macros that aren't available
|
||||
/// in testing (like snapshot testing). So, we just need an ID for each of these items, hence this struct.
|
||||
///
|
||||
/// This is "transparent" to partialeq and eq, so it will always return true when compared to another DynIdx.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct DynIdx {
|
||||
idx: Cell<Option<usize>>,
|
||||
}
|
||||
|
||||
impl PartialEq for DynIdx {
|
||||
fn eq(&self, _other: &Self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for DynIdx {}
|
||||
|
||||
impl DynIdx {
|
||||
pub fn set(&self, idx: usize) {
|
||||
self.idx.set(Some(idx));
|
||||
}
|
||||
|
||||
pub fn get(&self) -> usize {
|
||||
self.idx.get().unwrap_or(usize::MAX)
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for DynIdx {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.idx.get().hash(state);
|
||||
}
|
||||
}
|
||||
+229
-399
@@ -1,188 +1,43 @@
|
||||
use self::location::CallerLocation;
|
||||
|
||||
use super::*;
|
||||
#[cfg(feature = "hot_reload")]
|
||||
use dioxus_core::TemplateNode;
|
||||
|
||||
use crate::innerlude::*;
|
||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||
use quote::quote;
|
||||
use quote::ToTokens;
|
||||
use syn::{
|
||||
braced,
|
||||
parse::ParseBuffer,
|
||||
parse::{Parse, ParseStream},
|
||||
spanned::Spanned,
|
||||
token::{self, Brace},
|
||||
Expr, ExprCall, ExprIf, Ident, LitStr, Pat,
|
||||
Ident, LitStr, Result, Token,
|
||||
};
|
||||
|
||||
/*
|
||||
Parse
|
||||
-> div {}
|
||||
-> Component {}
|
||||
-> component()
|
||||
-> "text {with_args}"
|
||||
-> {(0..10).map(|f| rsx!("asd"))} // <--- notice the curly braces
|
||||
*/
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub enum BodyNode {
|
||||
/// div {}
|
||||
Element(Element),
|
||||
Text(IfmtInput),
|
||||
RawExpr(TokenStream2),
|
||||
|
||||
/// Component {}
|
||||
Component(Component),
|
||||
|
||||
/// "text {formatted}"
|
||||
Text(TextNode),
|
||||
|
||||
/// {expr}
|
||||
RawExpr(ExprNode),
|
||||
|
||||
/// for item in items {}
|
||||
ForLoop(ForLoop),
|
||||
|
||||
/// if cond {} else if cond {} (else {}?)
|
||||
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 { .. })
|
||||
}
|
||||
|
||||
pub fn span(&self) -> Span {
|
||||
match self {
|
||||
BodyNode::Element(el) => el.name.span(),
|
||||
BodyNode::Component(component) => component.name.span(),
|
||||
BodyNode::Text(text) => text.source.span(),
|
||||
BodyNode::RawExpr(exp) => exp.span(),
|
||||
BodyNode::ForLoop(fl) => fl.for_token.span(),
|
||||
BodyNode::IfChain(f) => f.if_token.span(),
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
impl Parse for BodyNode {
|
||||
fn parse(stream: ParseStream) -> Result<Self> {
|
||||
if stream.peek(LitStr) {
|
||||
return Ok(BodyNode::Text(stream.parse()?));
|
||||
}
|
||||
|
||||
// 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('-') && peek_brace(&body_stream, partial_completions) {
|
||||
return Ok(BodyNode::Element(stream.parse::<Element>()?));
|
||||
}
|
||||
}
|
||||
|
||||
let body_stream = stream.fork();
|
||||
|
||||
if let Ok(path) = body_stream.parse::<syn::Path>() {
|
||||
// this is an Element if path match of:
|
||||
// - one ident
|
||||
// - followed by `{`
|
||||
// - 1st char is lowercase
|
||||
// - no underscores (reserved for components)
|
||||
//
|
||||
// example:
|
||||
// div {}
|
||||
if let Some(ident) = path.get_ident() {
|
||||
if peek_brace(&body_stream, partial_completions)
|
||||
&& !ident_looks_like_component(ident)
|
||||
{
|
||||
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 appear in last segment
|
||||
// - followed by `{` or `(`, note `(` cannot be used with one ident
|
||||
//
|
||||
// example
|
||||
// Div {}
|
||||
// ::Div {}
|
||||
// crate::Div {}
|
||||
// component {} <-- already handled by elements
|
||||
// ::component {}
|
||||
// crate::component{}
|
||||
// Input::<InputProps<'_, i32> {}
|
||||
// crate::Input::<InputProps<'_, i32> {}
|
||||
if peek_brace(&body_stream, partial_completions) {
|
||||
return Ok(BodyNode::Component(stream.parse()?));
|
||||
}
|
||||
}
|
||||
|
||||
// Transform for loops into into_iter calls
|
||||
if stream.peek(Token![for]) {
|
||||
return Ok(BodyNode::ForLoop(stream.parse()?));
|
||||
@@ -194,273 +49,248 @@ impl BodyNode {
|
||||
}
|
||||
|
||||
// Match statements are special but have no special arm syntax
|
||||
// we could allow arm syntax if we wanted
|
||||
// we could allow arm syntax if we wanted.
|
||||
//
|
||||
// And it might even backwards compatible? - I think it is with the right fallback
|
||||
// -> parse as bodynode (BracedRawExpr will kick in on multiline arms)
|
||||
// -> if that fails parse as an expr, since that arm might be a one-liner
|
||||
//
|
||||
// ```
|
||||
// match {
|
||||
// val => div {}
|
||||
// other_val => div {}
|
||||
// match expr {
|
||||
// val => rsx! { div {} },
|
||||
// other_val => rsx! { div {} }
|
||||
// }
|
||||
// ```
|
||||
if stream.peek(Token![match]) {
|
||||
return Ok(BodyNode::RawExpr(stream.parse::<Expr>()?.to_token_stream()));
|
||||
return Ok(BodyNode::RawExpr(stream.parse()?));
|
||||
}
|
||||
|
||||
// Raw expressions need to be wrapped in braces - let RawBracedExpr handle partial expansion
|
||||
if stream.peek(token::Brace) {
|
||||
// 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));
|
||||
return Ok(BodyNode::RawExpr(stream.parse()?));
|
||||
}
|
||||
|
||||
Err(syn::Error::new(
|
||||
stream.span(),
|
||||
"Expected a valid body node.\nExpressions must be wrapped in curly braces.",
|
||||
))
|
||||
}
|
||||
}
|
||||
// If there's an ident immediately followed by a dash, it's a web component
|
||||
// Web components support no namespacing, so just parse it as an element directly
|
||||
if stream.peek(Ident) && stream.peek2(Token![-]) {
|
||||
return Ok(BodyNode::Element(stream.parse::<Element>()?));
|
||||
}
|
||||
|
||||
// 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('_')
|
||||
}
|
||||
// this is an Element if path match of:
|
||||
//
|
||||
// - one ident
|
||||
// - followed by `{`
|
||||
// - 1st char is lowercase
|
||||
// - no underscores (reserved for components)
|
||||
//
|
||||
// example:
|
||||
// div {}
|
||||
if stream.peek(Ident) && stream.peek2(Brace) {
|
||||
let ident = stream.fork().parse::<Ident>().unwrap();
|
||||
let el_name = ident.to_string();
|
||||
let first_char = el_name.chars().next().unwrap();
|
||||
|
||||
impl Parse for BodyNode {
|
||||
fn parse(stream: ParseStream) -> Result<Self> {
|
||||
Self::parse_with_options(stream, true)
|
||||
if first_char.is_ascii_lowercase() && !el_name.contains('_') {
|
||||
return Ok(BodyNode::Element(stream.parse::<Element>()?));
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise this should be Component, allowed syntax:
|
||||
// - syn::Path
|
||||
// - PathArguments can only apper in last segment
|
||||
// - followed by `{` or `(`, note `(` cannot be used with one ident
|
||||
//
|
||||
// example
|
||||
// Div {}
|
||||
// ::Div {}
|
||||
// crate::Div {}
|
||||
// component {} <-- already handled by elements
|
||||
// ::component {}
|
||||
// crate::component{}
|
||||
// Input::<InputProps<'_, i32> {}
|
||||
// crate::Input::<InputProps<'_, i32> {}
|
||||
Ok(BodyNode::Component(stream.parse()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for BodyNode {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
match self {
|
||||
BodyNode::Element(_) => {
|
||||
unimplemented!("Elements are statically created in the template")
|
||||
}
|
||||
BodyNode::Element(ela) => ela.to_tokens(tokens),
|
||||
BodyNode::RawExpr(exp) => exp.to_tokens(tokens),
|
||||
BodyNode::Text(txt) => txt.to_tokens(tokens),
|
||||
BodyNode::ForLoop(floop) => floop.to_tokens(tokens),
|
||||
BodyNode::Component(comp) => comp.to_tokens(tokens),
|
||||
BodyNode::IfChain(ifchain) => ifchain.to_tokens(tokens),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Text is simple, just write it out
|
||||
BodyNode::Text(txt) => tokens.append_all(quote! {
|
||||
dioxus_core::DynamicNode::Text(dioxus_core::VText::new(#txt.to_string()))
|
||||
}),
|
||||
impl BodyNode {
|
||||
/// Convert this BodyNode into a TemplateNode.
|
||||
///
|
||||
/// dioxus-core uses this to understand templates at compiletime
|
||||
#[cfg(feature = "hot_reload")]
|
||||
pub fn to_template_node<Ctx: crate::HotReloadingContext>(&self) -> TemplateNode {
|
||||
match self {
|
||||
BodyNode::Element(el) => {
|
||||
let rust_name = el.name.to_string();
|
||||
|
||||
// Expressons too
|
||||
BodyNode::RawExpr(exp) => tokens.append_all(quote! {
|
||||
{
|
||||
#[allow(clippy::let_and_return)]
|
||||
let ___nodes = (#exp).into_dyn_node();
|
||||
___nodes
|
||||
let (tag, namespace) =
|
||||
Ctx::map_element(&rust_name).unwrap_or((intern(rust_name.as_str()), None));
|
||||
|
||||
TemplateNode::Element {
|
||||
tag,
|
||||
namespace,
|
||||
children: intern(
|
||||
el.children
|
||||
.iter()
|
||||
.map(|c| c.to_template_node::<Ctx>())
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
attrs: intern(
|
||||
el.merged_attributes
|
||||
.iter()
|
||||
.map(|attr| attr.to_template_attribute::<Ctx>(&rust_name))
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
}
|
||||
}),
|
||||
}
|
||||
BodyNode::Text(text) if text.is_static() => {
|
||||
let text = text.input.source.clone();
|
||||
let text = intern(text.value().as_str());
|
||||
TemplateNode::Text { text }
|
||||
}
|
||||
BodyNode::Text(text) => TemplateNode::Dynamic {
|
||||
id: text.dyn_idx.get(),
|
||||
},
|
||||
BodyNode::RawExpr(exp) => TemplateNode::Dynamic {
|
||||
id: exp.dyn_idx.get(),
|
||||
},
|
||||
BodyNode::Component(comp) => TemplateNode::Dynamic {
|
||||
id: comp.dyn_idx.get(),
|
||||
},
|
||||
BodyNode::ForLoop(floop) => TemplateNode::Dynamic {
|
||||
id: floop.dyn_idx.get(),
|
||||
},
|
||||
BodyNode::IfChain(chain) => TemplateNode::Dynamic {
|
||||
id: chain.dyn_idx.get(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// todo:
|
||||
//
|
||||
// Component children should also participate in hotreloading
|
||||
// This is a *little* hard since components might not be able to take children in the
|
||||
// first place. I'm sure there's a hacky way to allow this... but it's not quite as
|
||||
// straightforward as a for loop.
|
||||
//
|
||||
// It might involve always generating a `children` field on the component and always
|
||||
// populating it with an empty template. This might lose the typesafety of whether
|
||||
// or not a component can even accept children - essentially allowing childrne in
|
||||
// every component - so it'd be breaking - but it would/could work.
|
||||
BodyNode::Component(comp) => tokens.append_all(quote! { #comp }),
|
||||
pub fn get_dyn_idx(&self) -> usize {
|
||||
match self {
|
||||
BodyNode::Text(text) => text.dyn_idx.get(),
|
||||
BodyNode::RawExpr(exp) => exp.dyn_idx.get(),
|
||||
BodyNode::Component(comp) => comp.dyn_idx.get(),
|
||||
BodyNode::ForLoop(floop) => floop.dyn_idx.get(),
|
||||
BodyNode::IfChain(chain) => chain.dyn_idx.get(),
|
||||
BodyNode::Element(_) => panic!("Cannot get dyn_idx for this node"),
|
||||
}
|
||||
}
|
||||
|
||||
BodyNode::ForLoop(exp) => tokens.append_all(quote! { #exp }),
|
||||
pub fn set_dyn_idx(&self, idx: usize) {
|
||||
match self {
|
||||
BodyNode::Text(text) => text.dyn_idx.set(idx),
|
||||
BodyNode::RawExpr(exp) => exp.dyn_idx.set(idx),
|
||||
BodyNode::Component(comp) => comp.dyn_idx.set(idx),
|
||||
BodyNode::ForLoop(floop) => floop.dyn_idx.set(idx),
|
||||
BodyNode::IfChain(chain) => chain.dyn_idx.set(idx),
|
||||
BodyNode::Element(_) => panic!("Cannot set dyn_idx for this node"),
|
||||
}
|
||||
}
|
||||
|
||||
BodyNode::IfChain(chain) => tokens.append_all(quote! { #chain }),
|
||||
pub fn is_litstr(&self) -> bool {
|
||||
matches!(self, BodyNode::Text { .. })
|
||||
}
|
||||
|
||||
pub fn span(&self) -> Span {
|
||||
match self {
|
||||
BodyNode::Element(el) => el.name.span(),
|
||||
BodyNode::Component(component) => component.name.span(),
|
||||
BodyNode::Text(text) => text.input.source.span(),
|
||||
BodyNode::RawExpr(exp) => exp.span(),
|
||||
BodyNode::ForLoop(fl) => fl.for_token.span(),
|
||||
BodyNode::IfChain(f) => f.if_token.span(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn element_children(&self) -> &[BodyNode] {
|
||||
match self {
|
||||
BodyNode::Element(el) => &el.children,
|
||||
_ => panic!("Children not available for this node"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn el_name(&self) -> &ElementName {
|
||||
match self {
|
||||
BodyNode::Element(el) => &el.name,
|
||||
_ => panic!("Element name not available for this node"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
||||
pub struct ForLoop {
|
||||
pub for_token: Token![for],
|
||||
pub pat: Pat,
|
||||
pub in_token: Token![in],
|
||||
pub expr: Box<Expr>,
|
||||
pub body: Vec<BodyNode>,
|
||||
pub brace_token: token::Brace,
|
||||
pub location: CallerLocation,
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use quote::quote;
|
||||
|
||||
impl Parse for ForLoop {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let for_token: Token![for] = input.parse()?;
|
||||
#[test]
|
||||
fn parsing_matches() {
|
||||
let element = quote! { div { class: "inline-block mr-4", icons::icon_14 {} } };
|
||||
assert!(matches!(
|
||||
syn::parse2::<BodyNode>(element).unwrap(),
|
||||
BodyNode::Element(_)
|
||||
));
|
||||
|
||||
let pat = Pat::parse_single(input)?;
|
||||
let text = quote! { "Hello, world!" };
|
||||
assert!(matches!(
|
||||
syn::parse2::<BodyNode>(text).unwrap(),
|
||||
BodyNode::Text(_)
|
||||
));
|
||||
|
||||
let in_token: Token![in] = input.parse()?;
|
||||
let expr: Expr = input.call(Expr::parse_without_eager_brace)?;
|
||||
let component = quote! { Component {} };
|
||||
assert!(matches!(
|
||||
syn::parse2::<BodyNode>(component).unwrap(),
|
||||
BodyNode::Component(_)
|
||||
));
|
||||
|
||||
let (brace_token, body) = parse_buffer_as_braced_children(input)?;
|
||||
let raw_expr = quote! { { 1 + 1 } };
|
||||
assert!(matches!(
|
||||
syn::parse2::<BodyNode>(raw_expr).unwrap(),
|
||||
BodyNode::RawExpr(_)
|
||||
));
|
||||
|
||||
Ok(Self {
|
||||
for_token,
|
||||
pat,
|
||||
in_token,
|
||||
body,
|
||||
brace_token,
|
||||
location: CallerLocation::default(),
|
||||
expr: Box::new(expr),
|
||||
})
|
||||
}
|
||||
}
|
||||
let for_loop = quote! { for item in items {} };
|
||||
assert!(matches!(
|
||||
syn::parse2::<BodyNode>(for_loop).unwrap(),
|
||||
BodyNode::ForLoop(_)
|
||||
));
|
||||
|
||||
impl ToTokens for ForLoop {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let ForLoop {
|
||||
pat, expr, body, ..
|
||||
} = self;
|
||||
let if_chain = quote! { if cond {} else if cond {} };
|
||||
assert!(matches!(
|
||||
syn::parse2::<BodyNode>(if_chain).unwrap(),
|
||||
BodyNode::IfChain(_)
|
||||
));
|
||||
|
||||
let renderer = TemplateRenderer::as_tokens(body, None);
|
||||
|
||||
// Signals expose an issue with temporary lifetimes
|
||||
// We need to directly render out the nodes first to collapse their lifetime to <'a>
|
||||
// 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
|
||||
let match_expr = quote! {
|
||||
match blah {
|
||||
val => rsx! { div {} },
|
||||
other_val => rsx! { div {} }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
||||
pub struct IfChain {
|
||||
pub if_token: Token![if],
|
||||
pub cond: Box<Expr>,
|
||||
pub then_branch: Vec<BodyNode>,
|
||||
pub else_if_branch: Option<Box<IfChain>>,
|
||||
pub else_branch: Option<Vec<BodyNode>>,
|
||||
pub location: CallerLocation,
|
||||
}
|
||||
|
||||
impl Parse for IfChain {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let if_token: Token![if] = input.parse()?;
|
||||
|
||||
// stolen from ExprIf
|
||||
let cond = Box::new(input.call(Expr::parse_without_eager_brace)?);
|
||||
|
||||
let (_, then_branch) = parse_buffer_as_braced_children(input)?;
|
||||
|
||||
let mut else_branch = None;
|
||||
let mut else_if_branch = None;
|
||||
|
||||
// if the next token is `else`, set the else branch as the next if chain
|
||||
if input.peek(Token![else]) {
|
||||
input.parse::<Token![else]>()?;
|
||||
if input.peek(Token![if]) {
|
||||
else_if_branch = Some(Box::new(input.parse::<IfChain>()?));
|
||||
} else {
|
||||
let (_, else_branch_nodes) = parse_buffer_as_braced_children(input)?;
|
||||
else_branch = Some(else_branch_nodes);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
cond,
|
||||
if_token,
|
||||
then_branch,
|
||||
else_if_branch,
|
||||
else_branch,
|
||||
location: CallerLocation::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for IfChain {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let mut body = TokenStream2::new();
|
||||
let mut terminated = false;
|
||||
|
||||
let mut elif = Some(self);
|
||||
|
||||
while let Some(chain) = elif {
|
||||
let IfChain {
|
||||
if_token,
|
||||
cond,
|
||||
then_branch,
|
||||
else_if_branch,
|
||||
else_branch,
|
||||
..
|
||||
} = chain;
|
||||
|
||||
let renderer = TemplateRenderer::as_tokens(then_branch, None);
|
||||
|
||||
body.append_all(quote! { #if_token #cond { {#renderer} } });
|
||||
|
||||
if let Some(next) = else_if_branch {
|
||||
body.append_all(quote! { else });
|
||||
elif = Some(next);
|
||||
} else if let Some(else_branch) = else_branch {
|
||||
let renderer = TemplateRenderer::as_tokens(else_branch, None);
|
||||
body.append_all(quote! { else { {#renderer} } });
|
||||
terminated = true;
|
||||
break;
|
||||
} else {
|
||||
elif = None;
|
||||
}
|
||||
}
|
||||
|
||||
if !terminated {
|
||||
body.append_all(quote! {
|
||||
else { dioxus_core::VNode::empty() }
|
||||
});
|
||||
}
|
||||
|
||||
tokens.append_all(quote! {
|
||||
{
|
||||
#[allow(clippy::let_and_return)]
|
||||
let ___nodes = (#body).into_dyn_node();
|
||||
___nodes
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_buffer_as_braced_children(
|
||||
input: &syn::parse::ParseBuffer<'_>,
|
||||
) -> Result<(Brace, Vec<BodyNode>)> {
|
||||
let content;
|
||||
let brace_token = braced!(content in input);
|
||||
let mut then_branch = vec![];
|
||||
while !content.is_empty() {
|
||||
then_branch.push(content.parse()?);
|
||||
}
|
||||
Ok((brace_token, then_branch))
|
||||
}
|
||||
|
||||
pub(crate) fn is_if_chain_terminated(chain: &ExprIf) -> bool {
|
||||
let mut current = chain;
|
||||
loop {
|
||||
if let Some((_, else_block)) = ¤t.else_branch {
|
||||
if let Expr::If(else_if) = else_block.as_ref() {
|
||||
current = else_if;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
assert!(matches!(
|
||||
syn::parse2::<BodyNode>(match_expr).unwrap(),
|
||||
BodyNode::RawExpr(_)
|
||||
),);
|
||||
|
||||
let incomplete_component = quote! {
|
||||
some::cool::Component
|
||||
};
|
||||
assert!(matches!(
|
||||
syn::parse2::<BodyNode>(incomplete_component).unwrap(),
|
||||
BodyNode::Component(_)
|
||||
),);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
use crate::PartialExpr;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::ToTokens;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
punctuated::Punctuated,
|
||||
Attribute, Expr, Pat, PatType, Result, ReturnType, Token, Type,
|
||||
};
|
||||
use syn::{BoundLifetimes, ExprClosure};
|
||||
|
||||
/// A closure whose body might not be valid rust code but we want to interpret it regardless.
|
||||
/// This lets us provide expansions in way more cases than normal closures at the expense of an
|
||||
/// increased mainteance burden and complexity.
|
||||
///
|
||||
/// We do our best to reuse the same logic from partial exprs for the body of the PartialClosure.
|
||||
/// The code here is simply stolen from `syn::ExprClosure` and lightly modified to work with
|
||||
/// PartialExprs. We only removed the attrs field and changed the body to be a PartialExpr.
|
||||
/// Otherwise, it's a direct copy of the original.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PartialClosure {
|
||||
pub lifetimes: Option<BoundLifetimes>,
|
||||
pub constness: Option<Token![const]>,
|
||||
pub movability: Option<Token![static]>,
|
||||
pub asyncness: Option<Token![async]>,
|
||||
pub capture: Option<Token![move]>,
|
||||
pub or1_token: Token![|],
|
||||
pub inputs: Punctuated<Pat, Token![,]>,
|
||||
pub or2_token: Token![|],
|
||||
pub output: ReturnType,
|
||||
pub body: PartialExpr,
|
||||
}
|
||||
|
||||
impl Parse for PartialClosure {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let lifetimes: Option<BoundLifetimes> = input.parse()?;
|
||||
let constness: Option<Token![const]> = input.parse()?;
|
||||
let movability: Option<Token![static]> = input.parse()?;
|
||||
let asyncness: Option<Token![async]> = input.parse()?;
|
||||
let capture: Option<Token![move]> = input.parse()?;
|
||||
let or1_token: Token![|] = input.parse()?;
|
||||
|
||||
let mut inputs = Punctuated::new();
|
||||
loop {
|
||||
if input.peek(Token![|]) {
|
||||
break;
|
||||
}
|
||||
let value = closure_arg(input)?;
|
||||
inputs.push_value(value);
|
||||
if input.peek(Token![|]) {
|
||||
break;
|
||||
}
|
||||
let punct: Token![,] = input.parse()?;
|
||||
inputs.push_punct(punct);
|
||||
}
|
||||
|
||||
let or2_token: Token![|] = input.parse()?;
|
||||
|
||||
let output = if input.peek(Token![->]) {
|
||||
let arrow_token: Token![->] = input.parse()?;
|
||||
let ty: Type = input.parse()?;
|
||||
ReturnType::Type(arrow_token, Box::new(ty))
|
||||
} else {
|
||||
ReturnType::Default
|
||||
};
|
||||
|
||||
let body = PartialExpr::parse(input)?;
|
||||
|
||||
Ok(PartialClosure {
|
||||
lifetimes,
|
||||
constness,
|
||||
movability,
|
||||
asyncness,
|
||||
capture,
|
||||
or1_token,
|
||||
inputs,
|
||||
or2_token,
|
||||
output,
|
||||
body,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for PartialClosure {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
self.lifetimes.to_tokens(tokens);
|
||||
self.constness.to_tokens(tokens);
|
||||
self.movability.to_tokens(tokens);
|
||||
self.asyncness.to_tokens(tokens);
|
||||
self.capture.to_tokens(tokens);
|
||||
self.or1_token.to_tokens(tokens);
|
||||
self.inputs.to_tokens(tokens);
|
||||
self.or2_token.to_tokens(tokens);
|
||||
self.output.to_tokens(tokens);
|
||||
self.body.to_tokens(tokens);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for PartialClosure {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.lifetimes == other.lifetimes
|
||||
&& self.constness == other.constness
|
||||
&& self.movability == other.movability
|
||||
&& self.asyncness == other.asyncness
|
||||
&& self.capture == other.capture
|
||||
&& self.or1_token == other.or1_token
|
||||
&& self.inputs == other.inputs
|
||||
&& self.or2_token == other.or2_token
|
||||
&& self.output == other.output
|
||||
&& self.body == other.body
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for PartialClosure {}
|
||||
impl Hash for PartialClosure {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.lifetimes.hash(state);
|
||||
self.constness.hash(state);
|
||||
self.movability.hash(state);
|
||||
self.asyncness.hash(state);
|
||||
self.capture.hash(state);
|
||||
self.or1_token.hash(state);
|
||||
self.inputs.hash(state);
|
||||
self.or2_token.hash(state);
|
||||
self.output.hash(state);
|
||||
self.body.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialClosure {
|
||||
/// Convert this partial closure into a full closure if it is valid
|
||||
/// Returns err if the internal tokens can't be parsed as a closure
|
||||
pub fn as_expr(&self) -> Result<Expr> {
|
||||
let expr_closure = ExprClosure {
|
||||
attrs: Vec::new(),
|
||||
asyncness: self.asyncness,
|
||||
capture: self.capture,
|
||||
inputs: self.inputs.clone(),
|
||||
output: self.output.clone(),
|
||||
lifetimes: self.lifetimes.clone(),
|
||||
constness: self.constness,
|
||||
movability: self.movability,
|
||||
or1_token: self.or1_token,
|
||||
or2_token: self.or2_token,
|
||||
|
||||
// try to lower the body to an expression - if might fail if it can't
|
||||
body: Box::new(self.body.as_expr()?),
|
||||
};
|
||||
|
||||
Ok(Expr::Closure(expr_closure))
|
||||
}
|
||||
}
|
||||
|
||||
/// This might look complex but it is just a ripoff of the `syn::ExprClosure` implementation. AFAIK
|
||||
/// This code is not particularly accessible from outside syn... so it lives here. sorry
|
||||
fn closure_arg(input: ParseStream) -> Result<Pat> {
|
||||
let attrs = input.call(Attribute::parse_outer)?;
|
||||
let mut pat = Pat::parse_single(input)?;
|
||||
|
||||
if input.peek(Token![:]) {
|
||||
Ok(Pat::Type(PatType {
|
||||
attrs,
|
||||
pat: Box::new(pat),
|
||||
colon_token: input.parse()?,
|
||||
ty: input.parse()?,
|
||||
}))
|
||||
} else {
|
||||
match &mut pat {
|
||||
Pat::Const(pat) => pat.attrs = attrs,
|
||||
Pat::Ident(pat) => pat.attrs = attrs,
|
||||
Pat::Lit(pat) => pat.attrs = attrs,
|
||||
Pat::Macro(pat) => pat.attrs = attrs,
|
||||
Pat::Or(pat) => pat.attrs = attrs,
|
||||
Pat::Paren(pat) => pat.attrs = attrs,
|
||||
Pat::Path(pat) => pat.attrs = attrs,
|
||||
Pat::Range(pat) => pat.attrs = attrs,
|
||||
Pat::Reference(pat) => pat.attrs = attrs,
|
||||
Pat::Rest(pat) => pat.attrs = attrs,
|
||||
Pat::Slice(pat) => pat.attrs = attrs,
|
||||
Pat::Struct(pat) => pat.attrs = attrs,
|
||||
Pat::Tuple(pat) => pat.attrs = attrs,
|
||||
Pat::TupleStruct(pat) => pat.attrs = attrs,
|
||||
Pat::Wild(pat) => pat.attrs = attrs,
|
||||
Pat::Type(_) => unreachable!(),
|
||||
Pat::Verbatim(_) => {}
|
||||
_ => {}
|
||||
}
|
||||
Ok(pat)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use quote::quote;
|
||||
|
||||
#[test]
|
||||
fn parses() {
|
||||
let doesnt_parse: Result<ExprClosure> = syn::parse2(quote! {
|
||||
|a, b| { method. }
|
||||
});
|
||||
|
||||
// regular closures can't parse as partial closures
|
||||
assert!(doesnt_parse.is_err());
|
||||
|
||||
let parses: Result<PartialClosure> = syn::parse2(quote! {
|
||||
|a, b| { method. }
|
||||
});
|
||||
|
||||
// but ours can - we just can't format it out
|
||||
let parses = parses.unwrap();
|
||||
dbg!(parses.to_token_stream().to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_real_world() {
|
||||
let parses: Result<PartialClosure> = syn::parse2(quote! {
|
||||
move |_| {
|
||||
let mut sidebar = SHOW_SIDEBAR.write();
|
||||
*sidebar = !*sidebar;
|
||||
}
|
||||
});
|
||||
|
||||
// but ours can - we just can't format it out
|
||||
let parses = parses.unwrap();
|
||||
dbg!(parses.to_token_stream().to_string());
|
||||
parses.as_expr().unwrap();
|
||||
|
||||
let parses: Result<PartialClosure> = syn::parse2(quote! {
|
||||
move |_| {
|
||||
rsx! {
|
||||
div { class: "max-w-lg lg:max-w-2xl mx-auto mb-16 text-center",
|
||||
"gomg"
|
||||
"hi!!"
|
||||
"womh"
|
||||
}
|
||||
};
|
||||
println!("hi")
|
||||
}
|
||||
});
|
||||
parses.unwrap().as_expr().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_eqs() {
|
||||
let a: PartialClosure = syn::parse2(quote! {
|
||||
move |e| {
|
||||
println!("clicked!");
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let b: PartialClosure = syn::parse2(quote! {
|
||||
move |e| {
|
||||
println!("clicked!");
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let c: PartialClosure = syn::parse2(quote! {
|
||||
move |e| {
|
||||
println!("unclicked");
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(a, b);
|
||||
assert_ne!(a, c);
|
||||
}
|
||||
|
||||
/// Ensure our ToTokens impl is the same as the one in syn
|
||||
#[test]
|
||||
fn same_to_tokens() {
|
||||
let a: PartialClosure = syn::parse2(quote! {
|
||||
move |e| {
|
||||
println!("clicked!");
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let b: PartialClosure = syn::parse2(quote! {
|
||||
move |e| {
|
||||
println!("clicked!");
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let c: ExprClosure = syn::parse2(quote! {
|
||||
move |e| {
|
||||
println!("clicked!");
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
a.to_token_stream().to_string(),
|
||||
b.to_token_stream().to_string()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
a.to_token_stream().to_string(),
|
||||
c.to_token_stream().to_string()
|
||||
);
|
||||
|
||||
let a: PartialClosure = syn::parse2(quote! {
|
||||
move |e| println!("clicked!")
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let b: ExprClosure = syn::parse2(quote! {
|
||||
move |e| println!("clicked!")
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
a.to_token_stream().to_string(),
|
||||
b.to_token_stream().to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::ToTokens;
|
||||
use std::hash;
|
||||
use syn::{parse::Parse, spanned::Spanned, token::Brace, Expr};
|
||||
|
||||
/// A raw expression potentially wrapped in curly braces that is parsed from the input stream.
|
||||
///
|
||||
/// If there are no braces, it tries to parse as an expression without partial expansion. If there
|
||||
/// are braces, it parses the contents as a `TokenStream2` and stores it as such.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PartialExpr {
|
||||
pub brace: Option<Brace>,
|
||||
pub expr: TokenStream2,
|
||||
}
|
||||
|
||||
impl Parse for PartialExpr {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
// Parse as an expression if there is no brace
|
||||
if !input.peek(syn::token::Brace) {
|
||||
let expr = input.parse::<syn::Expr>()?;
|
||||
return Ok(Self {
|
||||
brace: None,
|
||||
expr: expr.to_token_stream(),
|
||||
});
|
||||
}
|
||||
|
||||
let content;
|
||||
let brace = Some(syn::braced!(content in input));
|
||||
let expr: TokenStream2 = content.parse()?;
|
||||
|
||||
Ok(Self { brace, expr })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for PartialExpr {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
match &self.brace {
|
||||
Some(brace) => brace.surround(tokens, |tokens| self.expr.to_tokens(tokens)),
|
||||
_ => self.expr.to_tokens(tokens),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialExpr {
|
||||
pub fn as_expr(&self) -> syn::Result<syn::Expr> {
|
||||
// very important: make sure to include the brace in the span of the expr
|
||||
// otherwise autofmt will freak out since it will use the inner span
|
||||
if let Some(brace) = &self.brace {
|
||||
let mut tokens = TokenStream2::new();
|
||||
let f = |tokens: &mut TokenStream2| self.expr.to_tokens(tokens);
|
||||
brace.surround(&mut tokens, f);
|
||||
return syn::parse2(tokens);
|
||||
}
|
||||
|
||||
let expr = self.expr.clone();
|
||||
syn::parse2(expr.to_token_stream())
|
||||
}
|
||||
|
||||
pub fn from_expr(expr: &Expr) -> Self {
|
||||
Self {
|
||||
brace: None,
|
||||
expr: expr.to_token_stream(),
|
||||
}
|
||||
}
|
||||
pub fn span(&self) -> proc_macro2::Span {
|
||||
if let Some(brace) = &self.brace {
|
||||
brace.span.span()
|
||||
} else {
|
||||
self.expr.span()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for PartialExpr {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.expr.to_string() == other.expr.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for PartialExpr {}
|
||||
|
||||
impl hash::Hash for PartialExpr {
|
||||
fn hash<H: hash::Hasher>(&self, state: &mut H) {
|
||||
self.expr.to_string().hash(state);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/// An array that's optimized for finding and removing elements that match a predicate.
|
||||
///
|
||||
/// Currently will do a linear search for the first element that matches the predicate.
|
||||
/// Uses a scan_start pointer to optimize the search such that future searches start from left-most
|
||||
/// non-None item, making it O(1) on average for sorted input.
|
||||
///
|
||||
/// The motivating factor here is that hashes are expensive and actually quite hard to maintain for
|
||||
/// callbody. Hashing would imply a number of nested invariants that are hard to maintain.
|
||||
///
|
||||
/// Deriving hash will start to slurp up private fields which is not what we want, so the comparison
|
||||
/// function is moved here to the reloadstack interface.
|
||||
pub struct PopVec<T> {
|
||||
stack: Box<[Option<T>]>,
|
||||
scan_start: usize,
|
||||
}
|
||||
|
||||
impl<T> PopVec<T> {
|
||||
pub fn new(f: impl Iterator<Item = T>) -> Self {
|
||||
let stack = f.map(Some).collect();
|
||||
Self {
|
||||
stack,
|
||||
scan_start: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, idx: usize) -> Option<T> {
|
||||
let item = self.stack.get_mut(idx)?.take();
|
||||
|
||||
// move the scan_start pointer to the right-most non-none element
|
||||
for i in self.scan_start..=idx {
|
||||
if self.stack[i].is_some() {
|
||||
break;
|
||||
}
|
||||
self.scan_start = i + 1;
|
||||
}
|
||||
|
||||
item
|
||||
}
|
||||
|
||||
pub fn pop_where(&mut self, f: impl Fn(&T) -> bool) -> Option<T> {
|
||||
let idx = self
|
||||
.stack
|
||||
.iter()
|
||||
.position(|x| if let Some(x) = x { f(x) } else { false })?;
|
||||
|
||||
self.remove(idx)
|
||||
}
|
||||
|
||||
/// Returns the index and score of the highest scored element
|
||||
///
|
||||
/// shortcircuits if the score is usize::MAX
|
||||
/// returns None if the score was 0
|
||||
pub fn highest_score(&self, score: impl Fn(&T) -> usize) -> Option<(usize, usize)> {
|
||||
let mut highest_score = 0;
|
||||
let mut best = None;
|
||||
|
||||
for (idx, x) in self.stack.iter().enumerate().skip(self.scan_start) {
|
||||
if let Some(x) = x {
|
||||
let scored = score(x);
|
||||
if scored > highest_score {
|
||||
best = Some(idx);
|
||||
highest_score = scored;
|
||||
}
|
||||
|
||||
if highest_score == usize::MAX {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if highest_score == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
best.map(|idx| (idx, highest_score))
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
// next free is 0 when stack len = 1
|
||||
self.scan_start == self.stack.len() - 1
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn searches_and_works() {
|
||||
let mut stack = PopVec::new(vec![1, 2, 3, 4, 5].into_iter());
|
||||
|
||||
assert_eq!(stack.pop_where(|x| *x == 3), Some(3));
|
||||
assert_eq!(stack.pop_where(|x| *x == 1), Some(1));
|
||||
assert_eq!(stack.pop_where(|x| *x == 5), Some(5));
|
||||
assert_eq!(stack.pop_where(|x| *x == 2), Some(2));
|
||||
assert_eq!(stack.pop_where(|x| *x == 4), Some(4));
|
||||
assert_eq!(stack.pop_where(|x| *x == 4), None);
|
||||
|
||||
assert!(stack.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_optimization_works() {
|
||||
let mut stack = PopVec::new(vec![0, 1, 2, 3, 4, 5].into_iter());
|
||||
|
||||
_ = stack.remove(0);
|
||||
assert_eq!(stack.scan_start, 1);
|
||||
|
||||
_ = stack.remove(1);
|
||||
assert_eq!(stack.scan_start, 2);
|
||||
|
||||
_ = stack.remove(4);
|
||||
assert_eq!(stack.scan_start, 2);
|
||||
|
||||
_ = stack.remove(2);
|
||||
assert_eq!(stack.scan_start, 3);
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
use crate::*;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::quote;
|
||||
|
||||
pub struct TemplateRenderer<'a> {
|
||||
pub roots: &'a [BodyNode],
|
||||
location: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> TemplateRenderer<'a> {
|
||||
/// Render the contents of the callbody out with a specific location
|
||||
///
|
||||
/// This will cascade location information down the tree if it already hasn't been set
|
||||
pub fn as_tokens(roots: &'a [BodyNode], location: Option<String>) -> TokenStream2 {
|
||||
TemplateRenderer::render(Self { roots, location })
|
||||
}
|
||||
|
||||
fn render(mut self) -> TokenStream2 {
|
||||
// If there are no roots, this is an empty template, so just return None
|
||||
if self.roots.is_empty() {
|
||||
return quote! { Option::<dioxus_core::VNode>::None };
|
||||
}
|
||||
|
||||
// Create a new dynamic context that tracks the state of all the dynamic nodes
|
||||
// We have no old template, to seed it with, so it'll just be used for rendering
|
||||
let mut context = DynamicContext::default();
|
||||
|
||||
// If we have an implicit key, then we need to write its tokens
|
||||
let key_tokens = match self.implicit_key() {
|
||||
Some(tok) => quote! { Some( #tok.to_string() ) },
|
||||
None => quote! { None },
|
||||
};
|
||||
|
||||
// Get the tokens we'll use as the ID of the template
|
||||
// This follows the file:line:column:id format
|
||||
let name = self.get_template_id_tokens();
|
||||
|
||||
// Render the static nodes, generating the mapping of dynamic
|
||||
// This will modify the bodynodes in place - sorry about that
|
||||
let roots = self.render_body_nodes(&mut context);
|
||||
|
||||
let dynamic_nodes = &context.dynamic_nodes;
|
||||
let dyn_attr_printer = context
|
||||
.dynamic_attributes
|
||||
.iter()
|
||||
.map(|attrs| AttributeType::merge_quote(attrs));
|
||||
|
||||
let node_paths = context.node_paths.iter().map(|it| quote!(&[#(#it),*]));
|
||||
let attr_paths = context.attr_paths.iter().map(|it| quote!(&[#(#it),*]));
|
||||
|
||||
let vnode = quote! {
|
||||
static TEMPLATE: dioxus_core::Template = dioxus_core::Template {
|
||||
name: #name,
|
||||
roots: #roots,
|
||||
node_paths: &[ #(#node_paths),* ],
|
||||
attr_paths: &[ #(#attr_paths),* ],
|
||||
};
|
||||
|
||||
{
|
||||
// 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,
|
||||
Box::new([ #( #dynamic_nodes),* ]),
|
||||
Box::new([ #(#dyn_attr_printer),* ]),
|
||||
);
|
||||
__vnodes
|
||||
}
|
||||
};
|
||||
|
||||
quote! { dioxus_core::Element::Ok({ #vnode }) }
|
||||
}
|
||||
|
||||
fn get_template_id_tokens(&self) -> TokenStream2 {
|
||||
match self.location {
|
||||
Some(ref loc) => quote! { #loc },
|
||||
None => {
|
||||
// Get the root:column:id tag we'll use as the ID of the template
|
||||
let root_col = self.get_root_col_id();
|
||||
|
||||
// Replace `\\` to `/` and then replace `\` to `/`
|
||||
quote! {
|
||||
{
|
||||
const PATH: &str = dioxus_core::const_format::str_replace!(file!(), "\\\\", "/");
|
||||
const NORMAL: &str = dioxus_core::const_format::str_replace!(PATH, '\\', "/");
|
||||
dioxus_core::const_format::concatcp!(NORMAL, ':', line!(), ':', column!(), ':', #root_col)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_root_col_id(&self) -> String {
|
||||
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))
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
}
|
||||
_ => "0".to_string(),
|
||||
};
|
||||
root_col
|
||||
}
|
||||
|
||||
fn implicit_key(&self) -> Option<IfmtInput> {
|
||||
let key = match self.roots.first() {
|
||||
Some(BodyNode::Element(el)) if self.roots.len() == 1 => el.key.clone(),
|
||||
Some(BodyNode::Component(comp)) if self.roots.len() == 1 => comp.key().cloned(),
|
||||
_ => None,
|
||||
};
|
||||
key
|
||||
}
|
||||
|
||||
/// Render a list of BodyNodes as a static array (&[...])
|
||||
pub fn render_body_nodes(&mut self, context: &mut DynamicContext<'a>) -> TokenStream2 {
|
||||
let root_printer = self
|
||||
.roots
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, root)| context.render_children_nodes(idx, root));
|
||||
|
||||
// Render the static nodes, generating the mapping of dynamic
|
||||
quote! {
|
||||
&[ #( #root_printer ),* ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
//! An arbitrary block parser.
|
||||
//!
|
||||
//! Is meant to parse the contents of a block that is either a component or an element.
|
||||
//! We put these together to cut down on code duplication and make the parsers a bit more resilient.
|
||||
//!
|
||||
//! This involves custom structs for name, attributes, and children, as well as a custom parser for the block itself.
|
||||
//! It also bubbles out diagnostics if it can to give better errors.
|
||||
|
||||
use crate::innerlude::*;
|
||||
use proc_macro2::Span;
|
||||
use proc_macro2_diagnostics::SpanDiagnosticExt;
|
||||
use syn::{
|
||||
ext::IdentExt,
|
||||
parse::{Parse, ParseBuffer},
|
||||
spanned::Spanned,
|
||||
token::{self, Brace},
|
||||
Expr, Ident, LitStr, Token,
|
||||
};
|
||||
|
||||
/// An item in the form of
|
||||
///
|
||||
/// {
|
||||
/// attributes,
|
||||
/// ..spreads,
|
||||
/// children
|
||||
/// }
|
||||
///
|
||||
/// Does not make any guarnatees about the contents of the block - this is meant to be verified by the
|
||||
/// element/component impls themselves.
|
||||
///
|
||||
/// The name of the block is expected to be parsed by the parent parser. It will accept items out of
|
||||
/// order if possible and then bubble up diagnostics to the parent. This lets us give better errors
|
||||
/// and autocomplete
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub struct RsxBlock {
|
||||
pub brace: token::Brace,
|
||||
pub attributes: Vec<Attribute>,
|
||||
pub spreads: Vec<Spread>,
|
||||
pub children: Vec<BodyNode>,
|
||||
pub diagnostics: Diagnostics,
|
||||
}
|
||||
|
||||
impl Parse for RsxBlock {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let content: ParseBuffer;
|
||||
let brace = syn::braced!(content in input);
|
||||
RsxBlock::parse_inner(&content, brace)
|
||||
}
|
||||
}
|
||||
|
||||
impl RsxBlock {
|
||||
pub fn parse_inner(content: &ParseBuffer, brace: token::Brace) -> syn::Result<Self> {
|
||||
let mut items = vec![];
|
||||
let mut diagnostics = Diagnostics::new();
|
||||
|
||||
// Lots of manual parsing but it's important to do it all here to give the best diagnostics possible
|
||||
// We can do things like lookaheads, peeking, etc. to give better errors and autocomplete
|
||||
// We allow parsing in any order but complain if its done out of order.
|
||||
// Autofmt will fortunately fix this for us in most cases
|
||||
//
|
||||
// Weo do this by parsing the unambiguous cases first and then do some clever lookahead to parse the rest
|
||||
while !content.is_empty() {
|
||||
// Parse spread attributes
|
||||
if content.peek(Token![..]) {
|
||||
let dots = content.parse::<Token![..]>()?;
|
||||
|
||||
// in case someone tries to do ...spread which is not valid
|
||||
if let Ok(extra) = content.parse::<Token![.]>() {
|
||||
diagnostics.push(
|
||||
extra
|
||||
.span()
|
||||
.error("Spread expressions only take two dots - not 3! (..spread)"),
|
||||
);
|
||||
}
|
||||
|
||||
let expr = content.parse::<Expr>()?;
|
||||
let attr = Spread {
|
||||
expr,
|
||||
dots,
|
||||
dyn_idx: DynIdx::default(),
|
||||
comma: content.parse().ok(),
|
||||
};
|
||||
|
||||
if !content.is_empty() && attr.comma.is_none() {
|
||||
diagnostics.push(
|
||||
attr.span()
|
||||
.error("Attributes must be separated by commas")
|
||||
.help("Did you forget a comma?"),
|
||||
);
|
||||
}
|
||||
items.push(RsxItem::Spread(attr));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse unambiguous attributes - these can't be confused with anything
|
||||
if (content.peek(LitStr) || content.peek(Ident) || content.peek(Ident::peek_any))
|
||||
&& content.peek2(Token![:])
|
||||
&& !content.peek3(Token![:])
|
||||
{
|
||||
let attr = content.parse::<Attribute>()?;
|
||||
|
||||
if !content.is_empty() && attr.comma.is_none() {
|
||||
diagnostics.push(
|
||||
attr.span()
|
||||
.error("Attributes must be separated by commas")
|
||||
.help("Did you forget a comma?"),
|
||||
);
|
||||
}
|
||||
|
||||
items.push(RsxItem::Attribute(attr));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Eagerly match on children, generally
|
||||
if content.peek(LitStr)
|
||||
| content.peek(Token![for])
|
||||
| content.peek(Token![if])
|
||||
| content.peek(Token![match])
|
||||
| content.peek(token::Brace)
|
||||
// web components
|
||||
| (content.peek(Ident::peek_any) && content.peek2(Token![-]))
|
||||
// elements
|
||||
| (content.peek(Ident::peek_any) && content.peek2(token::Brace))
|
||||
// todo: eager parse components?
|
||||
{
|
||||
items.push(RsxItem::Child(content.parse::<BodyNode>()?));
|
||||
if !content.is_empty() && content.peek(Token![,]) {
|
||||
let comma = content.parse::<Token![,]>()?;
|
||||
diagnostics.push(
|
||||
comma.span().warning(
|
||||
"Elements and text nodes do not need to be separated by commas.",
|
||||
),
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse shorthand attributes
|
||||
// todo: this might cause complications with partial expansion... think more about the cases
|
||||
// where we can imagine expansion and what better diagnostics we can providea
|
||||
if content.peek(Ident)
|
||||
&& !content.peek2(Brace)
|
||||
&& !content.peek2(Token![:]) // regular attributes / components with generics
|
||||
&& !content.peek2(Token![-]) // web components
|
||||
&& !content.peek2(Token![<]) // generics on components
|
||||
// generics on components
|
||||
&& !content.peek2(Token![::])
|
||||
{
|
||||
let attribute = content.parse::<Attribute>()?;
|
||||
|
||||
if !content.is_empty() && attribute.comma.is_none() {
|
||||
diagnostics.push(
|
||||
attribute
|
||||
.span()
|
||||
.error("Attributes must be separated by commas")
|
||||
.help("Did you forget a comma?"),
|
||||
);
|
||||
}
|
||||
|
||||
items.push(RsxItem::Attribute(attribute));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Finally just attempt a bodynode parse
|
||||
items.push(RsxItem::Child(content.parse::<BodyNode>()?))
|
||||
}
|
||||
|
||||
// Validate the order of the items
|
||||
RsxBlock::validate(&items, &mut diagnostics);
|
||||
|
||||
// todo: maybe make this a method such that the rsxblock is lossless
|
||||
// Decompose into attributes, spreads, and children
|
||||
let mut attributes = vec![];
|
||||
let mut spreads = vec![];
|
||||
let mut children = vec![];
|
||||
for item in items {
|
||||
match item {
|
||||
RsxItem::Attribute(attr) => attributes.push(attr),
|
||||
RsxItem::Spread(spread) => spreads.push(spread),
|
||||
RsxItem::Child(child) => children.push(child),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
attributes,
|
||||
children,
|
||||
spreads,
|
||||
brace,
|
||||
diagnostics,
|
||||
})
|
||||
}
|
||||
|
||||
/// Ensure the ordering of the items is correct
|
||||
/// - Attributes must come before children
|
||||
/// - Spreads must come before children
|
||||
/// - Spreads must come after attributes
|
||||
///
|
||||
/// div {
|
||||
/// key: "value",
|
||||
/// ..props,
|
||||
/// "Hello, world!"
|
||||
/// }
|
||||
fn validate(items: &[RsxItem], diagnostics: &mut Diagnostics) {
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum ValidationState {
|
||||
Attributes,
|
||||
Spreads,
|
||||
Children,
|
||||
}
|
||||
use ValidationState::*;
|
||||
let mut state = ValidationState::Attributes;
|
||||
|
||||
for item in items.iter() {
|
||||
match item {
|
||||
RsxItem::Attribute(_) => {
|
||||
if state == Children || state == Spreads {
|
||||
diagnostics.push(
|
||||
item.span()
|
||||
.error("Attributes must come before children in an element"),
|
||||
);
|
||||
}
|
||||
state = Attributes;
|
||||
}
|
||||
RsxItem::Spread(_) => {
|
||||
if state == Children {
|
||||
diagnostics.push(
|
||||
item.span()
|
||||
.error("Spreads must come before children in an element"),
|
||||
);
|
||||
}
|
||||
state = Spreads;
|
||||
}
|
||||
RsxItem::Child(_) => {
|
||||
state = Children;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum RsxItem {
|
||||
Attribute(Attribute),
|
||||
Spread(Spread),
|
||||
Child(BodyNode),
|
||||
}
|
||||
|
||||
impl RsxItem {
|
||||
pub fn span(&self) -> Span {
|
||||
match self {
|
||||
RsxItem::Attribute(attr) => attr.span(),
|
||||
RsxItem::Spread(spread) => spread.dots.span(),
|
||||
RsxItem::Child(child) => child.span(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use quote::quote;
|
||||
|
||||
#[test]
|
||||
fn basic_cases() {
|
||||
let input = quote! {
|
||||
{ "Hello, world!" }
|
||||
};
|
||||
|
||||
let block: RsxBlock = syn::parse2(input).unwrap();
|
||||
assert_eq!(block.attributes.len(), 0);
|
||||
assert_eq!(block.children.len(), 1);
|
||||
|
||||
let input = quote! {
|
||||
{
|
||||
key: "value",
|
||||
onclick: move |_| {
|
||||
"Hello, world!"
|
||||
},
|
||||
..spread,
|
||||
"Hello, world!"
|
||||
}
|
||||
};
|
||||
|
||||
let block: RsxBlock = syn::parse2(input).unwrap();
|
||||
dbg!(block);
|
||||
|
||||
let complex_element = quote! {
|
||||
{
|
||||
key: "value",
|
||||
onclick2: move |_| {
|
||||
"Hello, world!"
|
||||
},
|
||||
thing: if true { "value" },
|
||||
otherthing: if true { "value" } else { "value" },
|
||||
onclick: move |_| {
|
||||
"Hello, world!"
|
||||
},
|
||||
..spread,
|
||||
..spread1
|
||||
..spread2,
|
||||
"Hello, world!"
|
||||
}
|
||||
};
|
||||
|
||||
let _block: RsxBlock = syn::parse2(complex_element).unwrap();
|
||||
|
||||
let complex_component = quote! {
|
||||
{
|
||||
key: "value",
|
||||
onclick2: move |_| {
|
||||
"Hello, world!"
|
||||
},
|
||||
..spread,
|
||||
"Hello, world!"
|
||||
}
|
||||
};
|
||||
|
||||
let _block: RsxBlock = syn::parse2(complex_component).unwrap();
|
||||
}
|
||||
|
||||
/// Some tests of partial expansion to give better autocomplete
|
||||
#[test]
|
||||
fn partial_cases() {
|
||||
let with_hander = quote! {
|
||||
{
|
||||
onclick: move |_| {
|
||||
some.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let _block: RsxBlock = syn::parse2(with_hander).unwrap();
|
||||
}
|
||||
|
||||
/// Ensure the hotreload scoring algorithm works as expected
|
||||
#[test]
|
||||
fn hr_score() {
|
||||
let _block = quote! {
|
||||
{
|
||||
a: "value {cool}",
|
||||
b: "{cool} value",
|
||||
b: "{cool} {thing} value",
|
||||
b: "{thing} value",
|
||||
}
|
||||
};
|
||||
|
||||
// loop { accumulate perfect matches }
|
||||
// stop when all matches are equally valid
|
||||
//
|
||||
// Remove new attr one by one as we find its perfect match. If it doesn't have a perfect match, we
|
||||
// score it instead.
|
||||
|
||||
quote! {
|
||||
// start with
|
||||
div {
|
||||
div { class: "other {abc} {def} {hij}" } // 1, 1, 1
|
||||
div { class: "thing {abc} {def}" } // 1, 1, 1
|
||||
// div { class: "thing {abc}" } // 1, 0, 1
|
||||
}
|
||||
|
||||
// end with
|
||||
div {
|
||||
h1 {
|
||||
class: "thing {abc}" // 1, 1, MAX
|
||||
}
|
||||
h1 {
|
||||
class: "thing {hij}" // 1, 1, MAX
|
||||
}
|
||||
// h2 {
|
||||
// class: "thing {def}" // 1, 1, 0
|
||||
// }
|
||||
// h3 {
|
||||
// class: "thing {def}" // 1, 1, 0
|
||||
// }
|
||||
}
|
||||
|
||||
// how about shuffling components, for, if, etc
|
||||
Component {
|
||||
class: "thing {abc}",
|
||||
other: "other {abc} {def}",
|
||||
}
|
||||
Component {
|
||||
class: "thing",
|
||||
other: "other",
|
||||
}
|
||||
|
||||
Component {
|
||||
class: "thing {abc}",
|
||||
other: "other",
|
||||
}
|
||||
Component {
|
||||
class: "thing {abc}",
|
||||
other: "other {abc} {def}",
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitchen_sink_parse() {
|
||||
let input = quote! {
|
||||
// Elements
|
||||
{
|
||||
class: "hello",
|
||||
id: "node-{node_id}",
|
||||
..props,
|
||||
|
||||
// Text Nodes
|
||||
"Hello, world!"
|
||||
|
||||
// Exprs
|
||||
{rsx! { "hi again!" }}
|
||||
|
||||
|
||||
for item in 0..10 {
|
||||
// "Second"
|
||||
div { "cool-{item}" }
|
||||
}
|
||||
|
||||
Link {
|
||||
to: "/home",
|
||||
class: "link {is_ready}",
|
||||
"Home"
|
||||
}
|
||||
|
||||
if false {
|
||||
div { "hi again!?" }
|
||||
} else if true {
|
||||
div { "its cool?" }
|
||||
} else {
|
||||
div { "not nice !" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let _parsed: RsxBlock = syn::parse2(input).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_comp_syntax() {
|
||||
let input = quote! {
|
||||
{ class: "inline-block mr-4", icons::icon_14 {} }
|
||||
};
|
||||
|
||||
let _parsed: RsxBlock = syn::parse2(input).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_sutter() {
|
||||
let input = quote! {
|
||||
{
|
||||
div {}
|
||||
d
|
||||
div {}
|
||||
}
|
||||
};
|
||||
|
||||
let _parsed: RsxBlock = syn::parse2(input).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn looks_like_prop_but_is_expr() {
|
||||
let input = quote! {
|
||||
{
|
||||
a: "asd".to_string(),
|
||||
// b can be omitted, and it will be filled with its default value
|
||||
c: "asd".to_string(),
|
||||
d: Some("asd".to_string()),
|
||||
e: Some("asd".to_string()),
|
||||
}
|
||||
};
|
||||
|
||||
let _parsed: RsxBlock = syn::parse2(input).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_comma_diagnostics() {
|
||||
let input = quote! {
|
||||
{ a, ..ComponentProps { a: 1, b: 2, c: 3, children: VNode::empty(), onclick: Default::default() } }
|
||||
};
|
||||
|
||||
let parsed: RsxBlock = syn::parse2(input).unwrap();
|
||||
assert!(parsed.diagnostics.is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn proper_attributes() {
|
||||
let input = quote! {
|
||||
{
|
||||
onclick: action,
|
||||
href,
|
||||
onmounted: onmounted,
|
||||
prevent_default,
|
||||
class,
|
||||
rel,
|
||||
target: tag_target,
|
||||
aria_current,
|
||||
..attributes,
|
||||
{children}
|
||||
}
|
||||
};
|
||||
|
||||
let parsed: RsxBlock = syn::parse2(input).unwrap();
|
||||
dbg!(parsed.attributes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reserved_attributes() {
|
||||
let input = quote! {
|
||||
{
|
||||
label {
|
||||
for: "blah",
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let parsed: RsxBlock = syn::parse2(input).unwrap();
|
||||
dbg!(parsed.attributes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnostics_check() {
|
||||
let input = quote::quote! {
|
||||
{
|
||||
class: "foo bar"
|
||||
"Hello world"
|
||||
}
|
||||
};
|
||||
|
||||
let _parsed: RsxBlock = syn::parse2(input).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_components() {
|
||||
let input = quote::quote! {
|
||||
{
|
||||
some::cool::Component
|
||||
}
|
||||
};
|
||||
|
||||
let _parsed: RsxBlock = syn::parse2(input).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
//! The actual rsx! macro implementation.
|
||||
//!
|
||||
//! This mostly just defers to the root TemplateBody with some additional tooling to provide better errors.
|
||||
//! Currently the additional tooling doesn't do much.
|
||||
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{quote, ToTokens};
|
||||
use std::{cell::Cell, fmt::Debug};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
Result,
|
||||
};
|
||||
|
||||
use crate::{BodyNode, TemplateBody};
|
||||
|
||||
/// The Callbody is the contents of the rsx! macro
|
||||
///
|
||||
/// It is a list of BodyNodes, which are the different parts of the template.
|
||||
/// The Callbody contains no information about how the template will be rendered, only information about the parsed tokens.
|
||||
///
|
||||
/// Every callbody should be valid, so you can use it to build a template.
|
||||
/// To generate the code used to render the template, use the ToTokens impl on the Callbody, or with the `render_with_location` method.
|
||||
///
|
||||
/// Ideally we don't need the metadata here and can bake the idx-es into the templates themselves but I haven't figured out how to do that yet.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CallBody {
|
||||
pub body: TemplateBody,
|
||||
pub ifmt_idx: Cell<usize>,
|
||||
pub template_idx: Cell<usize>,
|
||||
}
|
||||
|
||||
impl Parse for CallBody {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
// Defer to the `new` method such that we can wire up hotreload information
|
||||
Ok(CallBody::new(input.parse()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for CallBody {
|
||||
fn to_tokens(&self, out: &mut TokenStream2) {
|
||||
match self.body.is_empty() {
|
||||
true => quote! { dioxus_core::VNode::empty() }.to_tokens(out),
|
||||
false => self.body.to_tokens(out),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CallBody {
|
||||
/// Create a new CallBody from a TemplateBody
|
||||
///
|
||||
/// This will overwrite all internal metadata regarding hotreloading.
|
||||
pub fn new(body: TemplateBody) -> Self {
|
||||
let body = CallBody {
|
||||
body,
|
||||
ifmt_idx: Cell::new(0),
|
||||
template_idx: Cell::new(0),
|
||||
};
|
||||
|
||||
body.body.template_idx.set(body.next_template_idx());
|
||||
|
||||
body.cascade_hotreload_info(&body.body.roots);
|
||||
|
||||
body
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
// todo: actually throw warnings if there are any
|
||||
Self::parse(input)
|
||||
}
|
||||
|
||||
/// With the entire knowledge of the macro call, wire up location information for anything hotreloading
|
||||
/// specific. It's a little bit simpler just to have a global id per callbody than to try and track it
|
||||
/// relative to each template, though we could do that if we wanted to.
|
||||
///
|
||||
/// For now this is just information for ifmts and templates so that when they generate, they can be
|
||||
/// tracked back to the original location in the source code, to support formatted string hotreloading.
|
||||
///
|
||||
/// Note that there are some more complex cases we could in theory support, but have bigger plans
|
||||
/// to enable just pure rust hotreloading that would make those tricks moot. So, manage more of
|
||||
/// the simple cases until that proper stuff ships.
|
||||
///
|
||||
/// We need to make sure to wire up:
|
||||
/// - subtemplate IDs
|
||||
/// - ifmt IDs
|
||||
/// - dynamic node IDs
|
||||
/// - dynamic attribute IDs
|
||||
/// - paths for dynamic nodes and attributes
|
||||
///
|
||||
/// Lots of wiring!
|
||||
///
|
||||
/// However, here, we only need to wire up ifmt and template IDs since TemplateBody will handle the rest.
|
||||
///
|
||||
/// This is better though since we can save the relevant data on the structures themselves.
|
||||
fn cascade_hotreload_info(&self, nodes: &[BodyNode]) {
|
||||
for node in nodes.iter() {
|
||||
match node {
|
||||
BodyNode::RawExpr(_) => { /* one day maybe provide hr here?*/ }
|
||||
|
||||
BodyNode::Text(text) => {
|
||||
// one day we could also provide HR here to allow dynamic parts on the fly
|
||||
if !text.is_static() {
|
||||
text.hr_idx.set(self.next_ifmt_idx());
|
||||
}
|
||||
}
|
||||
|
||||
BodyNode::Element(el) => {
|
||||
// Walk the attributes looking for hotreload opportunities
|
||||
for attr in &el.merged_attributes {
|
||||
attr.with_literal(|lit| lit.hr_idx.set(self.next_ifmt_idx()));
|
||||
}
|
||||
|
||||
self.cascade_hotreload_info(&el.children);
|
||||
}
|
||||
|
||||
BodyNode::Component(comp) => {
|
||||
// walk the props looking for hotreload opportunities
|
||||
for prop in comp.fields.iter() {
|
||||
prop.with_literal(|lit| lit.hr_idx.set(self.next_ifmt_idx()));
|
||||
}
|
||||
|
||||
comp.children.template_idx.set(self.next_template_idx());
|
||||
self.cascade_hotreload_info(&comp.children.roots);
|
||||
}
|
||||
|
||||
BodyNode::ForLoop(floop) => {
|
||||
floop.body.template_idx.set(self.next_template_idx());
|
||||
self.cascade_hotreload_info(&floop.body.roots);
|
||||
}
|
||||
|
||||
BodyNode::IfChain(chain) => chain.for_each_branch(&mut |body| {
|
||||
body.template_idx.set(self.next_template_idx());
|
||||
self.cascade_hotreload_info(&body.roots)
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn next_ifmt_idx(&self) -> usize {
|
||||
let idx = self.ifmt_idx.get();
|
||||
self.ifmt_idx.set(idx + 1);
|
||||
idx
|
||||
}
|
||||
|
||||
fn next_template_idx(&self) -> usize {
|
||||
let idx = self.template_idx.get();
|
||||
self.template_idx.set(idx + 1);
|
||||
idx
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
#![cfg(feature = "hot_reload")]
|
||||
|
||||
use crate::{Attribute, AttributeValue, BodyNode, HotLiteralType, IfmtInput};
|
||||
|
||||
/// Take two nodes and return their similarity score
|
||||
///
|
||||
/// This is not normalized or anything, so longer nodes will have higher scores
|
||||
pub fn score_dynamic_node(old_node: &BodyNode, new_node: &BodyNode) -> usize {
|
||||
use BodyNode::*;
|
||||
|
||||
match (old_node, new_node) {
|
||||
(Element(_), Element(_)) => unreachable!("Elements are not dynamic nodes"),
|
||||
|
||||
(Text(old), Text(new)) => {
|
||||
// We shouldn't be seeing static text nodes here
|
||||
assert!(!old.input.is_static() && !new.input.is_static());
|
||||
score_ifmt(&old.input, &new.input)
|
||||
}
|
||||
|
||||
(RawExpr(old), RawExpr(new)) if old == new => usize::MAX,
|
||||
|
||||
(Component(old), Component(new))
|
||||
if old.name == new.name
|
||||
&& old.generics == new.generics
|
||||
&& old.fields.len() == new.fields.len() =>
|
||||
{
|
||||
let mut score = 1;
|
||||
|
||||
// todo: there might be a bug here where Idents and Strings will result in a match
|
||||
let mut left_fields = old.fields.iter().collect::<Vec<_>>();
|
||||
left_fields.sort_by(|a, b| a.name.to_string().cmp(&b.name.to_string()));
|
||||
|
||||
let mut right_fields = new.fields.iter().collect::<Vec<_>>();
|
||||
right_fields.sort_by(|a, b| a.name.to_string().cmp(&b.name.to_string()));
|
||||
|
||||
// Walk the attributes and score each one - if there's a zero we return zero
|
||||
// circuit if we there's an attribute mismatch that can't be hotreloaded
|
||||
for (left, right) in left_fields.iter().zip(right_fields.iter()) {
|
||||
let scored = match score_attribute(left, right) {
|
||||
usize::MAX => 3,
|
||||
0 => return 0,
|
||||
a if a == usize::MAX - 1 => 2,
|
||||
a => a,
|
||||
};
|
||||
|
||||
score += scored;
|
||||
}
|
||||
|
||||
score
|
||||
}
|
||||
|
||||
(ForLoop(a), ForLoop(b)) if a.pat == b.pat && a.expr == b.expr => {
|
||||
// The bodies don't necessarily need to be the same, but we should throw some simple heuristics at them to
|
||||
// encourage proper selection. For now just double check the templates are roughly the same
|
||||
1 + (a.body.roots.len() == b.body.roots.len()) as usize
|
||||
+ (a.body.node_paths.len() == b.body.node_paths.len()) as usize
|
||||
+ (a.body.attr_paths.len() == b.body.attr_paths.len()) as usize
|
||||
}
|
||||
|
||||
(IfChain(a), IfChain(b)) if a.cond == b.cond => {
|
||||
// The bodies don't necessarily need to be the same, but we should throw some simple heuristics at them to
|
||||
// encourage proper selection. For now just double check the templates are roughly the same
|
||||
1 + (a.then_branch.roots.len() == b.then_branch.roots.len()) as usize
|
||||
+ (a.then_branch.node_paths.len() == b.then_branch.node_paths.len()) as usize
|
||||
+ (a.then_branch.attr_paths.len() == b.then_branch.attr_paths.len()) as usize
|
||||
}
|
||||
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn score_attribute(old_attr: &Attribute, new_attr: &Attribute) -> usize {
|
||||
if old_attr.name != new_attr.name {
|
||||
return 0;
|
||||
}
|
||||
|
||||
score_attr_value(&old_attr.value, &new_attr.value)
|
||||
}
|
||||
|
||||
fn score_attr_value(old_attr: &AttributeValue, new_attr: &AttributeValue) -> usize {
|
||||
use AttributeValue::*;
|
||||
use HotLiteralType::*;
|
||||
|
||||
match (&old_attr, &new_attr) {
|
||||
// For literals, the value itself might change, but what's more important is the
|
||||
// structure of the literal. If the structure is the same, we can hotreload it
|
||||
// Ideally the value doesn't change, but we're hoping that our stack approach
|
||||
// Will prevent spurious reloads
|
||||
//
|
||||
// todo: maybe it's a good idea to modify the original in place?
|
||||
// todo: float to int is a little weird case that we can try to support better
|
||||
// right now going from float to int or vice versa will cause a full rebuild
|
||||
// which can get confusing. if we can figure out a way to hotreload this, that'd be great
|
||||
(AttrLiteral(left), AttrLiteral(right)) => {
|
||||
// We assign perfect matches for token resuse, to minimize churn on the renderer
|
||||
match (&left.value, &right.value) {
|
||||
// Quick shortcut if there's no change
|
||||
(Fmted(old), Fmted(new)) if old == new => usize::MAX,
|
||||
|
||||
// We can remove formatted bits but we can't add them. The scoring here must
|
||||
// realize that every bit of the new formatted segment must be in the old formatted segment
|
||||
(Fmted(old), Fmted(new)) => score_ifmt(old, new),
|
||||
|
||||
(Float(a), Float(b)) if a == b => usize::MAX,
|
||||
(Float(_), Float(_)) => 1,
|
||||
|
||||
(Int(a), Int(b)) if a == b => usize::MAX,
|
||||
(Int(_), Int(_)) => 1,
|
||||
|
||||
(Bool(a), Bool(b)) if a == b => usize::MAX,
|
||||
(Bool(_), Bool(_)) => 1,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
AttrOptionalExpr {
|
||||
condition: cond_a,
|
||||
value: value_a,
|
||||
},
|
||||
AttrOptionalExpr {
|
||||
condition: cond_b,
|
||||
value: value_b,
|
||||
},
|
||||
) if cond_a == cond_b => {
|
||||
// If the condition is the same, we can hotreload it
|
||||
score_attr_value(value_a, value_b)
|
||||
}
|
||||
|
||||
// todo: we should try and score recrusively if we can - templates need to propagate up their
|
||||
// scores. That would lead to a time complexity explosion but can be helpful in some cases.
|
||||
//
|
||||
// If it's expression-type things, we give a perfect score if they match completely
|
||||
_ if old_attr == new_attr => usize::MAX,
|
||||
|
||||
// If it's not a match, we give it a score of 0
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn score_ifmt(old: &IfmtInput, new: &IfmtInput) -> usize {
|
||||
// If they're the same by source, return max
|
||||
if old == new {
|
||||
return usize::MAX;
|
||||
}
|
||||
|
||||
// Default score to 1 - an ifmt with no dynamic segments still technically has a score of 1
|
||||
// since it's not disqualified, but it's not a perfect match
|
||||
let mut score = 1;
|
||||
let mut l_freq_map = old.dynamic_seg_frequency_map();
|
||||
|
||||
// Pluck out the dynamic segments from the other input
|
||||
for seg in new.dynamic_segments() {
|
||||
let Some(ct) = l_freq_map.get_mut(seg) else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
*ct -= 1;
|
||||
|
||||
if *ct == 0 {
|
||||
l_freq_map.remove(seg);
|
||||
}
|
||||
|
||||
score += 1;
|
||||
}
|
||||
|
||||
// If there's nothing remaining - a perfect match - return max -1
|
||||
// We compared the sources to start, so we know they're different in some way
|
||||
if l_freq_map.is_empty() {
|
||||
usize::MAX - 1
|
||||
} else {
|
||||
score
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::PopVec;
|
||||
|
||||
use super::*;
|
||||
use quote::quote;
|
||||
use syn::parse2;
|
||||
|
||||
#[test]
|
||||
fn score_components() {
|
||||
let a: BodyNode = parse2(quote! {
|
||||
for x in 0..1 {
|
||||
SomeComponent {
|
||||
count: 19999123,
|
||||
enabled: false,
|
||||
title: "pxasd-5 {x}",
|
||||
flot: 1233.5,
|
||||
height: 100,
|
||||
width: 500,
|
||||
color: "reasdssdasd {x}",
|
||||
handler: move |e| {
|
||||
println!("clickeasdd!");
|
||||
},
|
||||
"sick!! asasdsd!lasdasasdasddasdkasjdlkasjdlk!! {x}"
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let b: BodyNode = parse2(quote! {
|
||||
for x in 0..1 {
|
||||
SomeComponent {
|
||||
count: 19999123,
|
||||
enabled: false,
|
||||
title: "pxasd-5 {x}",
|
||||
flot: 1233.5,
|
||||
height: 100,
|
||||
width: 500,
|
||||
color: "reasdssdasd {x}",
|
||||
handler: move |e| {
|
||||
println!("clickeasdd!");
|
||||
},
|
||||
"sick!! asasdsd!lasdasasdaasdasdsddasdkasjdlkasjdlk!! {x}"
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let score = score_dynamic_node(&a, &b);
|
||||
assert_eq!(score, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn score_attributes() {
|
||||
let left: Attribute = parse2(quote! { attr: 123 }).unwrap();
|
||||
let right: Attribute = parse2(quote! { attr: 123 }).unwrap();
|
||||
assert_eq!(score_attribute(&left, &right), usize::MAX);
|
||||
|
||||
let left: Attribute = parse2(quote! { attr: 123 }).unwrap();
|
||||
let right: Attribute = parse2(quote! { attr: 456 }).unwrap();
|
||||
assert_eq!(score_attribute(&left, &right), 1);
|
||||
|
||||
// almost a perfect match
|
||||
let left: Attribute = parse2(quote! { class: if count > 3 { "blah {abc}" } }).unwrap();
|
||||
let right: Attribute = parse2(quote! { class: if count > 3 { "other {abc}" } }).unwrap();
|
||||
assert_eq!(score_attribute(&left, &right), usize::MAX - 1);
|
||||
}
|
||||
|
||||
/// Ensure the scoring algorithm works
|
||||
///
|
||||
/// - usize::MAX is return for perfect overlap
|
||||
/// - 0 is returned when the right case has segments not found in the first
|
||||
/// - a number for the other cases where there is some non-perfect overlap
|
||||
#[test]
|
||||
fn ifmt_scoring() {
|
||||
let left: IfmtInput = "{abc} {def}".parse().unwrap();
|
||||
let right: IfmtInput = "{abc}".parse().unwrap();
|
||||
assert_eq!(score_ifmt(&left, &right), 2);
|
||||
|
||||
let left: IfmtInput = "{abc} {def}".parse().unwrap();
|
||||
let right: IfmtInput = "{abc} {def}".parse().unwrap();
|
||||
assert_eq!(score_ifmt(&left, &right), usize::MAX);
|
||||
|
||||
let left: IfmtInput = "{abc} {def}".parse().unwrap();
|
||||
let right: IfmtInput = "{abc} {ghi}".parse().unwrap();
|
||||
assert_eq!(score_ifmt(&left, &right), 0);
|
||||
|
||||
let left: IfmtInput = "{abc} {def}".parse().unwrap();
|
||||
let right: IfmtInput = "{abc} {def} {ghi}".parse().unwrap();
|
||||
assert_eq!(score_ifmt(&left, &right), 0);
|
||||
|
||||
let left: IfmtInput = "{abc} {def} {ghi}".parse().unwrap();
|
||||
let right: IfmtInput = "{abc} {def}".parse().unwrap();
|
||||
assert_eq!(score_ifmt(&left, &right), 3);
|
||||
|
||||
let left: IfmtInput = "{abc}".parse().unwrap();
|
||||
let right: IfmtInput = "{abc} {def}".parse().unwrap();
|
||||
assert_eq!(score_ifmt(&left, &right), 0);
|
||||
|
||||
let left: IfmtInput = "{abc} {abc} {def}".parse().unwrap();
|
||||
let right: IfmtInput = "{abc} {def}".parse().unwrap();
|
||||
assert_eq!(score_ifmt(&left, &right), 3);
|
||||
|
||||
let left: IfmtInput = "{abc} {abc}".parse().unwrap();
|
||||
let right: IfmtInput = "{abc} {abc}".parse().unwrap();
|
||||
assert_eq!(score_ifmt(&left, &right), usize::MAX);
|
||||
|
||||
let left: IfmtInput = "{abc} {def}".parse().unwrap();
|
||||
let right: IfmtInput = "{hij}".parse().unwrap();
|
||||
assert_eq!(score_ifmt(&left, &right), 0);
|
||||
|
||||
let left: IfmtInput = "{abc}".parse().unwrap();
|
||||
let right: IfmtInput = "thing {abc}".parse().unwrap();
|
||||
assert_eq!(score_ifmt(&left, &right), usize::MAX - 1);
|
||||
|
||||
let left: IfmtInput = "thing {abc}".parse().unwrap();
|
||||
let right: IfmtInput = "{abc}".parse().unwrap();
|
||||
assert_eq!(score_ifmt(&left, &right), usize::MAX - 1);
|
||||
|
||||
let left: IfmtInput = "{abc} {def}".parse().unwrap();
|
||||
let right: IfmtInput = "thing {abc}".parse().unwrap();
|
||||
assert_eq!(score_ifmt(&left, &right), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_scoring() {
|
||||
let stack: PopVec<IfmtInput> = PopVec::new(
|
||||
vec![
|
||||
"{abc} {def}".parse().unwrap(),
|
||||
"{def}".parse().unwrap(),
|
||||
"{hij}".parse().unwrap(),
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
|
||||
let tests = vec![
|
||||
"thing {def}".parse().unwrap(),
|
||||
"thing {abc}".parse().unwrap(),
|
||||
"thing {hij}".parse().unwrap(),
|
||||
];
|
||||
|
||||
for item in tests {
|
||||
let score = stack.highest_score(|f| score_ifmt(f, &item));
|
||||
|
||||
dbg!(item, score);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
//! Logic for handlng sub-templates
|
||||
//!
|
||||
//! These are things like for/if and component calls that are expanded by the contaning macro but turn into templates
|
||||
//! separate template calls in the final output.
|
||||
//!
|
||||
//! Instead of recursively expanding sub-templates in place - and thus losing location information - we collect them
|
||||
//! into a list and then expand them at the end.
|
||||
//!
|
||||
//! The goal here is to provide a stable index for each sub-template in a way that we can find the for/if statements later.
|
||||
//! This requires some sort of hashing + indexing of the sub-templates.
|
||||
//!
|
||||
//! When expanding the for/if statements, we still want to expand them in the correct order, but we defer the actual token
|
||||
//! expansion until the end. This way the `TEMPLATE` static that subtemplate references is available *before* we convert
|
||||
//! an `if` statement into an actual vnode.
|
||||
//!
|
||||
//! Expanding a template will generate two token streams - one for the vnode creation and one for list of templates.
|
||||
//!
|
||||
//! While semantically it might make sense for a macro call that sees its dynamic contents to own those static nodes,
|
||||
//! core is currently designed around the assumption that the list of Dynamic attributes matches 1:1 the template it
|
||||
//! belongs to. This means that whatever dynamic nodes are rendered need to match the template they are rendered in,
|
||||
//! hence why we need to spit out true subtemplates rather than just building a "super template".
|
||||
//!
|
||||
//! I would prefer we used the "super template" strategy since it's more flexible when diffing - no need to keep track
|
||||
//! of nested templates, and we can just diff the super template and the sub templates as a whole.
|
||||
//!
|
||||
//! However core would need to be redesigned to support this strategy, and I'm not sure if it's worth the effort.
|
||||
//! It's just conceptually easier to have the sub-templates be a part of the template they are rendered in, and then
|
||||
//! deal with the complicated rsx diffing logic in the rsx crate itself.
|
||||
//!
|
||||
//! /// The subtemplates that we've discovered that need to be updated
|
||||
//! /// These will be collected just by recursing the body nodes of various items like for/if/components etc
|
||||
//! ///
|
||||
//! /// The locations of these templates will be the same location as the original template, with
|
||||
//! /// the addition of the dynamic node index that the template belongs to.
|
||||
//! ///
|
||||
//! /// rsx! { <-------- this is the location of the template "file.rs:123:0:0"
|
||||
//! /// div {
|
||||
//! /// for i in 0..10 { <-------- dyn_node(0)
|
||||
//! /// "hi" <-------- template with location "file.rs:123:0:1" (original name, but with the dynamic node index)
|
||||
//! /// }
|
||||
//! /// }
|
||||
//! /// }
|
||||
//! ///
|
||||
//! /// We only track this when the template changes
|
||||
//! pub discovered_templates: Vec<crate::Template>,
|
||||
@@ -0,0 +1,381 @@
|
||||
//! I'm so sorry this is so complicated. Here's my best to simplify and explain it:
|
||||
//!
|
||||
//! The `Callbody` is the contents of the rsx! macro - this contains all the information about every
|
||||
//! node that rsx! directly knows about. For loops, if statements, etc.
|
||||
//!
|
||||
//! However, thre are multiple *templates* inside a callbody - due to how core clones templates and
|
||||
//! just generally rationalize the concept of a template, nested bodies like for loops and if statements
|
||||
//! and component children are all templates, contained within the same Callbody.
|
||||
//!
|
||||
//! This gets confusing fast since there's lots of IDs bouncing around.
|
||||
//!
|
||||
//! The IDs at play:
|
||||
//! - The id of the template itself so we can find it and apply it to the dom.
|
||||
//! This is challenging since all calls to file/line/col/id are relative to the macro invocation,
|
||||
//! so they will have to share the same base ID and we need to give each template a new ID.
|
||||
//! The id of the template will be something like file!():line!():col!():ID where ID increases for
|
||||
//! each nested template.
|
||||
//!
|
||||
//! - The IDs of dynamic nodes relative to the template they live in. This is somewhat easy to track
|
||||
//! but needs to happen on a per-template basis.
|
||||
//!
|
||||
//! - The unique ID of a hotreloadable literal (like ifmt or integers or strings, etc). This ID is
|
||||
//! unique to the Callbody, not necessarily the template it lives in. This is similar to the
|
||||
//! template ID
|
||||
//!
|
||||
//! We solve this by parsing the structure completely and then doing a second pass that fills in IDs
|
||||
//! by walking the structure.
|
||||
//!
|
||||
//! This means you can't query the ID of any node "in a vacuum" - these are assigned once - but at
|
||||
//! least they're stable enough for the purposes of hotreloading
|
||||
//!
|
||||
//! The plumbing for hotreloadable literals could be template relative... ie "file:line:col:template:idx"
|
||||
//! That would be ideal if we could determine the the idx only relative to the template
|
||||
//!
|
||||
//! ```rust, ignore
|
||||
//! rsx! {
|
||||
//! div {
|
||||
//! class: "hello",
|
||||
//! id: "node-{node_id}", <--- hotreloadable with ID 0
|
||||
//! ..props, <--- spreads are not reloadable
|
||||
//!
|
||||
//! "Hello, world! <--- not tracked but reloadable since it's just a string
|
||||
//!
|
||||
//! for item in 0..10 { <--- both 0 and 10 are technically reloadable...
|
||||
//! div { "cool-{item}" } <--- the ifmt here is also reloadable
|
||||
//! }
|
||||
//!
|
||||
//! Link {
|
||||
//! to: "/home", <-- hotreloadable since its a component prop
|
||||
//! class: "link {is_ready}", <-- hotreloadable since its a formatted string as a prop
|
||||
//! "Home" <-- hotreloadable since its a component child (via template)
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use self::location::DynIdx;
|
||||
use crate::innerlude::Attribute;
|
||||
use crate::*;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use proc_macro2_diagnostics::SpanDiagnosticExt;
|
||||
use syn::token::Brace;
|
||||
|
||||
#[cfg(feature = "hot_reload")]
|
||||
use dioxus_core::prelude::Template;
|
||||
|
||||
type NodePath = Vec<u8>;
|
||||
type AttributePath = Vec<u8>;
|
||||
|
||||
/// A set of nodes in a template position
|
||||
///
|
||||
/// this could be:
|
||||
/// - The root of a callbody
|
||||
/// - The children of a component
|
||||
/// - The children of a for loop
|
||||
/// - The children of an if chain
|
||||
///
|
||||
/// The TemplateBody when needs to be parsed into a surrounding `Body` to be correctly re-indexed
|
||||
/// By default every body has a `0` default index
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub struct TemplateBody {
|
||||
pub roots: Vec<BodyNode>,
|
||||
pub template_idx: DynIdx,
|
||||
pub node_paths: Vec<NodePath>,
|
||||
pub attr_paths: Vec<(AttributePath, usize)>,
|
||||
pub diagnostic: Diagnostics,
|
||||
current_path: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Parse for TemplateBody {
|
||||
/// Parse the nodes of the callbody as `Body`.
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let brace = Brace::default();
|
||||
|
||||
let RsxBlock {
|
||||
brace: _,
|
||||
attributes,
|
||||
spreads,
|
||||
children,
|
||||
diagnostics: _, // we don't care about the diagnostics here - ours might be different
|
||||
} = RsxBlock::parse_inner(input, brace)?;
|
||||
|
||||
let mut template = Self::new(children);
|
||||
for spread in spreads {
|
||||
template.diagnostic.push(
|
||||
spread
|
||||
.span()
|
||||
.error("Spreads are only allowed in elements and components"),
|
||||
);
|
||||
}
|
||||
for attr in attributes {
|
||||
template.diagnostic.push(
|
||||
attr.span()
|
||||
.error("Attributes are only allowed in elements and components"),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(template)
|
||||
}
|
||||
}
|
||||
|
||||
/// Our ToTokens impl here just defers to rendering a template out like any other `Body`.
|
||||
/// This is because the parsing phase filled in all the additional metadata we need
|
||||
impl ToTokens for TemplateBody {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
// If there are no roots, this is an empty template, so just return None
|
||||
if self.roots.is_empty() {
|
||||
return tokens.append_all(quote! { dioxus_core::VNode::empty() });
|
||||
}
|
||||
|
||||
// If we have an implicit key, then we need to write its tokens
|
||||
let key_tokens = match self.implicit_key() {
|
||||
Some(tok) => quote! { Some( #tok.to_string() ) },
|
||||
None => quote! { None },
|
||||
};
|
||||
|
||||
let TemplateBody { roots, .. } = self;
|
||||
let roots = roots.iter().map(|node| match node {
|
||||
BodyNode::Element(el) => quote! { #el },
|
||||
BodyNode::Text(text) if text.is_static() => {
|
||||
let text = text.input.to_static().unwrap();
|
||||
quote! { dioxus_core::TemplateNode::Text { text: #text } }
|
||||
}
|
||||
BodyNode::Text(text) => {
|
||||
let id = text.dyn_idx.get();
|
||||
quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
|
||||
}
|
||||
BodyNode::ForLoop(floop) => {
|
||||
let id = floop.dyn_idx.get();
|
||||
quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
|
||||
}
|
||||
BodyNode::RawExpr(exp) => {
|
||||
let id = exp.dyn_idx.get();
|
||||
quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
|
||||
}
|
||||
BodyNode::Component(exp) => {
|
||||
let id = exp.dyn_idx.get();
|
||||
quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
|
||||
}
|
||||
BodyNode::IfChain(exp) => {
|
||||
let id = exp.dyn_idx.get();
|
||||
quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
|
||||
}
|
||||
});
|
||||
|
||||
// Print paths is easy - just print the paths
|
||||
let node_paths = self.node_paths.iter().map(|it| quote!(&[#(#it),*]));
|
||||
let attr_paths = self.attr_paths.iter().map(|(it, _)| quote!(&[#(#it),*]));
|
||||
|
||||
// For printing dynamic nodes, we rely on the ToTokens impl
|
||||
// Elements have a weird ToTokens - they actually are the entrypoint for Template creation
|
||||
let dynamic_nodes = self.node_paths.iter().map(|path| {
|
||||
let node = self.get_dyn_node(path);
|
||||
quote::quote! { #node }
|
||||
});
|
||||
|
||||
// We could add a ToTokens for Attribute but since we use that for both components and elements
|
||||
// They actually need to be different, so we just localize that here
|
||||
let dyn_attr_printer = self
|
||||
.attr_paths
|
||||
.iter()
|
||||
.map(|(path, idx)| self.get_dyn_attr(path, *idx).rendered_as_dynamic_attr());
|
||||
|
||||
// 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");
|
||||
// let index = if looks_like_rust_analyzer {
|
||||
// "0".to_string()
|
||||
// } else {
|
||||
// self.template_idx.get().to_string()
|
||||
// };
|
||||
// todo: this just might be fixed?
|
||||
let index = self.template_idx.get();
|
||||
|
||||
tokens.append_all(quote! {
|
||||
dioxus_core::Element::Ok({
|
||||
#[doc(hidden)] // vscode please stop showing these in symbol search
|
||||
static ___TEMPLATE: dioxus_core::Template = dioxus_core::Template {
|
||||
name: concat!( file!(), ":", line!(), ":", column!(), ":", #index ) ,
|
||||
roots: &[ #( #roots ),* ],
|
||||
node_paths: &[ #( #node_paths ),* ],
|
||||
attr_paths: &[ #( #attr_paths ),* ],
|
||||
};
|
||||
|
||||
{
|
||||
// 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,
|
||||
Box::new([ #( #dynamic_nodes),* ]),
|
||||
Box::new([ #( #dyn_attr_printer ),* ]),
|
||||
);
|
||||
__vnodes
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl TemplateBody {
|
||||
/// Create a new TemplateBody from a set of nodes
|
||||
///
|
||||
/// This will fill in all the necessary path information for the nodes in the template and will
|
||||
/// overwrite data like dynamic indexes.
|
||||
pub fn new(nodes: Vec<BodyNode>) -> Self {
|
||||
let mut body = Self {
|
||||
roots: vec![],
|
||||
template_idx: DynIdx::default(),
|
||||
node_paths: Vec::new(),
|
||||
attr_paths: Vec::new(),
|
||||
current_path: Vec::new(),
|
||||
diagnostic: Diagnostics::new(),
|
||||
};
|
||||
|
||||
// Assign paths to all nodes in the template
|
||||
body.assign_paths_inner(&nodes);
|
||||
|
||||
// And then save the roots
|
||||
body.roots = nodes;
|
||||
|
||||
body
|
||||
}
|
||||
|
||||
/// Cascade down path information into the children of this template
|
||||
///
|
||||
/// This provides the necessary path and index information for the children of this template
|
||||
/// so that they can render out their dynamic nodes correctly. Also does plumbing for things like
|
||||
/// hotreloaded literals which need to be tracked on a per-template basis.
|
||||
///
|
||||
/// This can only operate with knowledge of this template, not the surrounding callbody. Things like
|
||||
/// wiring of ifmt literals need to be done at the callbody level since those final IDs need to
|
||||
/// be unique to the entire app.
|
||||
fn assign_paths_inner(&mut self, nodes: &[BodyNode]) {
|
||||
for (idx, node) in nodes.iter().enumerate() {
|
||||
self.current_path.push(idx as u8);
|
||||
match node {
|
||||
// Just descend into elements - they're not dynamic
|
||||
BodyNode::Element(el) => {
|
||||
for (idx, attr) in el.merged_attributes.iter().enumerate() {
|
||||
if !attr.is_static_str_literal() {
|
||||
attr.dyn_idx.set(self.attr_paths.len());
|
||||
self.attr_paths.push((self.current_path.clone(), idx));
|
||||
}
|
||||
}
|
||||
|
||||
self.assign_paths_inner(&el.children)
|
||||
}
|
||||
|
||||
// Text nodes are dynamic if they contain dynamic segments
|
||||
BodyNode::Text(txt) => {
|
||||
if !txt.is_static() {
|
||||
self.assign_path_to(node);
|
||||
}
|
||||
}
|
||||
|
||||
// Raw exprs are always dynamic
|
||||
BodyNode::RawExpr(_)
|
||||
| BodyNode::ForLoop(_)
|
||||
| BodyNode::Component(_)
|
||||
| BodyNode::IfChain(_) => self.assign_path_to(node),
|
||||
};
|
||||
self.current_path.pop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Assign a path to a node and give it its dynamic index
|
||||
/// This simplifies the ToTokens implementation for the macro to be a little less centralized
|
||||
fn assign_path_to(&mut self, node: &BodyNode) {
|
||||
// Assign the TemplateNode::Dynamic index to the node
|
||||
node.set_dyn_idx(self.node_paths.len());
|
||||
|
||||
// And then save the current path as the corresponding path
|
||||
self.node_paths.push(self.current_path.clone());
|
||||
}
|
||||
|
||||
/// Create a new template from this TemplateBody
|
||||
///
|
||||
/// Note that this will leak memory! We explicitly call `leak` on the vecs to match the format of
|
||||
/// the `Template` struct.
|
||||
#[cfg(feature = "hot_reload")]
|
||||
pub fn to_template<Ctx: HotReloadingContext>(&self) -> Template {
|
||||
self.to_template_with_custom_paths::<Ctx>(
|
||||
"placeholder",
|
||||
self.node_paths.clone(),
|
||||
self.attr_paths.clone().into_iter().map(|v| v.0).collect(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "hot_reload")]
|
||||
pub fn to_template_with_custom_paths<Ctx: HotReloadingContext>(
|
||||
&self,
|
||||
location: &'static str,
|
||||
node_paths: Vec<NodePath>,
|
||||
attr_paths: Vec<AttributePath>,
|
||||
) -> Template {
|
||||
let roots = self
|
||||
.roots
|
||||
.iter()
|
||||
.map(|node| node.to_template_node::<Ctx>())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Template {
|
||||
name: location,
|
||||
roots: intern(roots.as_slice()),
|
||||
node_paths: intern(
|
||||
node_paths
|
||||
.into_iter()
|
||||
.map(|path| intern(path.as_slice()))
|
||||
.collect::<Vec<_>>()
|
||||
.as_slice(),
|
||||
),
|
||||
attr_paths: intern(
|
||||
attr_paths
|
||||
.into_iter()
|
||||
.map(|path| intern(path.as_slice()))
|
||||
.collect::<Vec<_>>()
|
||||
.as_slice(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.roots.is_empty()
|
||||
}
|
||||
|
||||
fn implicit_key(&self) -> Option<IfmtInput> {
|
||||
match self.roots.first() {
|
||||
Some(BodyNode::Element(el)) if self.roots.len() == 1 => el.key().cloned(),
|
||||
Some(BodyNode::Component(comp)) if self.roots.len() == 1 => {
|
||||
comp.get_key().and_then(|f| f.ifmt().cloned())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_dyn_node(&self, path: &[u8]) -> &BodyNode {
|
||||
let mut node = self.roots.get(path[0] as usize).unwrap();
|
||||
for idx in path.iter().skip(1) {
|
||||
node = node.element_children().get(*idx as usize).unwrap();
|
||||
}
|
||||
node
|
||||
}
|
||||
|
||||
pub fn get_dyn_attr(&self, path: &AttributePath, idx: usize) -> &Attribute {
|
||||
match self.get_dyn_node(path) {
|
||||
BodyNode::Element(el) => &el.merged_attributes[idx],
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dynamic_attributes(&self) -> impl DoubleEndedIterator<Item = &Attribute> {
|
||||
self.attr_paths
|
||||
.iter()
|
||||
.map(|(path, idx)| self.get_dyn_attr(path, *idx))
|
||||
}
|
||||
|
||||
pub fn dynamic_nodes(&self) -> impl DoubleEndedIterator<Item = &BodyNode> {
|
||||
self.node_paths.iter().map(|path| self.get_dyn_node(path))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
#[cfg(feature = "hot_reload")]
|
||||
use dioxus_core::TemplateNode;
|
||||
|
||||
use crate::{
|
||||
literal::{HotLiteral, HotLiteralType},
|
||||
location::DynIdx,
|
||||
IfmtInput,
|
||||
};
|
||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||
use quote::ToTokens;
|
||||
use quote::{quote, TokenStreamExt};
|
||||
use syn::Result;
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
LitStr,
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
||||
pub struct TextNode {
|
||||
pub input: IfmtInput,
|
||||
pub hr_idx: DynIdx,
|
||||
pub dyn_idx: DynIdx,
|
||||
}
|
||||
|
||||
impl Parse for TextNode {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
Ok(Self {
|
||||
input: input.parse()?,
|
||||
hr_idx: DynIdx::default(),
|
||||
dyn_idx: DynIdx::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for TextNode {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let txt = &self.input;
|
||||
|
||||
if txt.is_static() {
|
||||
tokens.append_all(quote! {
|
||||
dioxus_core::DynamicNode::Text(dioxus_core::VText::new(#txt.to_string()))
|
||||
})
|
||||
} else {
|
||||
// todo:
|
||||
// Use the RsxLiteral implementation to spit out a hotreloadable variant of this string
|
||||
// This is not super efficient since we're doing a bit of cloning
|
||||
let as_lit = HotLiteral {
|
||||
hr_idx: self.hr_idx.clone(),
|
||||
value: HotLiteralType::Fmted(txt.clone()),
|
||||
};
|
||||
|
||||
tokens.append_all(quote! {
|
||||
dioxus_core::DynamicNode::Text(dioxus_core::VText::new( #as_lit ))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextNode {
|
||||
pub fn from_text(text: &str) -> Self {
|
||||
let ifmt = IfmtInput {
|
||||
source: LitStr::new(text, Span::call_site()),
|
||||
segments: vec![],
|
||||
};
|
||||
Self {
|
||||
input: ifmt,
|
||||
dyn_idx: Default::default(),
|
||||
hr_idx: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_static(&self) -> bool {
|
||||
self.input.is_static()
|
||||
}
|
||||
|
||||
#[cfg(feature = "hot_reload")]
|
||||
pub fn to_template_node(&self) -> TemplateNode {
|
||||
use crate::intern;
|
||||
match self.is_static() {
|
||||
true => {
|
||||
let text = self.input.source.clone();
|
||||
let text = intern(text.value().as_str());
|
||||
TemplateNode::Text { text }
|
||||
}
|
||||
false => TemplateNode::Dynamic {
|
||||
id: self.dyn_idx.get(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::PrettyUnparse;
|
||||
|
||||
#[test]
|
||||
fn parses() {
|
||||
let input = syn::parse2::<TextNode>(quote! { "hello world" }).unwrap();
|
||||
assert_eq!(input.input.source.value(), "hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_tokens_with_hr() {
|
||||
let lit = syn::parse2::<TextNode>(quote! { "hi {world1} {world2} {world3}" }).unwrap();
|
||||
println!("{}", lit.to_token_stream().pretty_unparse());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_str() {
|
||||
let input = syn::parse2::<TextNode>(quote! { r#"hello world"# }).unwrap();
|
||||
println!("{}", input.input.source.to_token_stream());
|
||||
assert_eq!(input.input.source.value(), "hello world");
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,29 @@
|
||||
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))
|
||||
#![allow(unused)]
|
||||
|
||||
#[cfg(feature = "hot_reload")]
|
||||
use internment::Intern;
|
||||
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use std::{fmt::Debug, hash::Hash};
|
||||
|
||||
/// interns a object into a static object, resusing the value if it already exists
|
||||
#[cfg(feature = "hot_reload")]
|
||||
pub(crate) fn intern<T: Eq + Hash + Send + Sync + ?Sized + 'static>(
|
||||
s: impl Into<Intern<T>>,
|
||||
) -> &'static T {
|
||||
s.into().as_ref()
|
||||
}
|
||||
|
||||
/// These are just helpful methods for tests to pretty print the token stream - they are not used in the actual code
|
||||
// #[cfg(test)]
|
||||
pub trait PrettyUnparse {
|
||||
fn pretty_unparse(&self) -> String;
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
impl PrettyUnparse for TokenStream2 {
|
||||
fn pretty_unparse(&self) -> String {
|
||||
let parsed = syn::parse2::<syn::Expr>(self.clone()).unwrap();
|
||||
prettier_please::unparse_expr(&parsed)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user