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:
Evan Almloff 2024-07-30 19:16:27 +02:00 committed by GitHub
parent f3ca1484a1
commit 34bdcd15cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 2288 additions and 1897 deletions

View file

@ -1,7 +1,7 @@
use dioxus_core::{internal::HotReloadLiteral, Template};
use dioxus_core::internal::{HotReloadTemplateWithLocation, HotReloadedTemplate};
use dioxus_rsx::{
hot_reload::{diff_rsx, ChangedRsx},
CallBody, HotReloadedTemplate, HotReloadingContext,
CallBody, HotReloadingContext,
};
use krates::cm::MetadataCommand;
use krates::Cmd;
@ -18,8 +18,6 @@ pub struct FileMap {
pub errors: Vec<io::Error>,
pub in_workspace: HashMap<PathBuf, Option<PathBuf>>,
pub changed_lits: HashMap<String, HotReloadLiteral>,
}
/// A cached file that has been parsed
@ -27,7 +25,7 @@ pub struct FileMap {
/// We store the templates found in this file
pub struct CachedSynFile {
pub raw: String,
pub templates: HashMap<&'static str, Template>,
pub templates: HashMap<String, HotReloadedTemplate>,
}
impl FileMap {
@ -56,7 +54,6 @@ impl FileMap {
map,
errors,
in_workspace: HashMap::new(),
changed_lits: HashMap::new(),
};
map.load_assets::<Ctx>(crate_dir.as_path());
@ -74,12 +71,23 @@ impl FileMap {
}
}
/// Insert a file into the map and force a full rebuild
fn full_rebuild(&mut self, file_path: PathBuf, src: String) -> HotreloadError {
let cached_file = CachedSynFile {
raw: src.clone(),
templates: HashMap::new(),
};
self.map.insert(file_path, cached_file);
HotreloadError::Notreloadable
}
/// Try to update the rsx in a file
pub fn update_rsx<Ctx: HotReloadingContext>(
&mut self,
file_path: &Path,
crate_dir: &Path,
) -> Result<Vec<HotReloadedTemplate>, HotreloadError> {
) -> Result<Vec<HotReloadTemplateWithLocation>, HotreloadError> {
let src = std::fs::read_to_string(file_path)?;
// 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
@ -116,13 +124,7 @@ impl FileMap {
// 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
None => {
let cached_file = CachedSynFile {
raw: src.clone(),
templates: HashMap::new(),
};
self.map.insert(file_path.to_path_buf(), cached_file);
return Err(HotreloadError::Notreloadable);
return Err(self.full_rebuild(file_path.to_path_buf(), src));
}
};
@ -150,47 +152,48 @@ impl FileMap {
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());
let template_location = template_location(old_start, file);
// Returns a list of templates that are hotreloadable
let hotreload_result = dioxus_rsx::hotreload::HotReloadedTemplate::new::<Ctx>(
&old_call_body,
&new_call_body,
leaked_location,
self.changed_lits.clone(),
let hotreload_result = dioxus_rsx::hot_reload::HotReloadResult::new::<Ctx>(
&old_call_body.body,
&new_call_body.body,
template_location.clone(),
);
// if the template is not hotreloadable, we need to do a full rebuild
let Some(mut results) = hotreload_result else {
return Err(HotreloadError::Notreloadable);
return Err(self.full_rebuild(file_path.to_path_buf(), src));
};
// self.changed_lits
// .extend(std::mem::take(&mut results.changed_lits));
// Be careful to not send the bad templates
results.templates.retain(|template| {
results.templates.retain(|idx, template| {
// dioxus cannot handle empty templates...
if template.roots.is_empty() {
return false;
}
let template_location = format_template_name(&template_location, *idx);
// if the template is the same, don't send it
if old_cached.templates.get(template.name) == Some(template) {
// if the template is the same, don't send its
if old_cached.templates.get(&template_location) == Some(&*template) {
return false;
};
// Update the most recent idea of the template
// This lets us know if the template has changed so we don't need to send it
old_cached.templates.insert(template.name, *template);
old_cached
.templates
.insert(template_location, template.clone());
true
});
out_templates.push(results);
out_templates.extend(results.templates.into_iter().map(|(idx, template)| {
HotReloadTemplateWithLocation {
location: format_template_name(&template_location, idx),
template,
}
}));
}
Ok(out_templates)
@ -228,13 +231,11 @@ pub fn template_location(old_start: proc_macro2::LineColumn, file: &Path) -> Str
.collect::<Vec<_>>()
.join("/");
path
+ ":"
+ line.to_string().as_str()
+ ":"
+ column.to_string().as_str()
// the byte index doesn't matter, but dioxus needs it
+ ":0"
path + ":" + line.to_string().as_str() + ":" + column.to_string().as_str()
}
pub fn format_template_name(name: &str, index: usize) -> String {
format!("{}:{}", name, index)
}
struct FileMapSearchResult {

View file

@ -467,7 +467,7 @@ impl VNode {
dom: &VirtualDom,
to: &mut impl WriteMutations,
) -> usize {
let template = self.template.get();
let template = self.template;
let mount = dom.mounts.get(self.mount.get().0).unwrap();

View file

@ -103,11 +103,11 @@ impl VirtualDom {
to: &mut impl WriteMutations,
mut template: Template,
) {
if self.templates.contains_key(template.name) {
if self.templates.contains(&template.name) {
return;
}
_ = self.templates.insert(template.name, template);
_ = self.templates.insert(template.name);
// If it's all dynamic nodes, then we don't need to register it
if !template.is_completely_dynamic() {
@ -124,7 +124,7 @@ impl VirtualDom {
/// - for appending children we can use AppendChildren
#[allow(dead_code)]
fn is_dyn_node_only_child(node: &VNode, idx: usize) -> bool {
let template = node.template.get();
let template = node.template;
let path = template.node_paths[idx];
// use a loop to index every static node's children until the path has run out

View file

@ -1,5 +1,5 @@
use crate::innerlude::MountId;
use crate::{Attribute, AttributeValue, DynamicNode::*, Template};
use crate::{Attribute, AttributeValue, DynamicNode::*};
use crate::{VNode, VirtualDom, WriteMutations};
use core::iter::Peekable;
@ -21,21 +21,6 @@ impl VNode {
// The node we are diffing from should always be mounted
debug_assert!(dom.mounts.get(self.mount.get().0).is_some() || to.is_none());
// If hot reloading is enabled, we need to make sure we're using the latest template
#[cfg(debug_assertions)]
{
let name = new.template.get().name;
if let Some(template) = dom.templates.get(name).cloned() {
new.template.set(template);
if template != self.template.get() {
let mount_id = self.mount.get();
let parent = dom.mounts[mount_id.0].parent;
self.replace(std::slice::from_ref(new), parent, dom, to);
return;
}
}
}
// If the templates are different by name, we need to replace the entire template
if self.templates_are_different(new) {
return self.light_diff_templates(new, dom, to);
@ -120,7 +105,7 @@ impl VNode {
&self,
root_idx: usize,
) -> Option<(usize, &DynamicNode)> {
self.template.get().roots[root_idx]
self.template.roots[root_idx]
.dynamic_id()
.map(|id| (id, &self.dynamic_nodes[id]))
}
@ -148,7 +133,7 @@ impl VNode {
pub(crate) fn find_last_element(&self, dom: &VirtualDom) -> ElementId {
let mount = &dom.mounts[self.mount.get().0];
let last_root_index = self.template.get().roots.len() - 1;
let last_root_index = self.template.roots.len() - 1;
match self.get_dynamic_root_node_and_id(last_root_index) {
// This node is static, just get the root id
None | Some((_, Placeholder(_) | Text(_))) => mount.root_ids[last_root_index],
@ -272,7 +257,7 @@ impl VNode {
destroy_component_state: bool,
replace_with: Option<usize>,
) {
let roots = self.template.get().roots;
let roots = self.template.roots;
for (idx, node) in roots.iter().enumerate() {
let last_node = idx == roots.len() - 1;
if let Some(id) = node.dynamic_id() {
@ -305,7 +290,7 @@ impl VNode {
dom: &mut VirtualDom,
destroy_component_state: bool,
) {
let template = self.template.get();
let template = self.template;
for (idx, dyn_node) in self.dynamic_nodes.iter().enumerate() {
let path_len = template.node_paths.get(idx).map(|path| path.len());
// Roots are cleaned up automatically above and nodes with a empty path are placeholders
@ -361,14 +346,14 @@ impl VNode {
}
fn templates_are_different(&self, other: &VNode) -> bool {
let self_node_name = self.template.get().id();
let other_node_name = other.template.get().id();
let self_node_name = self.template.id();
let other_node_name = other.template.id();
self_node_name != other_node_name
}
pub(super) fn reclaim_attributes(&self, mount: MountId, dom: &mut VirtualDom) {
let mut next_id = None;
for (idx, path) in self.template.get().attr_paths.iter().enumerate() {
for (idx, path) in self.template.attr_paths.iter().enumerate() {
// We clean up the roots in the next step, so don't worry about them here
if path.len() <= 1 {
continue;
@ -399,7 +384,7 @@ impl VNode {
let mut old_attributes_iter = old_attrs.iter().peekable();
let mut new_attributes_iter = new_attrs.iter().peekable();
let attribute_id = dom.mounts[mount_id.0].mounted_attributes[idx];
let path = self.template.get().attr_paths[idx];
let path = self.template.attr_paths[idx];
loop {
match (old_attributes_iter.peek(), new_attributes_iter.peek()) {
@ -566,21 +551,6 @@ impl VNode {
}
}
/// Get the most up to date template for this rsx block
#[allow(unused)]
pub(crate) fn template(&self, dom: &VirtualDom) -> Template {
// check for a overridden template
#[cfg(debug_assertions)]
{
let template = self.template.get();
if let Some(new_template) = dom.templates.get(template.name) {
self.template.set(*new_template);
}
};
self.template.get()
}
/// Create this rsx block. This will create scopes from components that this rsx block contains, but it will not write anything to the DOM.
pub(crate) fn create(
&self,
@ -589,7 +559,7 @@ impl VNode {
mut to: Option<&mut impl WriteMutations>,
) -> usize {
// Get the most up to date template
let template = self.template(dom);
let template = self.template;
// Initialize the mount information for this vnode if it isn't already mounted
if !self.mount.get().mounted() {
@ -616,13 +586,9 @@ impl VNode {
}
// Walk the roots, creating nodes and assigning IDs
// nodes in an iterator of (dynamic_node_index, path)
let nodes_sorted = template.breadth_first_node_paths();
let attrs_sorted = template.breadth_first_attribute_paths();
let mut nodes = nodes_sorted.peekable();
let mut attrs = attrs_sorted.peekable();
// nodes in an iterator of (dynamic_node_index, path) and attrs in an iterator of (attr_index, path)
let mut nodes = template.node_paths.iter().copied().enumerate().peekable();
let mut attrs = template.attr_paths.iter().copied().enumerate().peekable();
// Get the mounted id of this block
// At this point, we should have already mounted the block
@ -690,7 +656,7 @@ impl VNode {
fn reference_to_dynamic_node(&self, mount: MountId, dynamic_node_id: usize) -> ElementRef {
ElementRef {
path: ElementPath {
path: self.template.get().node_paths[dynamic_node_id],
path: self.template.node_paths[dynamic_node_id],
},
mount,
}
@ -834,7 +800,7 @@ impl VNode {
let this_id = dom.next_element();
dom.mounts[mount.0].root_ids[root_idx] = this_id;
to.load_template(self.template.get().name, root_idx, this_id);
to.load_template(self.template.name, root_idx, this_id);
this_id
}
@ -874,7 +840,7 @@ impl VNode {
dom: &mut VirtualDom,
) -> (ElementId, &'static [u8]) {
// Add the mutation to the list
let path = self.template.get().node_paths[idx];
let path = self.template.node_paths[idx];
// Allocate a dynamic element reference for this text node
let new_id = mount.mount_node(idx, dom);
@ -940,8 +906,8 @@ fn matching_components<'a>(
left: &'a VNode,
right: &'a VNode,
) -> Option<Vec<(&'a VComponent, &'a VComponent)>> {
let left_node = left.template.get();
let right_node = right.template.get();
let left_node = left.template;
let right_node = right.template;
if left_node.roots.len() != right_node.roots.len() {
return None;
}

View file

@ -1,15 +1,26 @@
#[doc(hidden)]
use std::{
any::{Any, TypeId},
hash::{Hash, Hasher},
};
#[cfg(feature = "serialize")]
use crate::nodes::deserialize_string_leaky;
use crate::{
Attribute, AttributeValue, DynamicNode, Template, TemplateAttribute, TemplateNode, VNode, VText,
};
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
#[doc(hidden)]
#[derive(Debug, PartialEq, Clone)]
pub struct HotreloadedLiteral {
pub name: String,
pub value: HotReloadLiteral,
}
#[doc(hidden)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
#[doc(hidden)]
#[derive(Debug, PartialEq, Clone)]
pub enum HotReloadLiteral {
Fmted(FmtedSegments),
@ -18,12 +29,53 @@ pub enum HotReloadLiteral {
Bool(bool),
}
#[doc(hidden)]
impl HotReloadLiteral {
pub fn as_fmted(&self) -> Option<&FmtedSegments> {
match self {
Self::Fmted(segments) => Some(segments),
_ => None,
}
}
pub fn as_float(&self) -> Option<f64> {
match self {
Self::Float(f) => Some(*f),
_ => None,
}
}
pub fn as_int(&self) -> Option<i64> {
match self {
Self::Int(i) => Some(*i),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
Self::Bool(b) => Some(*b),
_ => None,
}
}
}
impl Hash for HotReloadLiteral {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
Self::Fmted(segments) => segments.hash(state),
Self::Float(f) => f.to_bits().hash(state),
Self::Int(i) => i.hash(state),
Self::Bool(b) => b.hash(state),
}
}
}
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
#[derive(Debug, PartialEq, Eq, Clone)]
#[doc(hidden)]
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub struct FmtedSegments {
pub segments: Vec<FmtSegment>,
pub(crate) segments: Vec<FmtSegment>,
}
impl FmtedSegments {
@ -32,33 +84,23 @@ impl FmtedSegments {
}
/// Render the formatted string by stitching together the segments
pub fn render_with(&self, dynamic_nodes: Vec<String>) -> String {
pub(crate) fn render_with(&self, dynamic_text: &[String]) -> String {
let mut out = String::new();
for segment in &self.segments {
match segment {
FmtSegment::Literal { value } => out.push_str(value),
FmtSegment::Dynamic { id } => out.push_str(&dynamic_nodes[*id]),
FmtSegment::Dynamic { id } => out.push_str(&dynamic_text[*id]),
}
}
out
}
/// Update the segments with new segments
///
/// this will change how we render the formatted string
pub fn update_segments(&mut self, new_segments: Vec<FmtSegment>) {
self.segments = new_segments;
}
}
#[cfg(feature = "serialize")]
use crate::nodes::deserialize_string_leaky;
#[doc(hidden)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, PartialEq, Eq, Clone)]
#[doc(hidden)]
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub enum FmtSegment {
Literal {
#[cfg_attr(
@ -71,3 +113,359 @@ pub enum FmtSegment {
id: usize,
},
}
// let __pool = DynamicValuePool::new(
// vec![...],
// vec![...],
// vec![...],
// );
// VNode::new(
// None,
// Template {
// name: "...",
// roots: &[...],
// node_paths: &[..],
// attr_paths: &[...],
// },
// Box::new([...]),
// Box::new([...]),
// )
// Open questions:
// - How do we handle type coercion for different sized component property integers?
// - Should non-string hot literals go through the centralized pool?
// - Should formatted strings be a runtime concept?
#[doc(hidden)]
pub struct DynamicLiteralPool {
dynamic_text: Box<[String]>,
}
impl DynamicLiteralPool {
pub fn new(dynamic_text: Vec<String>) -> Self {
Self {
dynamic_text: dynamic_text.into_boxed_slice(),
}
}
pub fn get_component_property<'a, T>(
&self,
id: usize,
hot_reload: &'a HotReloadedTemplate,
f: impl FnOnce(&'a HotReloadLiteral) -> Option<T>,
) -> Option<T> {
f(hot_reload.component_values.get(id)?)
}
/// Get a component property of a specific type at the component property index
pub fn component_property<T: 'static>(
&mut self,
id: usize,
hot_reload: &HotReloadedTemplate,
// We pass in the original value for better type inference
// For example, if the original literal is `0i128`, we know the output must be the type `i128`
_coherse_type: T,
) -> T {
fn assert_type<T: 'static, T2: 'static>(t: T) -> T2 {
*(Box::new(t) as Box<dyn Any>).downcast::<T2>().unwrap()
}
let grab_float = || {
self.get_component_property(id, hot_reload, HotReloadLiteral::as_float).expect("Expected a float component property. This is probably caused by a bug in dioxus hot reloading. Please report this issue.")
};
let grab_int = || {
self.get_component_property(id, hot_reload, HotReloadLiteral::as_int).expect("Expected an int component property. This is probably caused by a bug in dioxus hot reloading. Please report this issue.")
};
let grab_bool = || {
self.get_component_property(id, hot_reload, HotReloadLiteral::as_bool).expect("Expected a bool component property. This is probably caused by a bug in dioxus hot reloading. Please report this issue.")
};
let grab_fmted = || {
self.get_component_property(id, hot_reload, |fmted| HotReloadLiteral::as_fmted(fmted).map(|segments| self.render_formatted(segments))).expect("Expected a string component property. This is probably caused by a bug in dioxus hot reloading. Please report this issue.")
};
match TypeId::of::<T>() {
// Any string types that accept a literal
_ if TypeId::of::<String>() == TypeId::of::<T>() => assert_type(grab_fmted()),
_ if TypeId::of::<&'static str>() == TypeId::of::<T>() => {
assert_type(Box::leak(grab_fmted().into_boxed_str()) as &'static str)
}
// Any integer types that accept a literal
_ if TypeId::of::<i128>() == TypeId::of::<T>() => assert_type(grab_int() as i128),
_ if TypeId::of::<i64>() == TypeId::of::<T>() => assert_type(grab_int()),
_ if TypeId::of::<i32>() == TypeId::of::<T>() => assert_type(grab_int() as i32),
_ if TypeId::of::<i16>() == TypeId::of::<T>() => assert_type(grab_int() as i16),
_ if TypeId::of::<i8>() == TypeId::of::<T>() => assert_type(grab_int() as i8),
_ if TypeId::of::<isize>() == TypeId::of::<T>() => assert_type(grab_int() as isize),
_ if TypeId::of::<u128>() == TypeId::of::<T>() => assert_type(grab_int() as u128),
_ if TypeId::of::<u64>() == TypeId::of::<T>() => assert_type(grab_int() as u64),
_ if TypeId::of::<u32>() == TypeId::of::<T>() => assert_type(grab_int() as u32),
_ if TypeId::of::<u16>() == TypeId::of::<T>() => assert_type(grab_int() as u16),
_ if TypeId::of::<u8>() == TypeId::of::<T>() => assert_type(grab_int() as u8),
_ if TypeId::of::<usize>() == TypeId::of::<T>() => assert_type(grab_int() as usize),
// Any float types that accept a literal
_ if TypeId::of::<f64>() == TypeId::of::<T>() => assert_type(grab_float()),
_ if TypeId::of::<f32>() == TypeId::of::<T>() => assert_type(grab_float() as f32),
// Any bool types that accept a literal
_ if TypeId::of::<bool>() == TypeId::of::<T>() => assert_type(grab_bool()),
_ => panic!("Unsupported component property type"),
}
}
pub fn render_formatted(&self, segments: &FmtedSegments) -> String {
segments.render_with(&self.dynamic_text)
}
}
#[doc(hidden)]
pub struct DynamicValuePool {
dynamic_attributes: Box<[Box<[Attribute]>]>,
dynamic_nodes: Box<[DynamicNode]>,
literal_pool: DynamicLiteralPool,
}
impl DynamicValuePool {
pub fn new(
dynamic_nodes: Vec<DynamicNode>,
dynamic_attributes: Vec<Box<[Attribute]>>,
literal_pool: DynamicLiteralPool,
) -> Self {
Self {
dynamic_attributes: dynamic_attributes.into_boxed_slice(),
dynamic_nodes: dynamic_nodes.into_boxed_slice(),
literal_pool,
}
}
pub fn render_with(&mut self, hot_reload: &HotReloadedTemplate) -> VNode {
// Get the node_paths from a depth first traversal of the template
let node_paths = hot_reload.node_paths();
let attr_paths = hot_reload.attr_paths();
let template = Template {
name: hot_reload.name,
roots: hot_reload.roots,
node_paths,
attr_paths,
};
let key = hot_reload
.key
.as_ref()
.map(|key| self.literal_pool.render_formatted(key));
let dynamic_nodes = hot_reload
.dynamic_nodes
.iter()
.map(|node| self.render_dynamic_node(node))
.collect();
let dynamic_attrs = hot_reload
.dynamic_attributes
.iter()
.map(|attr| self.render_attribute(attr))
.collect();
VNode::new(key, template, dynamic_nodes, dynamic_attrs)
}
fn render_dynamic_node(&mut self, node: &HotReloadDynamicNode) -> DynamicNode {
match node {
// If the node is dynamic, take it from the pool and return it
HotReloadDynamicNode::Dynamic(id) => self.dynamic_nodes[*id].clone(),
// Otherwise, format the text node and return it
HotReloadDynamicNode::Formatted(segments) => DynamicNode::Text(VText {
value: self.literal_pool.render_formatted(segments),
}),
}
}
fn render_attribute(&mut self, attr: &HotReloadDynamicAttribute) -> Box<[Attribute]> {
match attr {
HotReloadDynamicAttribute::Dynamic(id) => self.dynamic_attributes[*id].clone(),
HotReloadDynamicAttribute::Named(NamedAttribute {
name,
namespace,
value,
}) => Box::new([Attribute {
name,
namespace: *namespace,
value: match value {
HotReloadAttributeValue::Literal(HotReloadLiteral::Fmted(segments)) => {
AttributeValue::Text(self.literal_pool.render_formatted(segments))
}
HotReloadAttributeValue::Literal(HotReloadLiteral::Float(f)) => {
AttributeValue::Float(*f)
}
HotReloadAttributeValue::Literal(HotReloadLiteral::Int(i)) => {
AttributeValue::Int(*i)
}
HotReloadAttributeValue::Literal(HotReloadLiteral::Bool(b)) => {
AttributeValue::Bool(*b)
}
HotReloadAttributeValue::Dynamic(id) => {
self.dynamic_attributes[*id][0].value.clone()
}
},
volatile: false,
}]),
}
}
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
pub struct HotReloadTemplateWithLocation {
pub location: String,
pub template: HotReloadedTemplate,
}
#[doc(hidden)]
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct HotReloadedTemplate {
pub name: &'static str,
pub key: Option<FmtedSegments>,
pub dynamic_nodes: Vec<HotReloadDynamicNode>,
pub dynamic_attributes: Vec<HotReloadDynamicAttribute>,
pub component_values: Vec<HotReloadLiteral>,
#[cfg_attr(
feature = "serialize",
serde(deserialize_with = "crate::nodes::deserialize_leaky")
)]
pub roots: &'static [TemplateNode],
}
impl HotReloadedTemplate {
pub fn new(
name: &'static str,
key: Option<FmtedSegments>,
dynamic_nodes: Vec<HotReloadDynamicNode>,
dynamic_attributes: Vec<HotReloadDynamicAttribute>,
component_values: Vec<HotReloadLiteral>,
roots: &'static [TemplateNode],
) -> Self {
Self {
name,
key,
dynamic_nodes,
dynamic_attributes,
component_values,
roots,
}
}
fn node_paths(&self) -> &'static [&'static [u8]] {
fn add_node_paths(
roots: &[TemplateNode],
node_paths: &mut Vec<&'static [u8]>,
current_path: Vec<u8>,
) {
for (idx, node) in roots.iter().enumerate() {
let mut path = current_path.clone();
path.push(idx as u8);
match node {
TemplateNode::Element { children, .. } => {
add_node_paths(children, node_paths, path);
}
TemplateNode::Text { .. } => {}
TemplateNode::Dynamic { id } => {
debug_assert_eq!(node_paths.len(), *id);
node_paths.push(Box::leak(path.into_boxed_slice()));
}
}
}
}
let mut node_paths = Vec::new();
add_node_paths(self.roots, &mut node_paths, Vec::new());
let leaked: &'static [&'static [u8]] = Box::leak(node_paths.into_boxed_slice());
leaked
}
fn attr_paths(&self) -> &'static [&'static [u8]] {
fn add_attr_paths(
roots: &[TemplateNode],
attr_paths: &mut Vec<&'static [u8]>,
current_path: Vec<u8>,
) {
for (idx, node) in roots.iter().enumerate() {
let mut path = current_path.clone();
path.push(idx as u8);
if let TemplateNode::Element {
children, attrs, ..
} = node
{
for attr in *attrs {
if let TemplateAttribute::Dynamic { id } = attr {
debug_assert_eq!(attr_paths.len(), *id);
attr_paths.push(Box::leak(path.clone().into_boxed_slice()));
}
}
add_attr_paths(children, attr_paths, path);
}
}
}
let mut attr_paths = Vec::new();
add_attr_paths(self.roots, &mut attr_paths, Vec::new());
let leaked: &'static [&'static [u8]] = Box::leak(attr_paths.into_boxed_slice());
leaked
}
}
#[doc(hidden)]
#[derive(Debug, PartialEq, Clone, Hash)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
pub enum HotReloadDynamicNode {
Dynamic(usize),
Formatted(FmtedSegments),
}
#[doc(hidden)]
#[derive(Debug, PartialEq, Clone, Hash)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
pub enum HotReloadDynamicAttribute {
Dynamic(usize),
Named(NamedAttribute),
}
#[doc(hidden)]
#[derive(Debug, PartialEq, Clone, Hash)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct NamedAttribute {
/// The name of this attribute.
#[cfg_attr(
feature = "serialize",
serde(deserialize_with = "crate::nodes::deserialize_string_leaky")
)]
name: &'static str,
/// The namespace of this attribute. Does not exist in the HTML spec
#[cfg_attr(
feature = "serialize",
serde(deserialize_with = "crate::nodes::deserialize_option_leaky")
)]
namespace: Option<&'static str>,
value: HotReloadAttributeValue,
}
impl NamedAttribute {
pub fn new(
name: &'static str,
namespace: Option<&'static str>,
value: HotReloadAttributeValue,
) -> Self {
Self {
name,
namespace,
value,
}
}
}
#[doc(hidden)]
#[derive(Debug, PartialEq, Clone, Hash)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
pub enum HotReloadAttributeValue {
Literal(HotReloadLiteral),
Dynamic(usize),
}

View file

@ -34,8 +34,11 @@ mod hotreload_utils;
pub mod internal {
pub use crate::properties::verify_component_called_as_component;
#[doc(hidden)]
pub use crate::hotreload_utils::{
FmtSegment, FmtedSegments, HotReloadLiteral, HotreloadedLiteral,
DynamicLiteralPool, DynamicValuePool, FmtSegment, FmtedSegments, HotReloadAttributeValue,
HotReloadDynamicAttribute, HotReloadDynamicNode, HotReloadLiteral,
HotReloadTemplateWithLocation, HotReloadedTemplate, HotreloadedLiteral, NamedAttribute,
};
}

View file

@ -6,7 +6,6 @@ use crate::{
properties::ComponentFunction,
};
use crate::{Properties, ScopeId, VirtualDom};
use core::panic;
use std::ops::{Deref, DerefMut};
use std::rc::Rc;
use std::vec;
@ -122,7 +121,7 @@ pub struct VNodeInner {
pub key: Option<String>,
/// The static nodes and static descriptor of the template
pub template: Cell<Template>,
pub template: Template,
/// The dynamic nodes in the template
pub dynamic_nodes: Box<[DynamicNode]>,
@ -252,12 +251,12 @@ impl VNode {
key: None,
dynamic_nodes: Box::new([DynamicNode::Placeholder(Default::default())]),
dynamic_attrs: Box::new([]),
template: Cell::new(Template {
template: Template {
name: "packages/core/nodes.rs:198:0:0",
roots: &[TemplateNode::Dynamic { id: 0 }],
node_paths: &[&[0]],
attr_paths: &[],
}),
},
})
})
.clone()
@ -278,7 +277,7 @@ impl VNode {
Self {
vnode: Rc::new(VNodeInner {
key,
template: Cell::new(template),
template,
dynamic_nodes,
dynamic_attrs,
}),
@ -290,7 +289,7 @@ impl VNode {
///
/// Returns [`None`] if the root is actually a static node (Element/Text)
pub fn dynamic_root(&self, idx: usize) -> Option<&DynamicNode> {
self.template.get().roots[idx]
self.template.roots[idx]
.dynamic_id()
.map(|id| &self.dynamic_nodes[id])
}
@ -411,7 +410,7 @@ where
}
#[cfg(feature = "serialize")]
fn deserialize_leaky<'a, 'de, T, D>(deserializer: D) -> Result<&'a [T], D::Error>
pub(crate) fn deserialize_leaky<'a, 'de, T, D>(deserializer: D) -> Result<&'a [T], D::Error>
where
T: serde::Deserialize<'de>,
D: serde::Deserializer<'de>,
@ -423,7 +422,9 @@ where
}
#[cfg(feature = "serialize")]
fn deserialize_option_leaky<'a, 'de, D>(deserializer: D) -> Result<Option<&'static str>, D::Error>
pub(crate) fn deserialize_option_leaky<'a, 'de, D>(
deserializer: D,
) -> Result<Option<&'static str>, D::Error>
where
D: serde::Deserializer<'de>,
{
@ -448,36 +449,6 @@ impl Template {
let ptr: *const str = self.name;
ptr as *const () as usize
}
/// Iterate over the attribute paths in order along with the original indexes for each path
pub(crate) fn breadth_first_attribute_paths(
&self,
) -> impl Iterator<Item = (usize, &'static [u8])> {
// In release mode, hot reloading is disabled and everything is in breadth first order already
#[cfg(not(debug_assertions))]
{
self.attr_paths.iter().copied().enumerate()
}
// If we are in debug mode, hot reloading may have messed up the order of the paths. We need to sort them
#[cfg(debug_assertions)]
{
sort_bfo(self.attr_paths).into_iter()
}
}
/// Iterate over the node paths in order along with the original indexes for each path
pub(crate) fn breadth_first_node_paths(&self) -> impl Iterator<Item = (usize, &'static [u8])> {
// In release mode, hot reloading is disabled and everything is in breadth first order already
#[cfg(not(debug_assertions))]
{
self.node_paths.iter().copied().enumerate()
}
// If we are in debug mode, hot reloading may have messed up the order of the paths. We need to sort them
#[cfg(debug_assertions)]
{
sort_bfo(self.node_paths).into_iter()
}
}
}
/// A statically known node in a layout.
@ -523,7 +494,10 @@ pub enum TemplateNode {
/// This template node is just a piece of static text
Text {
/// The actual text
#[serde(deserialize_with = "deserialize_string_leaky")]
#[cfg_attr(
feature = "serialize",
serde(deserialize_with = "deserialize_string_leaky")
)]
text: &'static str,
},
@ -548,7 +522,7 @@ impl TemplateNode {
/// A node created at runtime
///
/// This node's index in the DynamicNode list on VNode should match its respective `Dynamic` index
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum DynamicNode {
/// A component node
///
@ -692,8 +666,10 @@ pub struct VText {
impl VText {
/// Create a new VText
pub fn new(value: String) -> Self {
Self { value }
pub fn new(value: impl ToString) -> Self {
Self {
value: value.to_string(),
}
}
}
@ -783,6 +759,7 @@ impl Attribute {
///
/// These are built-in to be faster during the diffing process. To use a custom value, use the [`AttributeValue::Any`]
/// variant.
#[derive(Clone)]
pub enum AttributeValue {
/// Text attribute
Text(String),
@ -800,7 +777,7 @@ pub enum AttributeValue {
Listener(ListenerCb),
/// An arbitrary value that implements PartialEq and is static
Any(Box<dyn AnyValue>),
Any(Rc<dyn AnyValue>),
/// A "none" value, resulting in the removal of an attribute from the dom
None,
@ -824,7 +801,7 @@ impl AttributeValue {
/// Create a new [`AttributeValue`] with a value that implements [`AnyValue`]
pub fn any_value<T: AnyValue>(value: T) -> AttributeValue {
AttributeValue::Any(Box::new(value))
AttributeValue::Any(Rc::new(value))
}
}
@ -859,19 +836,6 @@ impl PartialEq for AttributeValue {
}
}
impl Clone for AttributeValue {
fn clone(&self) -> Self {
match self {
Self::Text(arg0) => Self::Text(arg0.clone()),
Self::Float(arg0) => Self::Float(*arg0),
Self::Int(arg0) => Self::Int(*arg0),
Self::Bool(arg0) => Self::Bool(*arg0),
Self::Listener(_) | Self::Any(_) => panic!("Cannot clone listener or any value"),
Self::None => Self::None,
}
}
}
#[doc(hidden)]
pub trait AnyValue: 'static {
fn any_cmp(&self, other: &dyn AnyValue) -> bool;
@ -1117,7 +1081,7 @@ impl IntoAttributeValue for Arguments<'_> {
}
}
impl IntoAttributeValue for Box<dyn AnyValue> {
impl IntoAttributeValue for Rc<dyn AnyValue> {
fn into_value(self) -> AttributeValue {
AttributeValue::Any(self)
}
@ -1143,54 +1107,3 @@ pub trait HasAttributes {
volatile: bool,
) -> Self;
}
#[cfg(debug_assertions)]
pub(crate) fn sort_bfo(paths: &[&'static [u8]]) -> Vec<(usize, &'static [u8])> {
let mut with_indices = paths.iter().copied().enumerate().collect::<Vec<_>>();
with_indices.sort_by(|(_, a), (_, b)| {
let mut a = a.iter();
let mut b = b.iter();
loop {
match (a.next(), b.next()) {
(Some(a), Some(b)) => {
if a != b {
return a.cmp(b);
}
}
// The shorter path goes first
(None, Some(_)) => return std::cmp::Ordering::Less,
(Some(_), None) => return std::cmp::Ordering::Greater,
(None, None) => return std::cmp::Ordering::Equal,
}
}
});
with_indices
}
#[test]
#[cfg(debug_assertions)]
fn sorting() {
let r: [(usize, &[u8]); 5] = [
(0, &[0, 1]),
(1, &[0, 2]),
(2, &[1, 0]),
(3, &[1, 0, 1]),
(4, &[1, 2]),
];
assert_eq!(
sort_bfo(&[&[0, 1,], &[0, 2,], &[1, 0,], &[1, 0, 1,], &[1, 2,],]),
r
);
let r: [(usize, &[u8]); 6] = [
(0, &[0]),
(1, &[0, 1]),
(2, &[0, 1, 2]),
(3, &[1]),
(4, &[1, 2]),
(5, &[2]),
];
assert_eq!(
sort_bfo(&[&[0], &[0, 1], &[0, 1, 2], &[1], &[1, 2], &[2],]),
r
);
}

View file

@ -18,7 +18,7 @@ use crate::{
};
use crate::{Task, VComponent};
use futures_util::StreamExt;
use rustc_hash::FxHashMap;
use rustc_hash::FxHashSet;
use slab::Slab;
use std::collections::BTreeSet;
use std::{any::Any, rc::Rc};
@ -208,8 +208,8 @@ pub struct VirtualDom {
pub(crate) dirty_scopes: BTreeSet<ScopeOrder>,
// A map of overridden templates?
pub(crate) templates: FxHashMap<TemplateId, Template>,
// A map of templates we have sent to the renderer
pub(crate) templates: FxHashSet<TemplateId>,
// Templates changes that are queued for the next render
pub(crate) queued_templates: Vec<Template>,
@ -572,60 +572,6 @@ impl VirtualDom {
}
}
/// Replace a template at runtime. This will re-render all components that use this template.
/// This is the primitive that enables hot-reloading.
///
/// The caller must ensure that the template references the same dynamic attributes and nodes as the original template.
///
/// This will only replace the parent template, not any nested templates.
#[instrument(skip(self), level = "trace", name = "VirtualDom::replace_template")]
pub fn replace_template(&mut self, template: Template) {
// we only replace templates if hot reloading is enabled
#[cfg(debug_assertions)]
{
// Save the template ID
self.templates.insert(template.name, template);
// Only queue the template to be written if its not completely dynamic
if !template.is_completely_dynamic() {
self.queued_templates.push(template);
}
// iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
let mut dirty = Vec::new();
for (id, scope) in self.scopes.iter() {
// Recurse into the dynamic nodes of the existing mounted node to see if the template is alive in the tree
fn check_node_for_templates(node: &crate::VNode, template: Template) -> bool {
if node.template.get().name == template.name {
return true;
}
for dynamic in node.dynamic_nodes.iter() {
if let crate::DynamicNode::Fragment(nodes) = dynamic {
for node in nodes {
if check_node_for_templates(node, template) {
return true;
}
}
}
}
false
}
if let Some(sync) = scope.try_root_node() {
if check_node_for_templates(sync, template) {
dirty.push(ScopeId(id));
}
}
}
for dirty in dirty {
self.mark_dirty(dirty);
}
}
}
/// Rebuild the virtualdom without handling any of the mutations
///
/// This is useful for testing purposes and in cases where you render the output of the virtualdom without
@ -885,11 +831,11 @@ impl VirtualDom {
return;
};
let el_ref = &mount.node;
let node_template = el_ref.template.get();
let node_template = el_ref.template;
let target_path = path.path;
// Accumulate listeners into the listener list bottom to top
for (idx, this_path) in node_template.breadth_first_attribute_paths() {
for (idx, this_path) in node_template.attr_paths.iter().enumerate() {
let attrs = &*el_ref.dynamic_attrs[idx];
for attr in attrs.iter() {
@ -943,10 +889,10 @@ impl VirtualDom {
return;
};
let el_ref = &mount.node;
let node_template = el_ref.template.get();
let node_template = el_ref.template;
let target_path = node.path;
for (idx, this_path) in node_template.breadth_first_attribute_paths() {
for (idx, this_path) in node_template.attr_paths.iter().enumerate() {
let attrs = &*el_ref.dynamic_attrs[idx];
for attr in attrs.iter() {

View file

@ -15,7 +15,7 @@ fn attributes_pass_properly() {
let o = h.unwrap();
let template = &o.template.get();
let template = &o.template;
assert_eq!(template.attr_paths.len(), 3);

View file

@ -1,38 +0,0 @@
use dioxus::prelude::*;
use dioxus_core::ElementId;
use dioxus_core::Mutation::{AppendChildren, LoadTemplate};
/// Swap out the template and get it back via the mutation
#[test]
fn hotreloads_template() {
let old_rsx = rsx! { "A" };
let name = old_rsx.as_ref().unwrap().template.get().name;
let mut dom = VirtualDom::new_with_props(move |_| old_rsx.clone(), ());
let new_template = Template {
name,
roots: &[TemplateNode::Text { text: "B" }],
node_paths: &[],
attr_paths: &[],
};
dom.replace_template(new_template);
let muts = dom.rebuild_to_vec();
// New template comes out
assert_eq!(muts.templates.len(), 1);
assert_eq!(
muts.edits,
[
LoadTemplate {
name: "packages/core/tests/hotreload.rs:8:19:0",
index: 0,
id: ElementId(1,),
},
AppendChildren { id: ElementId(0,), m: 1 },
]
)
}

View file

@ -1,43 +1,20 @@
use crate::HotReloadMsg;
use dioxus_core::{internal::HotReloadLiteral, ScopeId, VirtualDom};
use dioxus_core::{ScopeId, VirtualDom};
use dioxus_signals::Writable;
/// Applies template and literal changes to the VirtualDom
///
/// Assets need to be handled by the renderer.
pub fn apply_changes(dom: &mut VirtualDom, msg: &HotReloadMsg) {
for templates in &msg.templates {
for template in &templates.templates {
dom.replace_template(*template);
}
dom.runtime().on_scope(ScopeId::ROOT, || {
let ctx = dioxus_signals::get_global_context();
dom.runtime().on_scope(ScopeId::ROOT, || {
let ctx = dioxus_signals::get_global_context();
for (id, literal) in templates.changed_lits.iter() {
match &literal {
HotReloadLiteral::Fmted(f) => {
if let Some(mut signal) = ctx.get_signal_with_key(id) {
signal.set(f.clone());
}
}
HotReloadLiteral::Float(f) => {
if let Some(mut signal) = ctx.get_signal_with_key::<f64>(id) {
signal.set(*f);
}
}
HotReloadLiteral::Int(f) => {
if let Some(mut signal) = ctx.get_signal_with_key::<i64>(id) {
signal.set(*f);
}
}
HotReloadLiteral::Bool(f) => {
if let Some(mut signal) = ctx.get_signal_with_key::<bool>(id) {
signal.set(*f);
}
}
}
for template in &msg.templates {
let id = &template.location;
let value = template.template.clone();
if let Some(mut signal) = ctx.get_signal_with_key(id) {
signal.set(value);
}
});
}
}
});
}

View file

@ -1,4 +1,4 @@
use dioxus_rsx::HotReloadedTemplate;
use dioxus_core::internal::HotReloadTemplateWithLocation;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
@ -49,7 +49,7 @@ pub enum ClientMsg {
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(bound(deserialize = "'de: 'static"))]
pub struct HotReloadMsg {
pub templates: Vec<HotReloadedTemplate>,
pub templates: Vec<HotReloadTemplateWithLocation>,
pub assets: Vec<PathBuf>,
/// A file changed that's not an asset or a rust file - best of luck!

View file

@ -30,7 +30,7 @@ fn extract_single_text_node(children: &Element, component: &str) -> Option<Strin
// The title's children must be in one of two forms:
// 1. rsx! { "static text" }
// 2. rsx! { "title: {dynamic_text}" }
match vnode.template.get() {
match vnode.template {
// rsx! { "static text" }
Template {
roots: &[TemplateNode::Text { text }],
@ -91,7 +91,7 @@ pub struct TitleProps {
pub fn Title(props: TitleProps) -> Element {
let children = props.children;
let Some(text) = extract_single_text_node(&children, "Title") else {
return rsx! {};
return VNode::empty();
};
// Update the title as it changes. NOTE: We don't use use_effect here because we need this to run on the server
@ -109,7 +109,7 @@ pub fn Title(props: TitleProps) -> Element {
*last_text = text;
}
rsx! {}
VNode::empty()
}
/// Props for the [`Meta`] component
@ -176,7 +176,7 @@ pub fn Meta(props: MetaProps) -> Element {
document.create_meta(props);
});
rsx! {}
VNode::empty()
}
#[derive(Clone, Props, PartialEq)]
@ -271,7 +271,7 @@ pub fn Script(props: ScriptProps) -> Element {
document.create_script(props);
});
rsx! {}
VNode::empty()
}
#[derive(Clone, Props, PartialEq)]
@ -349,7 +349,7 @@ pub fn Style(props: StyleProps) -> Element {
document.create_style(props);
});
rsx! {}
VNode::empty()
}
use super::*;
@ -462,7 +462,7 @@ pub fn Link(props: LinkProps) -> Element {
document.create_link(props);
});
rsx! {}
VNode::empty()
}
fn get_or_insert_root_context<T: Default + Clone + 'static>() -> T {

View file

@ -131,6 +131,7 @@ pub fn collect_svgs(children: &mut [BodyNode], out: &mut Vec<BodyNode>) {
children: TemplateBody::new(vec![]),
brace: Default::default(),
dyn_idx: Default::default(),
component_literal_dyn_idx: vec![],
});
std::mem::swap(child, &mut new_comp);

View file

@ -0,0 +1,138 @@
use crate::attribute::Attribute;
use crate::{
AttributeValue, BodyNode, HotLiteral, HotReloadFormattedSegment, Segment, TemplateBody,
};
/// A visitor that assigns dynamic ids to nodes and attributes and accumulates paths to dynamic nodes and attributes
struct DynIdVisitor<'a> {
body: &'a mut TemplateBody,
current_path: Vec<u8>,
dynamic_text_index: usize,
component_literal_index: usize,
}
impl<'a> DynIdVisitor<'a> {
fn new(body: &'a mut TemplateBody) -> Self {
Self {
body,
current_path: Vec::new(),
dynamic_text_index: 0,
component_literal_index: 0,
}
}
fn visit_children(&mut self, children: &[BodyNode]) {
for (idx, node) in children.iter().enumerate() {
self.current_path.push(idx as u8);
self.visit(node);
self.current_path.pop();
}
}
fn visit(&mut self, node: &BodyNode) {
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() {
self.assign_path_to_attribute(attr, idx);
if let AttributeValue::AttrLiteral(HotLiteral::Fmted(lit)) = &attr.value {
self.assign_formatted_segment(lit);
}
}
}
// Assign formatted segments to the key which is not included in the merged_attributes
if let Some(AttributeValue::AttrLiteral(HotLiteral::Fmted(fmted))) = el.key() {
self.assign_formatted_segment(fmted);
}
self.visit_children(&el.children);
}
// Text nodes are dynamic if they contain dynamic segments
BodyNode::Text(txt) => {
if !txt.is_static() {
self.assign_path_to_node(node);
self.assign_formatted_segment(&txt.input);
}
}
// Raw exprs are always dynamic
BodyNode::RawExpr(_) | BodyNode::ForLoop(_) | BodyNode::IfChain(_) => {
self.assign_path_to_node(node)
}
BodyNode::Component(component) => {
self.assign_path_to_node(node);
let mut index = 0;
for property in &component.fields {
if let AttributeValue::AttrLiteral(literal) = &property.value {
if let HotLiteral::Fmted(segments) = literal {
self.assign_formatted_segment(segments);
}
component.component_literal_dyn_idx[index]
.set(self.component_literal_index);
self.component_literal_index += 1;
index += 1;
}
}
}
};
}
/// Assign ids to a formatted segment
fn assign_formatted_segment(&mut self, segments: &HotReloadFormattedSegment) {
let mut dynamic_node_indexes = segments.dynamic_node_indexes.iter();
for segment in &segments.segments {
if let Segment::Formatted(segment) = segment {
dynamic_node_indexes
.next()
.unwrap()
.set(self.dynamic_text_index);
self.dynamic_text_index += 1;
self.body.dynamic_text_segments.push(segment.clone());
}
}
}
/// 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_node(&mut self, node: &BodyNode) {
// Assign the TemplateNode::Dynamic index to the node
node.set_dyn_idx(self.body.node_paths.len());
// And then save the current path as the corresponding path
self.body.node_paths.push(self.current_path.clone());
}
/// Assign a path to a attribute and give it its dynamic index
/// This simplifies the ToTokens implementation for the macro to be a little less centralized
pub(crate) fn assign_path_to_attribute(
&mut self,
attribute: &Attribute,
attribute_index: usize,
) {
// Assign the dynamic index to the attribute
attribute.set_dyn_idx(self.body.attr_paths.len());
// And then save the current path as the corresponding path
self.body
.attr_paths
.push((self.current_path.clone(), attribute_index));
}
}
impl TemplateBody {
/// 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.
pub(crate) fn assign_paths_inner(&mut self, nodes: &[BodyNode]) {
let mut visitor = DynIdVisitor::new(self);
visitor.visit_children(nodes);
}
}

View file

@ -28,9 +28,6 @@ use syn::{
Block, Expr, ExprClosure, ExprIf, Ident, Lit, LitBool, LitFloat, LitInt, LitStr, Token,
};
#[cfg(feature = "hot_reload")]
use dioxus_core::prelude::TemplateAttribute;
/// A property value in the from of a `name: value` pair with an optional comma.
/// Note that the colon and value are optional in the case of shorthand attributes. We keep them around
/// to support "lossless" parsing in case that ever might be useful.
@ -120,6 +117,16 @@ impl Attribute {
}
}
/// Set the dynamic index of this attribute
pub fn set_dyn_idx(&self, idx: usize) {
self.dyn_idx.set(idx);
}
/// Get the dynamic index of this attribute
pub fn get_dyn_idx(&self) -> usize {
self.dyn_idx.get()
}
pub fn span(&self) -> proc_macro2::Span {
self.name.span()
}
@ -140,18 +147,15 @@ impl Attribute {
pub fn ifmt(&self) -> Option<&IfmtInput> {
match &self.value {
AttributeValue::AttrLiteral(lit) => match &lit.value {
HotLiteralType::Fmted(input) => Some(input),
_ => None,
},
AttributeValue::AttrLiteral(HotLiteral::Fmted(input)) => Some(input),
_ => None,
}
}
pub fn as_static_str_literal(&self) -> Option<(&AttributeName, &IfmtInput)> {
match &self.value {
AttributeValue::AttrLiteral(lit) => match &lit.value {
HotLiteralType::Fmted(input) if input.is_static() => Some((&self.name, input)),
AttributeValue::AttrLiteral(lit) => match &lit {
HotLiteral::Fmted(input) if input.is_static() => Some((&self.name, input)),
_ => None,
},
_ => None,
@ -162,11 +166,27 @@ impl Attribute {
self.as_static_str_literal().is_some()
}
#[cfg(feature = "hot_reload")]
pub(crate) fn html_tag_and_namespace<Ctx: crate::HotReloadingContext>(
&self,
) -> (&'static str, Option<&'static str>) {
let attribute_name_rust = self.name.to_string();
let element_name = self.el_name.as_ref().unwrap();
let rust_name = match element_name {
ElementName::Ident(i) => i.to_string(),
ElementName::Custom(s) => return (intern(s.value()), None),
};
Ctx::map_attribute(&rust_name, &attribute_name_rust)
.unwrap_or((intern(attribute_name_rust.as_str()), None))
}
#[cfg(feature = "hot_reload")]
pub fn to_template_attribute<Ctx: crate::HotReloadingContext>(
&self,
rust_name: &str,
) -> TemplateAttribute {
) -> dioxus_core::TemplateAttribute {
use dioxus_core::TemplateAttribute;
// If it's a dynamic node, just return it
// 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,
@ -180,11 +200,8 @@ impl Attribute {
}
// Otherwise it's a static node and we can build it
let (_name, value) = self.as_static_str_literal().unwrap();
let attribute_name_rust = self.name.to_string();
let (name, namespace) = Ctx::map_attribute(rust_name, &attribute_name_rust)
.unwrap_or((intern(attribute_name_rust.as_str()), None));
let (_, value) = self.as_static_str_literal().unwrap();
let (name, namespace) = self.html_tag_and_namespace::<Ctx>();
TemplateAttribute::Static {
name,
@ -303,8 +320,15 @@ impl Attribute {
return true;
}
if self.name.to_token_stream().to_string() == self.value.to_token_stream().to_string() {
return true;
// Or if it is a builtin attribute with a single ident value
if let (AttributeName::BuiltIn(name), AttributeValue::AttrExpr(expr)) =
(&self.name, &self.value)
{
if let Ok(Expr::Path(path)) = expr.as_expr() {
if path.path.get_ident() == Some(name) {
return true;
}
}
}
false
@ -537,7 +561,11 @@ impl IfAttributeValue {
return non_string_diagnostic(current_if_value.span());
};
let HotLiteralType::Fmted(new) = &lit.value else {
let HotLiteral::Fmted(HotReloadFormattedSegment {
formatted_input: new,
..
}) = &lit
else {
return non_string_diagnostic(current_if_value.span());
};
@ -554,8 +582,9 @@ impl IfAttributeValue {
}
// If the else value is a literal, then we need to append it to the expression and break
Some(AttributeValue::AttrLiteral(lit)) => {
if let HotLiteralType::Fmted(new) = &lit.value {
expression.extend(quote! { { #new.to_string() } });
if let HotLiteral::Fmted(new) = &lit {
let fmted = &new.formatted_input;
expression.extend(quote! { { #fmted.to_string() } });
break;
} else {
return non_string_diagnostic(current_if_value.span());

View file

@ -20,7 +20,7 @@ use crate::innerlude::*;
use proc_macro2::TokenStream as TokenStream2;
use proc_macro2_diagnostics::SpanDiagnosticExt;
use quote::{quote, ToTokens, TokenStreamExt};
use std::collections::HashSet;
use std::{collections::HashSet, vec};
use syn::{
parse::{Parse, ParseStream},
spanned::Spanned,
@ -32,6 +32,7 @@ pub struct Component {
pub name: syn::Path,
pub generics: Option<AngleBracketedGenericArguments>,
pub fields: Vec<Attribute>,
pub component_literal_dyn_idx: Vec<DynIdx>,
pub spreads: Vec<Spread>,
pub brace: token::Brace,
pub children: TemplateBody,
@ -56,12 +57,19 @@ impl Parse for Component {
diagnostics,
} = input.parse::<RsxBlock>()?;
let literal_properties_count = fields
.iter()
.filter(|attr| matches!(attr.value, AttributeValue::AttrLiteral(_)))
.count();
let component_literal_dyn_idx = vec![DynIdx::default(); literal_properties_count];
let mut component = Self {
dyn_idx: DynIdx::default(),
children: TemplateBody::new(children),
name,
generics,
fields,
component_literal_dyn_idx,
brace,
spreads,
diagnostics,
@ -71,7 +79,6 @@ impl Parse for 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)
@ -167,38 +174,17 @@ impl 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"))
pub fn get_key(&self) -> Option<&AttributeValue> {
self.fields.iter().find_map(|attr| match &attr.name {
AttributeName::BuiltIn(key) if key == "key" => Some(&attr.value),
_ => None,
})
}
/// 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
/// Also ensure there's no stringly typed props
fn validate_fields(&mut self) {
let mut seen = HashSet::new();
@ -271,9 +257,10 @@ impl Component {
}
fn make_field_idents(&self) -> Vec<(TokenStream2, TokenStream2)> {
let mut dynamic_literal_index = 0;
self.fields
.iter()
.filter_map(|attr| {
.filter_map(move |attr| {
let Attribute { name, value, .. } = attr;
let attr = match name {
@ -287,7 +274,30 @@ impl Component {
AttributeName::Spread(_) => return None,
};
Some((attr, value.to_token_stream()))
let release_value = value.to_token_stream();
// In debug mode, we try to grab the value from the dynamic literal pool if possible
let value = if let AttributeValue::AttrLiteral(literal) = &value {
let idx = self.component_literal_dyn_idx[dynamic_literal_index].get();
dynamic_literal_index += 1;
let debug_value = quote! { __dynamic_literal_pool.component_property(#idx, &*__template_read, #literal) };
quote! {
{
#[cfg(debug_assertions)]
{
#debug_value
}
#[cfg(not(debug_assertions))]
{
#release_value
}
}
}
} else {
release_value
};
Some((attr, value))
})
.collect()
}
@ -310,6 +320,7 @@ impl Component {
fields: vec![],
spreads: vec![],
children: TemplateBody::new(vec![]),
component_literal_dyn_idx: vec![],
dyn_idx: DynIdx::default(),
diagnostics,
}
@ -429,7 +440,7 @@ fn generics_params() {
let input_without_children = quote! {
Outlet::<R> {}
};
let component: CallBody = syn::parse2(input_without_children).unwrap();
let component: crate::CallBody = syn::parse2(input_without_children).unwrap();
println!("{}", component.to_token_stream().pretty_unparse());
}

View file

@ -270,11 +270,9 @@ impl Element {
}
// 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;
}
if let AttributeValue::AttrLiteral(HotLiteral::Fmted(lit)) = &matching_attr.value {
out.push_ifmt(lit.formatted_input.clone());
continue;
}
// Merge `if cond { "abc" } else if ...` into the output
@ -289,10 +287,7 @@ impl Element {
);
}
let out_lit = HotLiteral {
value: HotLiteralType::Fmted(out),
hr_idx: Default::default(),
};
let out_lit = HotLiteral::Fmted(out.into());
self.merged_attributes.push(Attribute {
name: attr.name.clone(),
@ -305,11 +300,11 @@ impl Element {
}
}
pub(crate) fn key(&self) -> Option<&IfmtInput> {
pub(crate) fn key(&self) -> Option<&AttributeValue> {
for attr in &self.raw_attributes {
if let AttributeName::BuiltIn(name) = &attr.name {
if name == "key" {
return attr.ifmt();
return Some(&attr.value);
}
}
}

View file

@ -0,0 +1,651 @@
//! This module contains the diffing logic for rsx hot reloading.
//!
//! There's a few details that I wish we could've gotten right but we can revisit later:
//!
//! - Expanding an if chain is not possible - only its contents can be hot reloaded
//!
//! - Components that don't start with children can't be hot reloaded - 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 pool. This would require handling aliases
//! in hot reload diffing.
//!
//! - 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 hot reloading.
//! However, you could imagine a scenario where literal hot reloading would be useful without the
//! compiler in the loop. Ideally we can slash most of this code once patching is stable.
//!
//! ## Assigning/Scoring Templates
//!
//! We can clone most dynamic items from the last full rebuild:
//! - Dynamic text segments: `div { width: "{x}%" } -> div { width: "{x}%", height: "{x}%" }`
//! - Dynamic attributes: `div { width: dynamic } -> div { width: dynamic, height: dynamic }`
//! - Dynamic nodes: `div { {children} } -> div { {children} {children} }`
//!
//! But we cannot clone rsx bodies themselves because we cannot hot reload the new rsx body:
//! - `div { Component { "{text}" } } -> div { Component { "{text}" } Component { "hello" } }` // We can't create a template for both "{text}" and "hello"
//!
//! In some cases, two nodes with children are ambiguous. For example:
//! ```rust, ignore
//! rsx! {
//! div {
//! Component { "{text}" }
//! Component { "hello" }
//! }
//! }
//! ```
//!
//! Outside of the template, both components are compatible for hot reloading.
//!
//! After we create a list of all components with compatible names and props, we need to find the best match for the
//! template.
//!
//!
//! Dioxus uses a greedy algorithm to find the best match. We first try to create the child template with the dynamic context from the last full rebuild.
//! Then we use the child template that leaves the least unused dynamic items in the pool to create the new template.
//!
//! For the example above:
//! - Hot reloading `Component { "hello" }`:
//! - Try to hot reload the component body `"hello"` with the dynamic pool from `"{text}"`: Success with 1 unused dynamic item
//! - Try to hot reload the component body `"hello"` with the dynamic pool from `"hello"`: Success with 0 unused dynamic items
//! - We use the the template that leaves the least unused dynamic items in the pool - `"hello"`
//! - Hot reloading `Component { "{text}" }`:
//! - Try to hot reload the component body `"{text}"` with the dynamic pool from `"{text}"`: Success with 0 unused dynamic items
//! - The `"hello"` template has already been hot reloaded, so we don't try to hot reload it again
//! - We use the the template that leaves the least unused dynamic items in the pool - `"{text}"`
//!
//! Greedy algorithms are optimal when:
//! - The step we take reduces the problem size
//! - The subproblem is optimal
//!
//! In this case, hot reloading a template removes it from the pool of templates we can use to hot reload the next template which reduces the problem size.
//!
//! The subproblem is optimal because the alternative is leaving less dynamic items for the remaining templates to hot reload which just makes it
//! more difficult to match future templates.
use crate::innerlude::*;
use crate::HotReloadingContext;
use dioxus_core::internal::{
FmtedSegments, HotReloadAttributeValue, HotReloadDynamicAttribute, HotReloadDynamicNode,
HotReloadLiteral, HotReloadedTemplate, NamedAttribute,
};
use std::collections::HashMap;
use std::hash::DefaultHasher;
use std::hash::Hash;
use std::hash::Hasher;
use super::last_build_state::LastBuildState;
/// A result of hot reloading
///
/// This contains information about what has changed so the hotreloader can apply the right changes
#[non_exhaustive]
#[derive(Debug, PartialEq, Clone)]
pub struct HotReloadResult {
/// The state of the last full rebuild.
full_rebuild_state: LastBuildState,
/// The child templates we have already used. As we walk through the template tree, we will run into child templates.
/// Each of those child templates also need to be hot reloaded. We keep track of which ones we've already hotreloaded
/// to avoid diffing the same template twice against different new templates.
///
/// ```rust, ignore
/// rsx! {
/// Component { class: "{class}", "{text}" } // The children of a Component is a new template
/// for item in items {
/// "{item}" // The children of a for loop is a new template
/// }
/// if true {
/// "{text}" // The children of an if chain is a new template
/// }
/// }
/// ```
///
/// If we hotreload the component, we don't need to hotreload the for loop
///
/// You should diff the result of this against the old template to see if you actually need to send down the result
pub templates: HashMap<usize, HotReloadedTemplate>,
/// The dynamic nodes for the current node
dynamic_nodes: Vec<HotReloadDynamicNode>,
/// The dynamic attributes for the current node
dynamic_attributes: Vec<HotReloadDynamicAttribute>,
/// The literal component properties for the current node
literal_component_properties: Vec<HotReloadLiteral>,
}
impl HotReloadResult {
/// Calculate the hot reload diff between two template bodies
pub fn new<Ctx: HotReloadingContext>(
full_rebuild_state: &TemplateBody,
new: &TemplateBody,
name: String,
) -> Option<Self> {
let full_rebuild_state = LastBuildState::new(full_rebuild_state, name);
let mut s = Self {
full_rebuild_state,
templates: Default::default(),
dynamic_nodes: Default::default(),
dynamic_attributes: Default::default(),
literal_component_properties: Default::default(),
};
s.hotreload_body::<Ctx>(new)?;
Some(s)
}
fn extend(&mut self, other: Self) {
self.templates.extend(other.templates);
}
/// Walk the dynamic contexts and do our best to find hot reload-able changes between the two
/// sets of dynamic nodes/attributes. If there's a change we can't hot reload, 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 hot reload a node if:
/// - We add or modify a new rust expression
/// - Adding a new formatted segment we haven't seen before
/// - Adding a new dynamic node (loop, fragment, if chain, etc)
/// - We add a new component field
/// - We remove a component field
/// - We change the type of a component field
///
/// If a dynamic node is removed, we don't necessarily need to kill hot reload - 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 hot reloader to hot onto DynamicContexts directly instead of the CallBody since
/// you can preserve more information about the nodes as they've changed over time.
fn hotreload_body<Ctx: HotReloadingContext>(&mut self, new: &TemplateBody) -> Option<()> {
// Quickly run through dynamic attributes first attempting to invalidate them
// Move over old IDs onto the new template
self.hotreload_attributes::<Ctx>(new)?;
let new_dynamic_attributes = std::mem::take(&mut self.dynamic_attributes);
// Now we can run through the dynamic nodes and see if we can hot reload them
// Move over old IDs onto the new template
self.hotreload_dynamic_nodes::<Ctx>(new)?;
let new_dynamic_nodes = std::mem::take(&mut self.dynamic_nodes);
let literal_component_properties = std::mem::take(&mut self.literal_component_properties);
let key = self.hot_reload_key(new)?;
let roots: Vec<_> = new
.roots
.iter()
.map(|node| node.to_template_node::<Ctx>())
.collect();
let roots: &[dioxus_core::TemplateNode] = intern(&*roots);
// Add the template name, the dyn index and the hash of the template to get a unique name
let name = {
let mut hasher = DefaultHasher::new();
key.hash(&mut hasher);
new_dynamic_attributes.hash(&mut hasher);
new_dynamic_nodes.hash(&mut hasher);
literal_component_properties.hash(&mut hasher);
roots.hash(&mut hasher);
let hash = hasher.finish();
let name = &self.full_rebuild_state.name;
format!("{}:{}-{}", name, hash, new.template_idx.get())
};
let name = Box::leak(name.into_boxed_str());
let template = HotReloadedTemplate::new(
name,
key,
new_dynamic_nodes,
new_dynamic_attributes,
literal_component_properties,
roots,
);
self.templates
.insert(self.full_rebuild_state.root_index.get(), template);
Some(())
}
fn hot_reload_key(&mut self, new: &TemplateBody) -> Option<Option<FmtedSegments>> {
match new.implicit_key() {
Some(AttributeValue::AttrLiteral(HotLiteral::Fmted(value))) => Some(Some(
self.full_rebuild_state
.hot_reload_formatted_segments(value)?,
)),
None => Some(None),
_ => None,
}
}
fn hotreload_dynamic_nodes<Ctx: HotReloadingContext>(
&mut self,
new: &TemplateBody,
) -> Option<()> {
for new_node in new.dynamic_nodes() {
self.hot_reload_node::<Ctx>(new_node)?
}
Some(())
}
fn hot_reload_node<Ctx: HotReloadingContext>(&mut self, node: &BodyNode) -> Option<()> {
match node {
BodyNode::Text(text) => self.hotreload_text_node(text),
BodyNode::Component(component) => self.hotreload_component::<Ctx>(component),
BodyNode::ForLoop(forloop) => self.hotreload_for_loop::<Ctx>(forloop),
BodyNode::IfChain(ifchain) => self.hotreload_if_chain::<Ctx>(ifchain),
BodyNode::RawExpr(expr) => self.hotreload_raw_expr(expr),
BodyNode::Element(_) => Some(()),
}
}
fn hotreload_raw_expr(&mut self, expr: &ExprNode) -> Option<()> {
// Try to find the raw expr in the last build
let expr_index = self
.full_rebuild_state
.dynamic_nodes
.position(|node| match &node {
BodyNode::RawExpr(raw_expr) => raw_expr.expr == expr.expr,
_ => false,
})?;
// If we find it, push it as a dynamic node
self.dynamic_nodes
.push(HotReloadDynamicNode::Dynamic(expr_index));
Some(())
}
fn hotreload_for_loop<Ctx>(&mut self, forloop: &ForLoop) -> Option<()>
where
Ctx: HotReloadingContext,
{
// Find all for loops that have the same pattern and expression
let candidate_for_loops = self
.full_rebuild_state
.dynamic_nodes
.inner
.iter()
.enumerate()
.filter_map(|(index, node)| {
if let BodyNode::ForLoop(for_loop) = &node.inner {
if for_loop.pat == forloop.pat && for_loop.expr == forloop.expr {
return Some((index, for_loop));
}
}
None
})
.collect::<Vec<_>>();
// Then find the one that has the least wasted dynamic items when hot reloading the body
let (index, best_call_body) = self.diff_best_call_body::<Ctx>(
candidate_for_loops
.iter()
.map(|(_, for_loop)| &for_loop.body),
&forloop.body,
)?;
// Push the new for loop as a dynamic node
self.dynamic_nodes
.push(HotReloadDynamicNode::Dynamic(candidate_for_loops[index].0));
self.extend(best_call_body);
Some(())
}
fn hotreload_text_node(&mut self, text_node: &TextNode) -> Option<()> {
// If it is static, it is already included in the template and we don't need to do anything
if text_node.input.is_static() {
return Some(());
}
// Otherwise, hot reload the formatted segments and push that as a dynamic node
let formatted_segments = self
.full_rebuild_state
.hot_reload_formatted_segments(&text_node.input)?;
self.dynamic_nodes
.push(HotReloadDynamicNode::Formatted(formatted_segments));
Some(())
}
/// Find the call body that minimizes the number of wasted dynamic items
///
/// Returns the index of the best call body and the state of the best call body
fn diff_best_call_body<'a, Ctx>(
&self,
bodies: impl Iterator<Item = &'a TemplateBody>,
new_call_body: &TemplateBody,
) -> Option<(usize, Self)>
where
Ctx: HotReloadingContext,
{
let mut best_score = usize::MAX;
let mut best_output = None;
for (index, body) in bodies.enumerate() {
// Skip templates we've already hotreloaded
if self.templates.contains_key(&body.template_idx.get()) {
continue;
}
if let Some(state) =
Self::new::<Ctx>(body, new_call_body, self.full_rebuild_state.name.clone())
{
let score = state.full_rebuild_state.unused_dynamic_items();
if score < best_score {
best_score = score;
best_output = Some((index, state));
}
}
}
best_output
}
fn hotreload_component<Ctx>(&mut self, component: &Component) -> Option<()>
where
Ctx: HotReloadingContext,
{
// First we need to find the component that matches the best in the last build
// We try each build and choose the option that wastes the least dynamic items
let components_with_matching_attributes: Vec<_> = self
.full_rebuild_state
.dynamic_nodes
.inner
.iter()
.enumerate()
.filter_map(|(index, node)| {
if let BodyNode::Component(comp) = &node.inner {
return Some((
index,
comp,
self.hotreload_component_fields(comp, component)?,
));
}
None
})
.collect();
let possible_bodies = components_with_matching_attributes
.iter()
.map(|(_, comp, _)| &comp.children);
let (index, new_body) =
self.diff_best_call_body::<Ctx>(possible_bodies, &component.children)?;
let (index, _, literal_component_properties) = &components_with_matching_attributes[index];
let index = *index;
self.full_rebuild_state.dynamic_nodes.inner[index]
.used
.set(true);
self.literal_component_properties
.extend(literal_component_properties.iter().cloned());
self.extend(new_body);
// Push the new component as a dynamic node
self.dynamic_nodes
.push(HotReloadDynamicNode::Dynamic(index));
Some(())
}
fn hotreload_component_fields(
&self,
old_component: &Component,
new_component: &Component,
) -> Option<Vec<HotReloadLiteral>> {
// First check if the component is the same
if new_component.name != old_component.name {
return None;
}
// Then check if the fields are the same
if new_component.fields.len() != old_component.fields.len() {
return None;
}
let mut new_fields = new_component.fields.clone();
new_fields.sort_by(|a, b| a.name.to_string().cmp(&b.name.to_string()));
let mut old_fields = old_component.fields.clone();
old_fields.sort_by(|a, b| a.name.to_string().cmp(&b.name.to_string()));
let mut literal_component_properties = Vec::new();
for (new_field, old_field) in new_fields.iter().zip(old_fields.iter()) {
// Verify the names match
if new_field.name != old_field.name {
return None;
}
// Verify the values match
match (&new_field.value, &old_field.value) {
// If the values are both literals, we can try to hotreload them
(
AttributeValue::AttrLiteral(new_value),
AttributeValue::AttrLiteral(old_value),
) => {
// Make sure that the types are the same
if std::mem::discriminant(new_value) != std::mem::discriminant(old_value) {
return None;
}
let literal = self.full_rebuild_state.hotreload_hot_literal(new_value)?;
literal_component_properties.push(literal);
}
_ => {
if new_field.value != old_field.value {
return None;
}
}
}
}
Some(literal_component_properties)
}
/// Hot reload an if chain
fn hotreload_if_chain<Ctx: HotReloadingContext>(
&mut self,
new_if_chain: &IfChain,
) -> Option<()> {
let mut best_if_chain = None;
let mut best_score = usize::MAX;
let if_chains = self
.full_rebuild_state
.dynamic_nodes
.inner
.iter()
.enumerate()
.filter_map(|(index, node)| {
if let BodyNode::IfChain(if_chain) = &node.inner {
return Some((index, if_chain));
}
None
});
// Find the if chain that matches all of the conditions and wastes the least dynamic items
for (index, old_if_chain) in if_chains {
let Some(chain_templates) = Self::diff_if_chains::<Ctx>(
old_if_chain,
new_if_chain,
self.full_rebuild_state.name.clone(),
) else {
continue;
};
let score = chain_templates
.iter()
.map(|t| t.full_rebuild_state.unused_dynamic_items())
.sum();
if score < best_score {
best_score = score;
best_if_chain = Some((index, chain_templates));
}
}
// If we found a hot reloadable if chain, hotreload it
let (index, chain_templates) = best_if_chain?;
// Mark the if chain as used
self.full_rebuild_state.dynamic_nodes.inner[index]
.used
.set(true);
// Merge the hot reload changes into the current state
for template in chain_templates {
self.extend(template);
}
// Push the new if chain as a dynamic node
self.dynamic_nodes
.push(HotReloadDynamicNode::Dynamic(index));
Some(())
}
/// Hot reload an if chain
fn diff_if_chains<Ctx: HotReloadingContext>(
old_if_chain: &IfChain,
new_if_chain: &IfChain,
name: String,
) -> Option<Vec<Self>> {
// Go through each part of the if chain and find the best match
let mut old_chain = old_if_chain;
let mut new_chain = new_if_chain;
let mut chain_templates = Vec::new();
loop {
// Make sure the conditions are the same
if old_chain.cond != new_chain.cond {
return None;
}
// If the branches are the same, we can hotreload them
let hot_reload =
Self::new::<Ctx>(&old_chain.then_branch, &new_chain.then_branch, name.clone())?;
chain_templates.push(hot_reload);
// Make sure the if else branches match
match (
old_chain.else_if_branch.as_ref(),
new_chain.else_if_branch.as_ref(),
) {
(Some(old), Some(new)) => {
old_chain = old;
new_chain = new;
}
(None, None) => {
break;
}
_ => return None,
}
}
// Make sure the else branches match
match (&old_chain.else_branch, &new_chain.else_branch) {
(Some(old), Some(new)) => {
let template = Self::new::<Ctx>(old, new, name.clone())?;
chain_templates.push(template);
}
(None, None) => {}
_ => return None,
}
Some(chain_templates)
}
/// Take a new template body and return the attributes that can be hot reloaded from the last build
///
/// IE if we shuffle attributes, remove attributes or add new attributes with the same dynamic segments, around we should be able to hot reload them.
///
/// ```rust, ignore
/// rsx! {
/// div { id: "{id}", class: "{class}", width, "Hi" }
/// }
///
/// rsx! {
/// div { width, class: "{class}", id: "{id} and {class}", "Hi" }
/// }
/// ```
fn hotreload_attributes<Ctx: HotReloadingContext>(&mut self, new: &TemplateBody) -> Option<()> {
// Walk through each attribute and create a new HotReloadAttribute for each one
for new_attr in new.dynamic_attributes() {
// 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::<Ctx>(new_attr)?;
}
Some(())
}
/// Try to hot reload an attribute and return the new HotReloadAttribute
fn hotreload_attribute<Ctx: HotReloadingContext>(
&mut self,
attribute: &Attribute,
) -> Option<()> {
let (tag, namespace) = attribute.html_tag_and_namespace::<Ctx>();
// If the attribute is a spread, try to grab it from the last build
// If it wasn't in the last build with the same name, we can't hot reload it
if let AttributeName::Spread(_) = &attribute.name {
let hot_reload_attribute = self
.full_rebuild_state
.dynamic_attributes
.position(|a| a.name == attribute.name && a.value == attribute.value)?;
self.dynamic_attributes
.push(HotReloadDynamicAttribute::Dynamic(hot_reload_attribute));
return Some(());
}
// Otherwise the attribute is named, try to hot reload the value
let value = match &attribute.value {
// If the attribute is a literal, we can generally hot reload it if the formatted segments exist in the last build
AttributeValue::AttrLiteral(literal) => {
// If it is static, it is already included in the template and we don't need to do anything
if literal.is_static() {
return Some(());
}
// Otherwise, hot reload the literal and push that as a dynamic attribute
let hot_reload_literal = self.full_rebuild_state.hotreload_hot_literal(literal)?;
HotReloadAttributeValue::Literal(hot_reload_literal)
}
// If it isn't a literal, try to find an exact match for the attribute value from the last build
_ => {
let value_index = self.full_rebuild_state.dynamic_attributes.position(|a| {
!matches!(a.name, AttributeName::Spread(_)) && a.value == attribute.value
})?;
HotReloadAttributeValue::Dynamic(value_index)
}
};
self.dynamic_attributes
.push(HotReloadDynamicAttribute::Named(NamedAttribute::new(
tag, namespace, value,
)));
Some(())
}
}

View file

@ -0,0 +1,157 @@
use crate::innerlude::*;
use dioxus_core::internal::{FmtSegment, FmtedSegments, HotReloadLiteral};
use std::cell::Cell;
/// A pool of items we can grab from during hot reloading.
/// We have three different pools we can pull from:
/// - Dynamic text segments (eg: "{class}")
/// - Dynamic nodes (eg: {children})
/// - Dynamic attributes (eg: ..spread )
///
/// As we try to create a new hot reloaded template, we will pull from these pools to create the new template. We mark
/// each item as used the first time we use it in the new template. Once the new template if fully created, we can tally
/// up how many items are unused to determine how well the new template matches the old template.
///
/// The template that matches best will leave the least unused items in the pool.
#[derive(Debug, PartialEq, Clone)]
pub(crate) struct BakedPool<T> {
pub inner: Vec<BakedItem<T>>,
}
impl<T> BakedPool<T> {
/// Create a new baked pool from an iterator of items
fn new(inner: impl IntoIterator<Item = T>) -> Self {
Self {
inner: inner.into_iter().map(BakedItem::new).collect(),
}
}
/// Find the first item in the pool that matches the condition and mark it as used
pub fn position(&self, condition: impl Fn(&T) -> bool) -> Option<usize> {
for (idx, baked_item) in self.inner.iter().enumerate() {
if condition(&baked_item.inner) {
baked_item.used.set(true);
return Some(idx);
}
}
None
}
/// Find the number of unused items in the pool
fn unused_dynamic_items(&self) -> usize {
self.inner
.iter()
.filter(|baked_item| !baked_item.used.get())
.count()
}
}
/// A single item in the baked item pool. We keep track if which items are used for scoring how well two templates match.
#[derive(Debug, PartialEq, Clone)]
pub(crate) struct BakedItem<T> {
pub inner: T,
pub used: Cell<bool>,
}
impl<T> BakedItem<T> {
fn new(inner: T) -> Self {
Self {
inner,
used: Cell::new(false),
}
}
}
/// The state of the last full rebuild.
/// This object contains the pool of compiled dynamic segments we can pull from for hot reloading
#[derive(Debug, PartialEq, Clone)]
pub(crate) struct LastBuildState {
/// The formatted segments that were used in the last build. Eg: "{class}", "{id}"
///
/// We are free to use each of these segments many times in the same build.
/// We just clone the result (assuming display + debug have no side effects)
pub dynamic_text_segments: BakedPool<FormattedSegment>,
/// The dynamic nodes that were used in the last build. Eg: div { {children} }
///
/// We are also free to clone these nodes many times in the same build.
pub dynamic_nodes: BakedPool<BodyNode>,
/// The attributes that were used in the last build. Eg: div { class: "{class}" }
///
/// We are also free to clone these nodes many times in the same build.
pub dynamic_attributes: BakedPool<Attribute>,
/// The component literal properties we can hot reload from the last build. Eg: Component { class: "{class}" }
///
/// In the new build, we must assign each of these a value even if we no longer use the component.
/// The type must be the same as the last time we compiled the property
pub component_properties: Vec<HotLiteral>,
/// The root indexes of the last build
pub root_index: DynIdx,
/// The name of the original template
pub name: String,
}
impl LastBuildState {
/// Create a new LastBuildState from the given [`TemplateBody`]
pub fn new(body: &TemplateBody, name: String) -> Self {
let dynamic_text_segments = body.dynamic_text_segments.iter().cloned();
let dynamic_nodes = body.dynamic_nodes().cloned();
let dynamic_attributes = body.dynamic_attributes().cloned();
let component_properties = body.literal_component_properties().cloned().collect();
Self {
dynamic_text_segments: BakedPool::new(dynamic_text_segments),
dynamic_nodes: BakedPool::new(dynamic_nodes),
dynamic_attributes: BakedPool::new(dynamic_attributes),
component_properties,
root_index: body.template_idx.clone(),
name,
}
}
/// Return the number of unused dynamic items in the pool
pub fn unused_dynamic_items(&self) -> usize {
self.dynamic_text_segments.unused_dynamic_items()
+ self.dynamic_nodes.unused_dynamic_items()
+ self.dynamic_attributes.unused_dynamic_items()
}
/// Hot reload a hot literal
pub fn hotreload_hot_literal(&self, hot_literal: &HotLiteral) -> Option<HotReloadLiteral> {
match hot_literal {
// If the literal is a formatted segment, map the segments to the new formatted segments
HotLiteral::Fmted(segments) => {
let new_segments = self.hot_reload_formatted_segments(segments)?;
Some(HotReloadLiteral::Fmted(new_segments))
}
// Otherwise just pass the literal through unchanged
HotLiteral::Bool(b) => Some(HotReloadLiteral::Bool(b.value())),
HotLiteral::Float(f) => Some(HotReloadLiteral::Float(f.base10_parse().ok()?)),
HotLiteral::Int(i) => Some(HotReloadLiteral::Int(i.base10_parse().ok()?)),
}
}
pub fn hot_reload_formatted_segments(
&self,
new: &HotReloadFormattedSegment,
) -> Option<FmtedSegments> {
// Go through each dynamic segment and look for a match in the formatted segments pool.
// If we find a match, we can hot reload the segment otherwise we need to do a full rebuild
let mut segments = Vec::new();
for segment in &new.segments {
match segment {
// If it is a literal, we can always hot reload it. Just add it to the segments
Segment::Literal(value) => {
segments.push(FmtSegment::Literal {
value: Box::leak(value.clone().into_boxed_str()),
});
} // If it is a dynamic segment, we need to check if it exists in the formatted segments pool
Segment::Formatted(formatted) => {
let index = self.dynamic_text_segments.position(|s| s == formatted)?;
segments.push(FmtSegment::Dynamic { id: index });
}
}
}
Some(FmtedSegments::new(segments))
}
}

View file

@ -1,9 +1,17 @@
#[cfg(feature = "hot_reload")]
mod hot_reload_diff;
mod collect;
#[cfg(feature = "hot_reload")]
pub use hot_reload_diff::*;
pub use collect::*;
#[cfg(feature = "hot_reload_traits")]
mod hot_reloading_context;
mod context;
#[cfg(feature = "hot_reload_traits")]
pub use hot_reloading_context::*;
pub use context::*;
#[cfg(feature = "hot_reload")]
mod diff;
#[cfg(feature = "hot_reload")]
pub use diff::*;
#[cfg(feature = "hot_reload")]
mod last_build_state;

View file

@ -1,429 +0,0 @@
#![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())),
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)
}
}

View file

@ -1,6 +1,3 @@
#[cfg(feature = "hot_reload")]
use dioxus_core::TemplateNode;
use crate::location::DynIdx;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
@ -35,13 +32,6 @@ impl IfChain {
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 {

View file

@ -1,6 +1,3 @@
#[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};
@ -94,64 +91,6 @@ impl IfmtInput {
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,
@ -272,6 +211,11 @@ impl IfmtInput {
impl ToTokens for IfmtInput {
fn to_tokens(&self, tokens: &mut TokenStream) {
// If the input is a string literal, we can just return it
if let Some(static_str) = self.to_static() {
return static_str.to_tokens(tokens);
}
// Try to turn it into a single _.to_string() call
if !cfg!(debug_assertions) {
if let Some(single_dynamic) = self.try_to_string() {
@ -284,7 +228,7 @@ impl ToTokens for IfmtInput {
if self.is_simple_expr() {
let raw = &self.source;
tokens.extend(quote! {
::std::format_args!(#raw)
::std::format!(#raw)
});
return;
}
@ -323,7 +267,7 @@ impl ToTokens for IfmtInput {
quote_spanned! {
span =>
::std::format_args!(
::std::format!(
#format_literal
#(, #positional_args)*
)
@ -359,7 +303,7 @@ impl ToTokens for FormattedSegment {
let (fmt, seg) = (&self.format_args, &self.segment);
let fmt = format!("{{0:{fmt}}}");
tokens.append_all(quote! {
format_args!(#fmt, #seg)
format!(#fmt, #seg)
});
}
}
@ -470,11 +414,4 @@ mod tests {
println!("{}", input.to_string_with_quotes());
assert!(input.is_static());
}
#[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();
}
}

View file

@ -49,6 +49,7 @@
//! dioxus_elements::elements::di
//! ```
mod assign_dyn_ids;
mod attribute;
mod component;
mod element;
@ -63,13 +64,10 @@ 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;
// Re-export the namespaces into each other
@ -105,16 +103,11 @@ pub(crate) mod innerlude {
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::*;
pub use crate::diagnostics::*;
pub use crate::ifmt::*;
pub use crate::literal::*;
pub use crate::reload_stack::*;
pub use crate::util::*;
#[cfg(feature = "hot_reload")]
pub use crate::hotreload::*;
}

View file

@ -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());

View file

@ -1,6 +1,3 @@
#[cfg(feature = "hot_reload")]
use dioxus_core::TemplateNode;
use crate::innerlude::*;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::ToTokens;
@ -133,7 +130,8 @@ impl BodyNode {
///
/// dioxus-core uses this to understand templates at compiletime
#[cfg(feature = "hot_reload")]
pub fn to_template_node<Ctx: crate::HotReloadingContext>(&self) -> TemplateNode {
pub fn to_template_node<Ctx: crate::HotReloadingContext>(&self) -> dioxus_core::TemplateNode {
use dioxus_core::TemplateNode;
match self {
BodyNode::Element(el) => {
let rust_name = el.name.to_string();
@ -153,7 +151,7 @@ impl BodyNode {
attrs: intern(
el.merged_attributes
.iter()
.map(|attr| attr.to_template_attribute::<Ctx>(&rust_name))
.map(|attr| attr.to_template_attribute::<Ctx>())
.collect::<Vec<_>>(),
),
}

View file

@ -1,113 +0,0 @@
/// 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);
}

View file

@ -4,7 +4,7 @@
//! Currently the additional tooling doesn't do much.
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
use quote::ToTokens;
use std::{cell::Cell, fmt::Debug};
use syn::{
parse::{Parse, ParseStream},
@ -25,7 +25,6 @@ use crate::{BodyNode, TemplateBody};
#[derive(Debug, Clone)]
pub struct CallBody {
pub body: TemplateBody,
pub ifmt_idx: Cell<usize>,
pub template_idx: Cell<usize>,
}
@ -38,10 +37,7 @@ impl Parse for CallBody {
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),
}
self.body.to_tokens(out)
}
}
@ -52,7 +48,6 @@ impl CallBody {
pub fn new(body: TemplateBody) -> Self {
let body = CallBody {
body,
ifmt_idx: Cell::new(0),
template_idx: Cell::new(0),
};
@ -91,36 +86,17 @@ impl CallBody {
///
/// Lots of wiring!
///
/// However, here, we only need to wire up ifmt and template IDs since TemplateBody will handle the rest.
/// However, here, we only need to wire up 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);
}
@ -134,16 +110,12 @@ impl CallBody {
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);

View file

@ -1,330 +0,0 @@
#![cfg(feature = "hot_reload")]
use crate::{Attribute, AttributeValue, BodyNode, HotLiteralType, IfAttributeValue, 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 reuse, 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,
}
}
(
IfExpr(IfAttributeValue {
condition: cond_a,
then_value: value_a,
else_value: else_value_a,
}),
IfExpr(IfAttributeValue {
condition: cond_b,
then_value: value_b,
else_value: else_value_b,
}),
) if cond_a == cond_b => {
// If the condition is the same, we can hotreload it
score_attr_value(value_a, value_b)
+ match (else_value_a, else_value_b) {
(Some(a), Some(b)) => score_attr_value(a, b),
(None, None) => 0,
_ => usize::MAX,
}
}
// todo: we should try and score recursively 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);
}
}
}

View file

@ -19,9 +19,11 @@
//! - 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
//! - The IDs of formatted strings in debug mode only. Any formatted segments like "{x:?}" get pulled out
//! into a pool so we can move them around during hot reloading on a per-template basis.
//!
//! - The IDs of component property literals in debug mode only. Any component property literals like
//! 1234 get pulled into the pool so we can hot reload them with the context of the literal pool.
//!
//! We solve this by parsing the structure completely and then doing a second pass that fills in IDs
//! by walking the structure.
@ -29,25 +31,22 @@
//! 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
//! id: "node-{node_id}", <--- {node_id} has the formatted segment id 0 in the literal pool
//! ..props, <--- spreads are not reloadable
//!
//! "Hello, world! <--- not tracked but reloadable since it's just a string
//! "Hello, world! <--- not tracked but reloadable in the template 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
//! for item in 0..10 { <--- both 0 and 10 are technically reloadable, but we don't hot reload them today...
//! div { "cool-{item}" } <--- {item} has the formatted segment id 1 in the literal pool
//! }
//!
//! Link {
//! to: "/home", <-- hotreloadable since its a component prop
//! class: "link {is_ready}", <-- hotreloadable since its a formatted string as a prop
//! to: "/home", <-- hotreloadable since its a component prop literal (with component literal id 0)
//! class: "link {is_ready}", <-- {is_ready} has the formatted segment id 2 in the literal pool and the property has the component literal id 1
//! "Home" <-- hotreloadable since its a component child (via template)
//! }
//! }
@ -58,9 +57,8 @@ use self::location::DynIdx;
use crate::innerlude::Attribute;
use crate::*;
use proc_macro2::TokenStream as TokenStream2;
#[cfg(feature = "hot_reload")]
use dioxus_core::prelude::Template;
use proc_macro2_diagnostics::SpanDiagnosticExt;
use syn::parse_quote;
type NodePath = Vec<u8>;
type AttributePath = Vec<u8>;
@ -81,8 +79,8 @@ pub struct TemplateBody {
pub template_idx: DynIdx,
pub node_paths: Vec<NodePath>,
pub attr_paths: Vec<(AttributePath, usize)>,
pub dynamic_text_segments: Vec<FormattedSegment>,
pub diagnostics: Diagnostics,
current_path: Vec<u8>,
}
impl Parse for TemplateBody {
@ -101,9 +99,19 @@ impl Parse for TemplateBody {
/// 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 the nodes are completely empty, insert a placeholder node
// Core expects at least one node in the template to make it easier to replace
if self.is_empty() {
// Create an empty template body with a placeholder and diagnostics + the template index from the original
let empty = Self::new(vec![BodyNode::RawExpr(parse_quote! {()})]);
let default = Self {
diagnostics: self.diagnostics.clone(),
template_idx: self.template_idx.clone(),
..empty
};
// And then render the default template body
default.to_tokens(tokens);
return;
}
// If we have an implicit key, then we need to write its tokens
@ -112,34 +120,7 @@ impl ToTokens for TemplateBody {
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 } }
}
});
let roots = self.quote_roots();
// Print paths is easy - just print the paths
let node_paths = self.node_paths.iter().map(|it| quote!(&[#(#it),*]));
@ -147,50 +128,76 @@ impl ToTokens for TemplateBody {
// 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 }
});
let dynamic_nodes: Vec<_> = self.dynamic_nodes().collect();
// 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());
let dyn_attr_printer: Vec<_> = self
.dynamic_attributes()
.map(|attr| attr.rendered_as_dynamic_attr())
.collect();
let dynamic_text = self.dynamic_text_segments.iter();
let index = self.template_idx.get();
let diagnostics = &self.diagnostics;
let hot_reload_mapping = self.hot_reload_mapping(quote! { ___TEMPLATE_NAME });
tokens.append_all(quote! {
dioxus_core::Element::Ok({
let vnode = quote! {
#[doc(hidden)] // vscode please stop showing these in symbol search
const ___TEMPLATE_NAME: &str = {
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!(), ':', #index)
};
#[cfg(not(debug_assertions))]
{
#[doc(hidden)] // vscode please stop showing these in symbol search
static ___TEMPLATE: dioxus_core::Template = dioxus_core::Template {
name: {
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!(), ':', #index)
},
name: ___TEMPLATE_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
}
#[cfg(debug_assertions)]
{
// The key is important here - we're creating a new GlobalSignal each call to this
// But the key is what's keeping it stable
let __template = GlobalSignal::with_key(
|| #hot_reload_mapping,
___TEMPLATE_NAME
);
__template.maybe_with_rt(|__template_read| {
let mut __dynamic_literal_pool = dioxus_core::internal::DynamicLiteralPool::new(
vec![ #( #dynamic_text.to_string() ),* ],
);
let mut __dynamic_value_pool = dioxus_core::internal::DynamicValuePool::new(
vec![ #( #dynamic_nodes ),* ],
vec![ #( #dyn_attr_printer ),* ],
__dynamic_literal_pool
);
__dynamic_value_pool.render_with(__template_read)
})
}
};
tokens.append_all(quote! {
dioxus_core::Element::Ok({
#diagnostics
{
// 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
}
#vnode
})
});
}
@ -207,12 +214,13 @@ impl TemplateBody {
template_idx: DynIdx::default(),
node_paths: Vec::new(),
attr_paths: Vec::new(),
current_path: Vec::new(),
dynamic_text_segments: Vec::new(),
diagnostics: Diagnostics::new(),
};
// Assign paths to all nodes in the template
body.assign_paths_inner(&nodes);
body.validate_key();
// And then save the roots
body.roots = nodes;
@ -220,118 +228,42 @@ impl TemplateBody {
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> {
pub(crate) fn implicit_key(&self) -> Option<&AttributeValue> {
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())
}
Some(BodyNode::Element(el)) => el.key(),
Some(BodyNode::Component(comp)) => comp.get_key(),
_ => None,
}
}
/// 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.implicit_key();
if let Some(attr) = key {
let diagnostic = match &attr {
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}\"")
} else {
return;
}
}
_ => attr
.span()
.error("Key must be in the form of a formatted string like `key: \"{value}\""),
};
self.diagnostics.push(diagnostic);
}
}
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) {
@ -356,4 +288,71 @@ impl TemplateBody {
pub fn dynamic_nodes(&self) -> impl DoubleEndedIterator<Item = &BodyNode> {
self.node_paths.iter().map(|path| self.get_dyn_node(path))
}
fn quote_roots(&self) -> impl Iterator<Item = TokenStream2> + '_ {
self.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 } }
}
_ => {
let id = node.get_dyn_idx();
quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
}
})
}
/// Iterate through the literal component properties of this rsx call in depth-first order
pub(crate) fn literal_component_properties(&self) -> impl Iterator<Item = &HotLiteral> + '_ {
self.dynamic_nodes()
.filter_map(|node| {
if let BodyNode::Component(component) = node {
Some(component)
} else {
None
}
})
.flat_map(|component| {
component.fields.iter().filter_map(|field| {
if let AttributeValue::AttrLiteral(literal) = &field.value {
Some(literal)
} else {
None
}
})
})
}
fn hot_reload_mapping(&self, name: impl ToTokens) -> TokenStream2 {
let key = if let Some(AttributeValue::AttrLiteral(HotLiteral::Fmted(key))) =
self.implicit_key()
{
quote! { Some(#key) }
} else {
quote! { None }
};
let roots = self.quote_roots();
let dynamic_nodes = self.dynamic_nodes().map(|node| {
let id = node.get_dyn_idx();
quote! { dioxus_core::internal::HotReloadDynamicNode::Dynamic(#id) }
});
let dyn_attr_printer = self.dynamic_attributes().map(|attr| {
let id = attr.get_dyn_idx();
quote! { dioxus_core::internal::HotReloadDynamicAttribute::Dynamic(#id) }
});
let component_values = self
.literal_component_properties()
.map(|literal| literal.quote_as_hot_reload_literal());
quote! {
dioxus_core::internal::HotReloadedTemplate::new(
#name,
#key,
vec![ #( #dynamic_nodes ),* ],
vec![ #( #dyn_attr_printer ),* ],
vec![ #( #component_values ),* ],
&[ #( #roots ),* ],
)
}
}
}

View file

@ -1,11 +1,7 @@
#[cfg(feature = "hot_reload")]
use dioxus_core::TemplateNode;
use crate::{
literal::{HotLiteral, HotLiteralType},
location::DynIdx,
IfmtInput,
};
use crate::{literal::HotLiteral, location::DynIdx, HotReloadFormattedSegment, IfmtInput};
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::ToTokens;
use quote::{quote, TokenStreamExt};
@ -17,8 +13,7 @@ use syn::{
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub struct TextNode {
pub input: IfmtInput,
pub hr_idx: DynIdx,
pub input: HotReloadFormattedSegment,
pub dyn_idx: DynIdx,
}
@ -26,7 +21,6 @@ impl Parse for TextNode {
fn parse(input: ParseStream) -> Result<Self> {
Ok(Self {
input: input.parse()?,
hr_idx: DynIdx::default(),
dyn_idx: DynIdx::default(),
})
}
@ -44,10 +38,7 @@ impl ToTokens for TextNode {
// 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()),
};
let as_lit = HotLiteral::Fmted(txt.clone());
tokens.append_all(quote! {
dioxus_core::DynamicNode::Text(dioxus_core::VText::new( #as_lit ))
@ -63,9 +54,8 @@ impl TextNode {
segments: vec![],
};
Self {
input: ifmt,
input: ifmt.into(),
dyn_idx: Default::default(),
hr_idx: Default::default(),
}
}

View file

@ -11,7 +11,7 @@ use syn::{
Ident,
};
/// interns a object into a static object, resusing the value if it already exists
/// interns a object into a static object, reusing 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>>,

View file

@ -1,9 +1,17 @@
#![allow(unused)]
use dioxus_core::{prelude::Template, VNode};
use std::collections::HashMap;
use dioxus_core::{
internal::{
FmtSegment, FmtedSegments, HotReloadAttributeValue, HotReloadDynamicAttribute,
HotReloadDynamicNode, HotReloadLiteral, HotReloadedTemplate, NamedAttribute,
},
prelude::{Template, TemplateNode},
TemplateAttribute, VNode,
};
use dioxus_rsx::{
hot_reload::{diff_rsx, ChangedRsx},
hotreload::HotReloadedTemplate,
hot_reload::{self, diff_rsx, ChangedRsx, HotReloadResult},
CallBody, HotReloadingContext,
};
use proc_macro2::TokenStream;
@ -36,38 +44,34 @@ impl HotReloadingContext for Mock {
}
}
fn boilerplate(old: TokenStream, new: TokenStream) -> Option<Vec<Template>> {
fn hot_reload_from_tokens(
old: TokenStream,
new: TokenStream,
) -> Option<HashMap<usize, HotReloadedTemplate>> {
let old: CallBody = syn::parse2(old).unwrap();
let new: CallBody = syn::parse2(new).unwrap();
let location = "file:line:col:0";
hotreload_callbody::<Mock>(&old, &new, location)
hotreload_callbody::<Mock>(&old, &new)
}
fn can_hotreload(old: TokenStream, new: TokenStream) -> Option<HotReloadedTemplate> {
let old: CallBody = syn::parse2(old).unwrap();
let new: CallBody = syn::parse2(new).unwrap();
let location = "file:line:col:0";
let results = HotReloadedTemplate::new::<Mock>(&old, &new, location, Default::default())?;
Some(results)
fn can_hotreload(old: TokenStream, new: TokenStream) -> bool {
hot_reload_from_tokens(old, new).is_some()
}
fn hotreload_callbody<Ctx: HotReloadingContext>(
old: &CallBody,
new: &CallBody,
location: &'static str,
) -> Option<Vec<Template>> {
let results = HotReloadedTemplate::new::<Ctx>(old, new, location, Default::default())?;
) -> Option<HashMap<usize, HotReloadedTemplate>> {
let results = HotReloadResult::new::<Ctx>(&old.body, &new.body, Default::default())?;
Some(results.templates)
}
fn callbody_to_template<Ctx: HotReloadingContext>(
old: &CallBody,
location: &'static str,
) -> Option<Template> {
let results = HotReloadedTemplate::new::<Ctx>(old, old, location, Default::default())?;
Some(*results.templates.first().unwrap())
) -> Option<HotReloadedTemplate> {
let mut results = HotReloadResult::new::<Ctx>(&old.body, &old.body, Default::default())?;
Some(results.templates.remove(&0).unwrap())
}
fn base_stream() -> TokenStream {
@ -120,8 +124,8 @@ fn simple_for_loop() {
let new_valid: CallBody = syn::parse2(new_valid).unwrap();
let new_invalid: CallBody = syn::parse2(new_invalid).unwrap();
assert!(hotreload_callbody::<Mock>(&old, &new_valid, location).is_some());
assert!(hotreload_callbody::<Mock>(&old, &new_invalid, location).is_none());
assert!(hotreload_callbody::<Mock>(&old, &new_valid).is_some());
assert!(hotreload_callbody::<Mock>(&old, &new_invalid).is_none());
}
#[test]
@ -140,23 +144,234 @@ fn valid_reorder() {
}
};
let location = "file:line:col:0";
let new: CallBody = syn::parse2(new_valid).unwrap();
let valid = hotreload_callbody::<Mock>(&old, &new, location);
let valid = hotreload_callbody::<Mock>(&old, &new);
assert!(valid.is_some());
let templates = valid.unwrap();
// Currently we return all the templates, even if they didn't change
assert_eq!(templates.len(), 3);
let template = &templates[2];
let template = &templates[&0];
// It's an inversion, so we should get them in reverse
assert_eq!(template.node_paths, &[&[0, 1], &[0, 0]]);
assert_eq!(
template.roots,
&[TemplateNode::Element {
tag: "div",
namespace: None,
attrs: &[],
children: &[
TemplateNode::Dynamic { id: 0 },
TemplateNode::Dynamic { id: 1 }
]
}]
);
assert_eq!(
template.dynamic_nodes,
&[
HotReloadDynamicNode::Dynamic(1),
HotReloadDynamicNode::Dynamic(0)
]
);
}
// And the byte index should be the original template
assert_eq!(template.name, "file:line:col:0");
#[test]
fn valid_new_node() {
// Adding a new dynamic node should be hot reloadable as long as the text was present in the old version
// of the rsx block
let old = quote! {
div {
for item in vec![1, 2, 3] {
div { "item is {item}" }
}
}
};
let new = quote! {
div {
for item in vec![1, 2, 3] {
div { "item is {item}" }
div { "item is also {item}" }
}
}
};
let templates = hot_reload_from_tokens(old, new).unwrap();
// Currently we return all the templates, even if they didn't change
assert_eq!(templates.len(), 2);
let template = &templates[&1];
// The new dynamic node should be created from the formatted segments pool
assert_eq!(
template.dynamic_nodes,
&[
HotReloadDynamicNode::Formatted(FmtedSegments::new(vec![
FmtSegment::Literal { value: "item is " },
FmtSegment::Dynamic { id: 0 }
],)),
HotReloadDynamicNode::Formatted(FmtedSegments::new(vec![
FmtSegment::Literal {
value: "item is also "
},
FmtSegment::Dynamic { id: 0 }
],)),
]
);
}
#[test]
fn valid_new_dynamic_attribute() {
// Adding a new dynamic attribute should be hot reloadable as long as the text was present in the old version
// of the rsx block
let old = quote! {
div {
for item in vec![1, 2, 3] {
div {
class: "item is {item}"
}
}
}
};
let new = quote! {
div {
for item in vec![1, 2, 3] {
div {
class: "item is {item}"
}
div {
class: "item is also {item}"
}
}
}
};
let templates = hot_reload_from_tokens(old, new).unwrap();
// Currently we return all the templates, even if they didn't change
assert_eq!(templates.len(), 2);
let template = &templates[&1];
// We should have a new dynamic attribute
assert_eq!(
template.roots,
&[
TemplateNode::Element {
tag: "div",
namespace: None,
attrs: &[TemplateAttribute::Dynamic { id: 0 }],
children: &[]
},
TemplateNode::Element {
tag: "div",
namespace: None,
attrs: &[TemplateAttribute::Dynamic { id: 1 }],
children: &[]
}
]
);
// The new dynamic attribute should be created from the formatted segments pool
assert_eq!(
template.dynamic_attributes,
&[
HotReloadDynamicAttribute::Named(NamedAttribute::new(
"class",
None,
HotReloadAttributeValue::Literal(HotReloadLiteral::Fmted(FmtedSegments::new(
vec![
FmtSegment::Literal { value: "item is " },
FmtSegment::Dynamic { id: 0 }
],
)))
)),
HotReloadDynamicAttribute::Named(NamedAttribute::new(
"class",
None,
HotReloadAttributeValue::Literal(HotReloadLiteral::Fmted(FmtedSegments::new(
vec![
FmtSegment::Literal {
value: "item is also "
},
FmtSegment::Dynamic { id: 0 }
],
)))
)),
]
);
}
#[test]
fn valid_move_dynamic_segment_between_nodes() {
// Hot reloading should let you move around a dynamic formatted segment between nodes
let old = quote! {
div {
for item in vec![1, 2, 3] {
div {
class: "item is {item}"
}
}
}
};
let new = quote! {
div {
for item in vec![1, 2, 3] {
"item is {item}"
}
}
};
let templates = hot_reload_from_tokens(old, new).unwrap();
// Currently we return all the templates, even if they didn't change
assert_eq!(templates.len(), 2);
let template = &templates[&1];
// We should have a new dynamic node and no attributes
assert_eq!(template.roots, &[TemplateNode::Dynamic { id: 0 }]);
// The new dynamic node should be created from the formatted segments pool
assert_eq!(
template.dynamic_nodes,
&[HotReloadDynamicNode::Formatted(FmtedSegments::new(vec![
FmtSegment::Literal { value: "item is " },
FmtSegment::Dynamic { id: 0 }
])),]
);
}
#[test]
fn valid_keys() {
let a = quote! {
div {
key: "{value}",
}
};
// we can clone dynamic nodes to hot reload them
let b = quote! {
div {
key: "{value}-1234",
}
};
let hot_reload = hot_reload_from_tokens(a, b).unwrap();
assert_eq!(hot_reload.len(), 1);
let template = &hot_reload[&0];
assert_eq!(
template.key,
Some(FmtedSegments::new(vec![
FmtSegment::Dynamic { id: 0 },
FmtSegment::Literal { value: "-1234" }
]))
);
}
#[test]
@ -227,63 +442,32 @@ fn invalid_cases() {
syn::parse2(new_invalid_new_dynamic_internal).unwrap();
let new_invalid_added: CallBody = syn::parse2(new_invalid_added).unwrap();
assert!(hotreload_callbody::<Mock>(&old, &new_invalid, location).is_none());
assert!(
hotreload_callbody::<Mock>(&old, &new_invalid_new_dynamic_internal, location).is_none()
);
assert!(hotreload_callbody::<Mock>(&old, &new_invalid).is_none());
assert!(hotreload_callbody::<Mock>(&old, &new_invalid_new_dynamic_internal).is_none());
let removed = hotreload_callbody::<Mock>(&old, &new_valid_removed, location);
assert!(removed.is_some());
let templates = removed.unwrap();
let templates = hotreload_callbody::<Mock>(&old, &new_valid_removed).unwrap();
// we don't get the removed template back
assert_eq!(templates.len(), 2);
let template = &templates[1];
let template = &templates.get(&0).unwrap();
// We just completely removed the dynamic node, so it should be a "dud" path and then the placement
assert_eq!(template.node_paths, &[&[], &[0u8, 0] as &[u8]]);
assert_eq!(
template.roots,
&[TemplateNode::Element {
tag: "div",
namespace: None,
attrs: &[],
children: &[TemplateNode::Dynamic { id: 0 }]
}]
);
assert_eq!(template.dynamic_nodes, &[HotReloadDynamicNode::Dynamic(1)]);
// Adding a new dynamic node should not be hot reloadable
let added = hotreload_callbody::<Mock>(&old, &new_invalid_added, location);
let added = hotreload_callbody::<Mock>(&old, &new_invalid_added);
assert!(added.is_none());
}
#[test]
fn new_names() {
let old = quote! {
div {
for item in vec![1, 2, 3] {
div { "asasddasdasd" }
div { "123" }
}
}
};
// Same order, just different contents
let new_valid_internal = quote! {
div {
for item in vec![1, 2, 3] {
div { "asasddasdasd" }
div { "456" }
}
}
};
let templates = boilerplate(old, new_valid_internal).unwrap();
// Getting back all the templates even though some might not have changed
// This is currently just a symptom of us not checking if anything has changed, but has no bearing
// on output really.
assert_eq!(templates.len(), 2);
// The ordering is going to be inverse since its a depth-first traversal
let external = &templates[1];
assert_eq!(external.name, "file:line:col:0");
let internal = &templates[0];
assert_eq!(internal.name, "file:line:col:1");
}
#[test]
fn attributes_reload() {
let old = quote! {
@ -303,7 +487,7 @@ fn attributes_reload() {
}
};
let templates = boilerplate(old, new_valid_internal).unwrap();
let templates = hot_reload_from_tokens(old, new_valid_internal).unwrap();
dbg!(templates);
}
@ -377,13 +561,12 @@ fn diffs_complex() {
let old: CallBody = syn::parse2(old).unwrap();
let new: CallBody = syn::parse2(new).unwrap();
let location = "file:line:col:0";
let templates = hotreload_callbody::<Mock>(&old, &new, location).unwrap();
let templates = hotreload_callbody::<Mock>(&old, &new).unwrap();
}
#[test]
fn remove_node() {
let changed = boilerplate(
let valid = hot_reload_from_tokens(
quote! {
svg {
Comp {}
@ -398,12 +581,12 @@ fn remove_node() {
)
.unwrap();
dbg!(changed);
dbg!(valid);
}
#[test]
fn if_chains() {
let changed = boilerplate(
let valid = hot_reload_from_tokens(
quote! {
if cond {
"foo"
@ -417,7 +600,7 @@ fn if_chains() {
)
.unwrap();
let very_complex_chain = boilerplate(
let very_complex_chain = hot_reload_from_tokens(
quote! {
if cond {
if second_cond {
@ -448,7 +631,7 @@ fn if_chains() {
#[test]
fn component_bodies() {
let changed = boilerplate(
let valid = can_hotreload(
quote! {
Comp {
"foo"
@ -459,16 +642,36 @@ fn component_bodies() {
"baz"
}
},
)
.unwrap();
);
dbg!(changed);
assert!(valid);
}
// We currently don't track aliasing which means we can't allow dynamic nodes/formatted segments to be moved between scopes
#[test]
fn moving_between_scopes() {
let valid = can_hotreload(
quote! {
for x in 0..10 {
for y in 0..10 {
div { "x is {x}" }
}
}
},
quote! {
for x in 0..10 {
div { "x is {x}" }
}
},
);
assert!(!valid);
}
/// Everything reloads!
#[test]
fn kitch_sink_of_reloadability() {
let changed = boilerplate(
let valid = hot_reload_from_tokens(
quote! {
div {
for i in 0..10 {
@ -497,14 +700,14 @@ fn kitch_sink_of_reloadability() {
)
.unwrap();
dbg!(changed);
dbg!(valid);
}
/// Moving nodes inbetween multiple rsx! calls currently doesn't work
/// Sad. Needs changes to core to work, and is technically flawed?
#[test]
fn entire_kitchen_sink() {
let changed = boilerplate(
let valid = hot_reload_from_tokens(
quote! {
div {
for i in 0..10 {
@ -534,12 +737,12 @@ fn entire_kitchen_sink() {
},
);
assert!(changed.is_none());
assert!(valid.is_none());
}
#[test]
fn tokenstreams_and_locations() {
let changed = boilerplate(
let valid = hot_reload_from_tokens(
quote! {
div { "hhi" }
div {
@ -589,12 +792,12 @@ fn tokenstreams_and_locations() {
},
);
dbg!(changed);
dbg!(valid);
}
#[test]
fn ide_testcase() {
let changed = boilerplate(
let valid = hot_reload_from_tokens(
quote! {
div {
div { "hi!!!123 in!stant relo123a1123dasasdasdasdasd" }
@ -613,7 +816,7 @@ fn ide_testcase() {
},
);
dbg!(changed);
dbg!(valid);
}
#[test]
@ -635,7 +838,7 @@ fn assigns_ids() {
#[test]
fn simple_start() {
let changed = boilerplate(
let valid = can_hotreload(
//
quote! {
div {
@ -653,12 +856,12 @@ fn simple_start() {
},
);
dbg!(changed.unwrap());
assert!(valid);
}
#[test]
fn complex_cases() {
let changed = can_hotreload(
let valid = can_hotreload(
quote! {
div {
class: "Some {one}",
@ -675,12 +878,12 @@ fn complex_cases() {
},
);
dbg!(changed.unwrap());
assert!(valid);
}
#[test]
fn attribute_cases() {
let changed = can_hotreload(
let valid = can_hotreload(
quote! {
div {
class: "Some {one}",
@ -695,52 +898,59 @@ fn attribute_cases() {
}
},
);
dbg!(changed.unwrap());
assert!(valid);
let changed = can_hotreload(
let valid = can_hotreload(
//
quote! { div { class: 123 } },
quote! { div { class: 456 } },
);
dbg!(changed.unwrap());
assert!(valid);
let changed = can_hotreload(
let valid = can_hotreload(
//
quote! { div { class: 123.0 } },
quote! { div { class: 456.0 } },
);
dbg!(changed.unwrap());
assert!(valid);
let changed = can_hotreload(
let valid = can_hotreload(
//
quote! { div { class: "asd {123}", } },
quote! { div { class: "def", } },
);
dbg!(changed.unwrap());
assert!(valid);
}
#[test]
fn text_node_cases() {
let changed = can_hotreload(
let valid = can_hotreload(
//
quote! { div { "hello {world}" } },
quote! { div { "world {world}" } },
);
dbg!(changed.unwrap());
assert!(valid);
let changed = can_hotreload(
let valid = can_hotreload(
//
quote! { div { "hello {world}" } },
quote! { div { "world" } },
);
dbg!(changed.unwrap());
assert!(valid);
let changed = can_hotreload(
let valid = can_hotreload(
//
quote! { div { "hello {world}" } },
quote! { div { "world {world} {world}" } },
);
assert!(valid);
let valid = can_hotreload(
//
quote! { div { "hello" } },
quote! { div { "world {world}" } },
);
assert!(changed.is_none());
assert!(!valid);
}
#[test]
@ -759,8 +969,8 @@ fn simple_carry() {
"thing {hij}"
};
let changed = can_hotreload(a, b);
dbg!(changed.unwrap());
let valid = can_hotreload(a, b);
assert!(valid);
}
#[test]
@ -778,8 +988,8 @@ fn complex_carry_text() {
"thing {hij}"
};
let changed = can_hotreload(a, b);
dbg!(changed.unwrap());
let valid = can_hotreload(a, b);
assert!(valid);
}
#[test]
@ -807,8 +1017,8 @@ fn complex_carry() {
}
};
let changed = can_hotreload(a, b);
dbg!(changed.unwrap());
let valid = can_hotreload(a, b);
assert!(valid);
}
#[test]
@ -832,8 +1042,8 @@ fn component_with_lits() {
}
};
let changed = can_hotreload(a, b);
dbg!(changed.unwrap());
let valid = can_hotreload(a, b);
assert!(valid);
}
#[test]
@ -859,6 +1069,59 @@ fn component_with_handlers() {
}
};
let changed = can_hotreload(a, b);
dbg!(changed.unwrap());
let valid = can_hotreload(a, b);
assert!(valid);
}
#[test]
fn duplicating_dynamic_nodes() {
let a = quote! {
div {
{some_expr}
}
};
// we can clone dynamic nodes to hot reload them
let b = quote! {
div {
{some_expr}
{some_expr}
}
};
let valid = can_hotreload(a, b);
assert!(valid);
}
#[test]
fn duplicating_dynamic_attributes() {
let a = quote! {
div {
width: value,
}
};
// we can clone dynamic nodes to hot reload them
let b = quote! {
div {
width: value,
height: value,
}
};
let valid = can_hotreload(a, b);
assert!(valid);
}
// We should be able to fill in empty nodes
#[test]
fn valid_fill_empty() {
let valid = can_hotreload(
quote! {},
quote! {
div { "x is 123" }
},
);
assert!(valid);
}

View file

@ -1,4 +1,4 @@
use dioxus_rsx::{hot_reload::Empty, CallBody};
use dioxus_rsx::CallBody;
use quote::ToTokens;
use dioxus_rsx::PrettyUnparse;
@ -40,9 +40,6 @@ fn callbody_ctx() {
let cb: CallBody = syn::parse2(item).unwrap();
dbg!(cb.template_idx.get());
dbg!(cb.ifmt_idx.get());
let _template = cb.body.to_template::<Empty>();
}
#[test]

View file

@ -126,7 +126,7 @@ impl StringCache {
let mut cur_path = vec![];
for (root_idx, root) in template.template.get().roots.iter().enumerate() {
for (root_idx, root) in template.template.roots.iter().enumerate() {
from_template_recursive(root, &mut cur_path, root_idx, true, &mut chain)?;
}

View file

@ -108,7 +108,7 @@ impl Renderer {
) -> std::fmt::Result {
let entry = self
.template_cache
.entry(template.template.get().id())
.entry(template.template.id())
.or_insert_with(move || Arc::new(StringCache::from_template(template).unwrap()))
.clone();

View file

@ -245,7 +245,7 @@ impl WebsysDom {
ids: &mut Vec<u32>,
to_mount: &mut Vec<ElementId>,
) -> Result<(), RehydrationError> {
for (i, root) in vnode.template.get().roots.iter().enumerate() {
for (i, root) in vnode.template.roots.iter().enumerate() {
self.rehydrate_template_node(
dom,
vnode,