rosace_widgets/template/
reload.rs1use super::{apply_swap, parse_file_templates, registry, EscalationReason, SwapOutcome, Template};
16
17#[derive(Debug, Clone, Default, PartialEq)]
19pub struct ReloadReport {
20 pub applied: usize,
22 pub unchanged: usize,
24 pub unknown: usize,
27 pub escalations: Vec<EscalationReason>,
29 pub parse_error: Option<String>,
31}
32
33impl ReloadReport {
34 pub fn hot_swapped(&self) -> bool {
36 self.applied > 0 && !self.needs_restart() && self.parse_error.is_none()
37 }
38 pub fn needs_restart(&self) -> bool {
41 !self.escalations.is_empty() || self.unknown > 0
42 }
43 pub fn ignored(&self) -> bool {
45 self.parse_error.is_some()
46 }
47}
48
49pub fn apply_reload(file: &str, src: &str) -> ReloadReport {
51 let mut report = ReloadReport::default();
52
53 let scanned = match parse_file_templates(src, file) {
54 Ok(v) => v,
55 Err(e) => {
56 report.parse_error = Some(e.to_string());
57 return report;
58 }
59 };
60
61 for site in scanned {
62 match registry::find_running(file, site.key.line) {
63 None => report.unknown += 1,
64 Some(running) => {
65 let candidate = Template::new(running.key.clone(), site.root);
68 match apply_swap(candidate) {
69 SwapOutcome::Applied => report.applied += 1,
70 SwapOutcome::Unchanged => report.unchanged += 1,
71 SwapOutcome::Escalate(reason) => report.escalations.push(reason),
72 SwapOutcome::UnknownSite => report.unknown += 1,
73 }
74 }
75 }
76 }
77
78 report
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use crate::template::{registry, PropValue, StaticValue, Template, TemplateKey, TemplateNode};
85
86 fn baseline(file: &str, line: u32, col: u32, spacing: PropValue) -> Template {
87 let mut root = TemplateNode::new("Column");
88 root.props.push(("spacing".into(), spacing));
89 Template::new(TemplateKey::new(file, line, col), root)
90 }
91
92 #[test]
93 fn safe_edit_swaps_despite_different_path_form_and_column() {
94 registry::register(baseline(
97 "reload_pkg/src/app.rs",
98 3,
99 17,
100 PropValue::Static(StaticValue::Float(4.0)),
101 ));
102
103 let src = "// l1\n// l2\nfn v() { let _ = view! { Column { spacing: 40.0 } }; }\n";
106 let report = apply_reload("/Users/x/reload_pkg/src/app.rs", src);
107
108 assert_eq!(report.applied, 1, "should hot-swap the matched site: {report:?}");
109 assert!(report.hot_swapped());
110 assert!(!report.needs_restart());
111 let now = registry::get(&TemplateKey::new("reload_pkg/src/app.rs", 3, 17)).unwrap();
113 assert_eq!(now.root.props[0].1, PropValue::Static(StaticValue::Float(40.0)));
114 }
115
116 #[test]
117 fn a_logic_change_reports_needs_restart() {
118 registry::register(baseline(
119 "reload_pkg/src/b.rs",
120 1,
121 9,
122 PropValue::Static(StaticValue::Float(4.0)),
123 ));
124 let src = "fn v() { let _ = view! { Column { spacing: g } }; }\n";
126 let report = apply_reload("reload_pkg/src/b.rs", src);
127 assert_eq!(report.applied, 0);
128 assert!(report.needs_restart(), "a new hole must force a restart: {report:?}");
129 }
130
131 #[test]
132 fn a_new_view_site_is_unknown_and_needs_restart() {
133 let src = "fn v() { let _ = view! { Row { } }; }\n";
135 let report = apply_reload("reload_pkg/src/brand_new.rs", src);
136 assert_eq!(report.unknown, 1);
137 assert!(report.needs_restart());
138 }
139
140 #[test]
141 fn an_unparseable_edit_is_ignored_not_restarted() {
142 let src = "fn v() { let _ = view! { Column { : : : } }; }\n";
143 let report = apply_reload("reload_pkg/src/c.rs", src);
144 assert!(report.ignored());
145 assert!(!report.needs_restart(), "mid-edit garbage should be ignored, not restarted");
146 }
147}