dioxus_rsx_hotreload/
collect.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//! Compare two files and find any rsx calls that have changed
//!
//! This is used to determine if a hotreload is needed.
//! We use a special syn visitor to find all the rsx! calls in the file and then compare them to see
//! if they are the same. This visitor will actually remove the rsx! calls and replace them with a
//! dummy rsx! call. The final file type is thus mutated in place, leaving the original file idents
//! in place. We then compare the two files to see if they are the same. We're able to differentiate
//! between rust code changes and rsx code changes with much less code than the previous manual diff
//! approach.

use syn::visit_mut::VisitMut;
use syn::{File, Macro};

pub struct ChangedRsx {
    /// The old macro - the original RSX from the original file
    pub old: Macro,

    /// The new macro
    pub new: Macro,
}

#[derive(Debug)]
pub enum ReloadableRustCode {
    Rsx { old: Macro, new: Macro },
}

/// Find any rsx calls in the given file and return a list of all the rsx calls that have changed.
///
/// Takes in the two files, clones them, removes the rsx! contents and prunes any doc comments.
/// Then it compares the two files to see if they are different - if they are, the code changed.
/// Otherwise, the code is the same and we can move on to handling the changed rsx
///
/// Returns `None` if the files are the same and `Some` if they are different
/// If there are no rsx! calls in the files, the vec will be empty.
pub fn diff_rsx(new: &File, old: &File) -> Option<Vec<ChangedRsx>> {
    // Make a clone of these files in place so we don't have to worry about mutating the original
    let mut old = old.clone();
    let mut new = new.clone();

    // Collect all the rsx! macros from the old file - modifying the files in place
    let old_macros = collect_from_file(&mut old);
    let new_macros = collect_from_file(&mut new);

    // If the number of rsx! macros is different, then it's not hotreloadable
    if old_macros.len() != new_macros.len() {
        return None;
    }

    // If the files are not the same, then it's not hotreloadable
    if old != new {
        return None;
    }

    Some(
        old_macros
            .into_iter()
            .zip(new_macros)
            .map(|(old, new)| ChangedRsx { old, new })
            .collect(),
    )
}

pub fn collect_from_file(file: &mut File) -> Vec<Macro> {
    struct MacroCollector(Vec<Macro>);
    impl VisitMut for MacroCollector {
        /// Take out the rsx! macros, leaving a default in their place
        fn visit_macro_mut(&mut self, dest: &mut syn::Macro) {
            let name = &dest.path.segments.last().map(|i| i.ident.to_string());
            if let Some("rsx" | "render") = name.as_deref() {
                let mut default: syn::Macro = syn::parse_quote! { rsx! {} };
                std::mem::swap(dest, &mut default);
                self.0.push(default)
            }
        }

        /// Ignore doc comments by swapping them out with a default
        fn visit_attribute_mut(&mut self, i: &mut syn::Attribute) {
            if i.path().is_ident("doc") {
                *i = syn::parse_quote! { #[doc = ""] };
            }
        }
    }

    let mut macros = MacroCollector(vec![]);
    macros.visit_file_mut(file);
    macros.0
}

#[test]
fn changing_files() {
    let old = r#"
use dioxus::prelude::*;

/// some comment
pub fn CoolChild() -> Element {
    let a = 123;

    rsx! {
        div {
            {some_expr()}
        }
    }
}"#;

    let new = r#"
use dioxus::prelude::*;

/// some comment
pub fn CoolChild() -> Element {
    rsx! {
        div {
            {some_expr()}
        }
    }
}"#;

    let same = r#"
use dioxus::prelude::*;

/// some comment!!!!!
pub fn CoolChild() -> Element {
    let a = 123;

    rsx! {
        div {
            {some_expr()}
        }
    }
}"#;

    let old = syn::parse_file(old).unwrap();
    let new = syn::parse_file(new).unwrap();
    let same = syn::parse_file(same).unwrap();

    assert!(
        diff_rsx(&old, &new).is_none(),
        "Files with different expressions should not be hotreloadable"
    );

    assert!(
        diff_rsx(&new, &new).is_some(),
        "The same file should be reloadable with itself"
    );

    assert!(
        diff_rsx(&old, &same).is_some(),
        "Files with changed comments should be hotreloadable"
    );
}