use std::collections::BTreeMap;
use super::{PropValue, Template, TemplateNode};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TemplateDiff {
Unchanged,
Swappable,
Escalate(EscalationReason),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EscalationReason {
KeyMismatch,
HoleCountChanged { old: usize, new: usize },
HoleSlotRetargeted { index: usize },
}
pub fn diff(old: &Template, new: &Template) -> TemplateDiff {
if old.key != new.key {
return TemplateDiff::Escalate(EscalationReason::KeyMismatch);
}
if old.root == new.root {
return TemplateDiff::Unchanged;
}
if old.hole_count != new.hole_count {
return TemplateDiff::Escalate(EscalationReason::HoleCountChanged {
old: old.hole_count,
new: new.hole_count,
});
}
let old_slots = hole_slots(old);
let new_slots = hole_slots(new);
for (index, old_site) in &old_slots {
if new_slots.get(index) != Some(old_site) {
return TemplateDiff::Escalate(EscalationReason::HoleSlotRetargeted { index: *index });
}
}
TemplateDiff::Swappable
}
fn hole_slots(t: &Template) -> BTreeMap<usize, (String, String)> {
let mut slots = BTreeMap::new();
collect_slots(&t.root, &mut slots);
slots
}
fn collect_slots(node: &TemplateNode, slots: &mut BTreeMap<usize, (String, String)>) {
for (pos, value) in node.args.iter().enumerate() {
if let PropValue::Hole(i) = value {
slots.insert(*i, (node.widget.clone(), format!("$arg{pos}")));
}
}
for (prop, value) in &node.props {
if let PropValue::Hole(i) = value {
slots.insert(*i, (node.widget.clone(), prop.clone()));
}
}
for child in &node.children {
collect_slots(child, slots);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::template::{StaticValue, TemplateKey, TemplateNode};
fn key() -> TemplateKey {
TemplateKey::new("src/app.rs", 10, 5)
}
fn t(root: TemplateNode) -> Template {
Template::new(key(), root)
}
#[test]
fn identical_templates_are_unchanged() {
let a = t(TemplateNode::new("Column").with_static("spacing", StaticValue::Float(8.0)));
let b = t(TemplateNode::new("Column").with_static("spacing", StaticValue::Float(8.0)));
assert_eq!(diff(&a, &b), TemplateDiff::Unchanged);
}
#[test]
fn changing_a_static_literal_is_swappable() {
let a = t(TemplateNode::new("Text").with_static("content", StaticValue::Str("Save".into())));
let b = t(TemplateNode::new("Text").with_static("content", StaticValue::Str("Store".into())));
assert_eq!(diff(&a, &b), TemplateDiff::Swappable);
}
#[test]
fn wrapping_in_a_static_container_preserves_hole_sites_and_is_swappable() {
let a = t(TemplateNode::new("Column").with_child(TemplateNode::new("Text").with_hole("content", 0)));
let b = t(TemplateNode::new("Column")
.with_child(TemplateNode::new("Container").with_child(TemplateNode::new("Text").with_hole("content", 0))));
assert_eq!(diff(&a, &b), TemplateDiff::Swappable);
}
#[test]
fn adding_a_hole_escalates_on_count() {
let a = t(TemplateNode::new("Column").with_hole("spacing", 0));
let b = t(TemplateNode::new("Column")
.with_hole("spacing", 0)
.with_child(TemplateNode::new("Text").with_hole("content", 1)));
assert_eq!(
diff(&a, &b),
TemplateDiff::Escalate(EscalationReason::HoleCountChanged { old: 1, new: 2 })
);
}
#[test]
fn removing_a_hole_escalates_on_count() {
let a = t(TemplateNode::new("Column").with_hole("spacing", 0).with_hole("cross", 1));
let b = t(TemplateNode::new("Column").with_hole("spacing", 0));
assert_eq!(
diff(&a, &b),
TemplateDiff::Escalate(EscalationReason::HoleCountChanged { old: 2, new: 1 })
);
}
#[test]
fn retargeting_a_hole_to_a_different_prop_escalates_even_at_same_count() {
let a = t(TemplateNode::new("Row")
.with_child(TemplateNode::new("Column").with_hole("spacing", 0))
.with_child(TemplateNode::new("Text").with_static("content", StaticValue::Str("x".into()))));
let b = t(TemplateNode::new("Row")
.with_child(TemplateNode::new("Column").with_static("spacing", StaticValue::Float(5.0)))
.with_child(TemplateNode::new("Text").with_hole("content", 0)));
assert_eq!(
diff(&a, &b),
TemplateDiff::Escalate(EscalationReason::HoleSlotRetargeted { index: 0 })
);
}
#[test]
fn same_count_same_sites_but_reordered_static_neighbours_is_swappable() {
let a = t(TemplateNode::new("Column").with_child(TemplateNode::new("Text").with_hole("content", 0)));
let b = t(TemplateNode::new("Column")
.with_child(TemplateNode::new("Text").with_static("content", StaticValue::Str("header".into())))
.with_child(TemplateNode::new("Text").with_hole("content", 0)));
assert_eq!(diff(&a, &b), TemplateDiff::Swappable);
}
#[test]
fn different_site_keys_are_a_caller_bug() {
let a = Template::new(TemplateKey::new("src/a.rs", 1, 1), TemplateNode::new("Column"));
let b = Template::new(TemplateKey::new("src/b.rs", 2, 2), TemplateNode::new("Column"));
assert_eq!(diff(&a, &b), TemplateDiff::Escalate(EscalationReason::KeyMismatch));
}
}