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:
authored and GitHub committed 2024-07-17 19:11:18 -07:00
1 parent 176e67e5b7
commit 88af3e7eff
181 files changed
+12755 -10806

No files matched your search

+503 -403
View File
@@ -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();
}