mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-22 04:03:04 +00:00
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:
parent
f3ca1484a1
commit
34bdcd15cf
40 changed files with 2288 additions and 1897 deletions
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 },
|
||||
]
|
||||
)
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
138
packages/rsx/src/assign_dyn_ids.rs
Normal file
138
packages/rsx/src/assign_dyn_ids.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
651
packages/rsx/src/hot_reload/diff.rs
Normal file
651
packages/rsx/src/hot_reload/diff.rs
Normal 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(())
|
||||
}
|
||||
}
|
157
packages/rsx/src/hot_reload/last_build_state.rs
Normal file
157
packages/rsx/src/hot_reload/last_build_state.rs
Normal 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))
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::*;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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<_>>(),
|
||||
),
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 ),* ],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>>,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)?;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue