Switch to a pool of dynamic values for hot reloading (#2705)
* create the dynamic value pool * assign ids to dynamic formatted segments * separate the rendering and literal pools * rsx output compiles again * more examples compiling with new rsx expansion * update template body explanation * all workspace examples compile * fix formatted segments in keys * start hot reload diffing * fix component literal hot reloading * start integrate new hot reloading with the CLI * simple hot reloads working * Fix hot reloading blocks with components * implement hot reloading for if chains * Fix hot reloading after a template requires a full rebuild * Fix hot reloading any attribute values * remove unsafe from hot reload utils * Fix hot reloading empty rsx * add more hot reloading tests * reorganize hot reload module * fix web hydration * fix empty rsx nodes in autoformatting * fix tests * remove path sorting logic from core * make template names more consistent in debug mode * fix quote_as_hot_reload_literal for explicitly typed literals * fix can_be_shorthand for string literals * fix formatted single dynamic expression * Fix usize component properties and playwright tests * remove default implementation for TemplateBody * add a bunch more comments for diffing, scoring and why this scoring system is optimal
This commit is contained in:
1 parent
f3ca1484a1
commit
34bdcd15cf
40 files changed
+2288
-1897
No files matched your search
+112
-144
@@ -1,13 +1,15 @@
|
||||
use proc_macro2::Span;
|
||||
use quote::quote;
|
||||
use quote::ToTokens;
|
||||
use quote::{quote, TokenStreamExt};
|
||||
use std::fmt::Display;
|
||||
use std::ops::Deref;
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
Lit, LitBool, LitFloat, LitInt, LitStr,
|
||||
};
|
||||
|
||||
use crate::{location::DynIdx, IfmtInput, Segment};
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
|
||||
/// A literal value in the rsx! macro
|
||||
///
|
||||
@@ -17,20 +19,14 @@ use crate::{location::DynIdx, IfmtInput, Segment};
|
||||
/// 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 {
|
||||
pub enum HotLiteral {
|
||||
/// 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),
|
||||
Fmted(HotReloadFormattedSegment),
|
||||
|
||||
/// A float literal
|
||||
///
|
||||
@@ -48,15 +44,28 @@ pub enum HotLiteralType {
|
||||
Bool(LitBool),
|
||||
}
|
||||
|
||||
impl HotLiteral {
|
||||
pub fn quote_as_hot_reload_literal(&self) -> TokenStream2 {
|
||||
match &self {
|
||||
HotLiteral::Fmted(f) => quote! { dioxus_core::internal::HotReloadLiteral::Fmted(#f) },
|
||||
HotLiteral::Float(f) => {
|
||||
quote! { dioxus_core::internal::HotReloadLiteral::Float(#f as _) }
|
||||
}
|
||||
HotLiteral::Int(f) => quote! { dioxus_core::internal::HotReloadLiteral::Int(#f as _) },
|
||||
HotLiteral::Bool(f) => quote! { dioxus_core::internal::HotReloadLiteral::Bool(#f) },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)),
|
||||
Lit::Int(a) => HotLiteral::Int(a),
|
||||
Lit::Bool(a) => HotLiteral::Bool(a),
|
||||
Lit::Float(a) => HotLiteral::Float(a),
|
||||
Lit::Str(a) => HotLiteral::Fmted(IfmtInput::new_litstr(a).into()),
|
||||
_ => {
|
||||
return Err(syn::Error::new(
|
||||
raw.span(),
|
||||
@@ -65,125 +74,30 @@ impl Parse for HotLiteral {
|
||||
}
|
||||
};
|
||||
|
||||
Ok(HotLiteral {
|
||||
value,
|
||||
hr_idx: DynIdx::default(),
|
||||
})
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
match &self {
|
||||
HotLiteral::Fmted(f) => {
|
||||
f.formatted_input.to_tokens(out);
|
||||
}
|
||||
|
||||
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, {
|
||||
{
|
||||
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!(), ':', #hr_idx)
|
||||
}
|
||||
})
|
||||
.maybe_with_rt(|s| s #mapped)
|
||||
}
|
||||
|
||||
// just render the literal directly
|
||||
#[cfg(not(debug_assertions))]
|
||||
{ #as_lit #map_lit }
|
||||
}
|
||||
})
|
||||
HotLiteral::Float(f) => f.to_tokens(out),
|
||||
HotLiteral::Int(f) => f.to_tokens(out),
|
||||
HotLiteral::Bool(f) => f.to_tokens(out),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HotLiteralType {
|
||||
fn span(&self) -> Span {
|
||||
impl HotLiteral {
|
||||
pub 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(),
|
||||
HotLiteral::Fmted(f) => f.span(),
|
||||
HotLiteral::Float(f) => f.span(),
|
||||
HotLiteral::Int(f) => f.span(),
|
||||
HotLiteral::Bool(f) => f.span(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,38 +119,92 @@ impl HotLiteral {
|
||||
}
|
||||
|
||||
pub fn is_static(&self) -> bool {
|
||||
match &self.value {
|
||||
HotLiteralType::Fmted(fmt) => fmt.is_static(),
|
||||
match &self {
|
||||
HotLiteral::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(),
|
||||
}
|
||||
HotLiteral::Fmted(HotReloadFormattedSegment::from(IfmtInput {
|
||||
source: LitStr::new(text, Span::call_site()),
|
||||
segments: vec![],
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
match &self {
|
||||
HotLiteral::Fmted(l) => l.to_string_with_quotes().fmt(f),
|
||||
HotLiteral::Float(l) => l.fmt(f),
|
||||
HotLiteral::Int(l) => l.fmt(f),
|
||||
HotLiteral::Bool(l) => l.value().fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A formatted segment that can be hot reloaded
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
||||
pub struct HotReloadFormattedSegment {
|
||||
pub formatted_input: IfmtInput,
|
||||
pub dynamic_node_indexes: Vec<DynIdx>,
|
||||
}
|
||||
|
||||
impl Deref for HotReloadFormattedSegment {
|
||||
type Target = IfmtInput;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.formatted_input
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IfmtInput> for HotReloadFormattedSegment {
|
||||
fn from(input: IfmtInput) -> Self {
|
||||
let mut dynamic_node_indexes = Vec::new();
|
||||
for segment in &input.segments {
|
||||
if let Segment::Formatted { .. } = segment {
|
||||
dynamic_node_indexes.push(DynIdx::default());
|
||||
}
|
||||
}
|
||||
Self {
|
||||
formatted_input: input,
|
||||
dynamic_node_indexes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for HotReloadFormattedSegment {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let ifmt: IfmtInput = input.parse()?;
|
||||
Ok(Self::from(ifmt))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for HotReloadFormattedSegment {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let mut idx = 0_usize;
|
||||
let segments = self.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 = self.dynamic_node_indexes[idx].get();
|
||||
idx += 1;
|
||||
quote! {
|
||||
dioxus_core::internal::FmtSegment::Dynamic { id: #_idx }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// The static segments with idxs for locations
|
||||
tokens.extend(quote! {
|
||||
dioxus_core::internal::FmtedSegments::new( vec![ #(#segments),* ], )
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -256,10 +224,10 @@ mod tests {
|
||||
assert!(syn::parse2::<HotLiteral>(quote! { 'a' }).is_err());
|
||||
|
||||
let lit = syn::parse2::<HotLiteral>(quote! { "hello" }).unwrap();
|
||||
assert!(matches!(lit.value, HotLiteralType::Fmted(_)));
|
||||
assert!(matches!(lit, HotLiteral::Fmted(_)));
|
||||
|
||||
let lit = syn::parse2::<HotLiteral>(quote! { "hello {world}" }).unwrap();
|
||||
assert!(matches!(lit.value, HotLiteralType::Fmted(_)));
|
||||
assert!(matches!(lit, HotLiteral::Fmted(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -279,7 +247,7 @@ mod tests {
|
||||
#[test]
|
||||
fn static_str_becomes_str() {
|
||||
let lit = syn::parse2::<HotLiteral>(quote! { "hello" }).unwrap();
|
||||
let HotLiteralType::Fmted(segments) = &lit.value else {
|
||||
let HotLiteral::Fmted(segments) = &lit else {
|
||||
panic!("expected a formatted string");
|
||||
};
|
||||
assert!(segments.is_static());
|
||||
@@ -290,7 +258,7 @@ mod tests {
|
||||
#[test]
|
||||
fn formatted_prints_as_formatted() {
|
||||
let lit = syn::parse2::<HotLiteral>(quote! { "hello {world}" }).unwrap();
|
||||
let HotLiteralType::Fmted(segments) = &lit.value else {
|
||||
let HotLiteral::Fmted(segments) = &lit else {
|
||||
panic!("expected a formatted string");
|
||||
};
|
||||
assert!(!segments.is_static());
|
||||
|
||||
Reference in new issue
Block a user