use super::{apply_swap, parse_file_templates, registry, EscalationReason, SwapOutcome, Template};
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ReloadReport {
pub applied: usize,
pub unchanged: usize,
pub unknown: usize,
pub escalations: Vec<EscalationReason>,
pub parse_error: Option<String>,
}
impl ReloadReport {
pub fn hot_swapped(&self) -> bool {
self.applied > 0 && !self.needs_restart() && self.parse_error.is_none()
}
pub fn needs_restart(&self) -> bool {
!self.escalations.is_empty() || self.unknown > 0
}
pub fn ignored(&self) -> bool {
self.parse_error.is_some()
}
}
pub fn apply_reload(file: &str, src: &str) -> ReloadReport {
let mut report = ReloadReport::default();
let scanned = match parse_file_templates(src, file) {
Ok(v) => v,
Err(e) => {
report.parse_error = Some(e.to_string());
return report;
}
};
for site in scanned {
match registry::find_running(file, site.key.line) {
None => report.unknown += 1,
Some(running) => {
let candidate = Template::new(running.key.clone(), site.root);
match apply_swap(candidate) {
SwapOutcome::Applied => report.applied += 1,
SwapOutcome::Unchanged => report.unchanged += 1,
SwapOutcome::Escalate(reason) => report.escalations.push(reason),
SwapOutcome::UnknownSite => report.unknown += 1,
}
}
}
}
report
}
#[cfg(test)]
mod tests {
use super::*;
use crate::template::{registry, PropValue, StaticValue, Template, TemplateKey, TemplateNode};
fn baseline(file: &str, line: u32, col: u32, spacing: PropValue) -> Template {
let mut root = TemplateNode::new("Column");
root.props.push(("spacing".into(), spacing));
Template::new(TemplateKey::new(file, line, col), root)
}
#[test]
fn safe_edit_swaps_despite_different_path_form_and_column() {
registry::register(baseline(
"reload_pkg/src/app.rs",
3,
17,
PropValue::Static(StaticValue::Float(4.0)),
));
let src = "// l1\n// l2\nfn v() { let _ = view! { Column { spacing: 40.0 } }; }\n";
let report = apply_reload("/Users/x/reload_pkg/src/app.rs", src);
assert_eq!(report.applied, 1, "should hot-swap the matched site: {report:?}");
assert!(report.hot_swapped());
assert!(!report.needs_restart());
let now = registry::get(&TemplateKey::new("reload_pkg/src/app.rs", 3, 17)).unwrap();
assert_eq!(now.root.props[0].1, PropValue::Static(StaticValue::Float(40.0)));
}
#[test]
fn a_logic_change_reports_needs_restart() {
registry::register(baseline(
"reload_pkg/src/b.rs",
1,
9,
PropValue::Static(StaticValue::Float(4.0)),
));
let src = "fn v() { let _ = view! { Column { spacing: g } }; }\n";
let report = apply_reload("reload_pkg/src/b.rs", src);
assert_eq!(report.applied, 0);
assert!(report.needs_restart(), "a new hole must force a restart: {report:?}");
}
#[test]
fn a_new_view_site_is_unknown_and_needs_restart() {
let src = "fn v() { let _ = view! { Row { } }; }\n";
let report = apply_reload("reload_pkg/src/brand_new.rs", src);
assert_eq!(report.unknown, 1);
assert!(report.needs_restart());
}
#[test]
fn an_unparseable_edit_is_ignored_not_restarted() {
let src = "fn v() { let _ = view! { Column { : : : } }; }\n";
let report = apply_reload("reload_pkg/src/c.rs", src);
assert!(report.ignored());
assert!(!report.needs_restart(), "mid-edit garbage should be ignored, not restarted");
}
}