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

+323
View File
@@ -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);
}
}
}