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
150
151
152
153
154
155
156
157
158
159
160
161
162
extern crate proc_macro;

use anyhow::Result;
use camino::Utf8PathBuf;
use diff::Patches;
use node::LNode;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::{
    collections::HashMap,
    fs::File,
    io::Read,
    path::{Path, PathBuf},
    sync::Arc,
};
use syn::{
    spanned::Spanned,
    visit::{self, Visit},
    Macro,
};
use walkdir::WalkDir;

pub mod diff;
pub mod node;
pub mod parsing;

pub const HOT_RELOAD_JS: &str = include_str!("patch.js");

#[derive(Debug, Clone, Default)]
pub struct ViewMacros {
    // keyed by original location identifier
    views: Arc<RwLock<HashMap<Utf8PathBuf, Vec<MacroInvocation>>>>,
}

impl ViewMacros {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn update_from_paths<T: AsRef<Path>>(&self, paths: &[T]) -> Result<()> {
        let mut views = HashMap::new();

        for path in paths {
            for entry in WalkDir::new(path).into_iter().flatten() {
                if entry.file_type().is_file() {
                    let path: PathBuf = entry.path().into();
                    let path = Utf8PathBuf::try_from(path)?;
                    if path.extension() == Some("rs") || path.ends_with(".rs") {
                        let macros = Self::parse_file(&path)?;
                        let entry = views.entry(path.clone()).or_default();
                        *entry = macros;
                    }
                }
            }
        }

        *self.views.write() = views;

        Ok(())
    }

    pub fn parse_file(path: &Utf8PathBuf) -> Result<Vec<MacroInvocation>> {
        let mut file = File::open(path)?;
        let mut content = String::new();
        file.read_to_string(&mut content)?;
        let ast = syn::parse_file(&content)?;

        let mut visitor = ViewMacroVisitor::default();
        visitor.visit_file(&ast);
        let mut views = Vec::new();
        for view in visitor.views {
            let span = view.span();
            let id = span_to_stable_id(path, span);
            let mut tokens = view.tokens.clone().into_iter();
            tokens.next(); // cx
            tokens.next(); // ,
                           // TODO handle class = ...
            let rsx =
                syn_rsx::parse2(tokens.collect::<proc_macro2::TokenStream>())?;
            let template = LNode::parse_view(rsx)?;
            views.push(MacroInvocation { id, template })
        }
        Ok(views)
    }

    pub fn patch(&self, path: &Utf8PathBuf) -> Result<Option<Patches>> {
        let new_views = Self::parse_file(path)?;
        let mut lock = self.views.write();
        let diffs = match lock.get(path) {
            None => return Ok(None),
            Some(current_views) => {
                if current_views.len() == new_views.len() {
                    let mut diffs = Vec::new();
                    for (current_view, new_view) in
                        current_views.iter().zip(&new_views)
                    {
                        if current_view.id == new_view.id
                            && current_view.template != new_view.template
                        {
                            diffs.push((
                                current_view.id.clone(),
                                current_view.template.diff(&new_view.template),
                            ));
                        }
                    }
                    diffs
                } else {
                    return Ok(None);
                }
            }
        };

        // update the status to the new views
        lock.insert(path.clone(), new_views);

        Ok(Some(Patches(diffs)))
    }
}

#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MacroInvocation {
    id: String,
    template: LNode,
}

impl std::fmt::Debug for MacroInvocation {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("MacroInvocation")
            .field("id", &self.id)
            .finish()
    }
}

#[derive(Default, Debug)]
pub struct ViewMacroVisitor<'a> {
    views: Vec<&'a Macro>,
}

impl<'ast> Visit<'ast> for ViewMacroVisitor<'ast> {
    fn visit_macro(&mut self, node: &'ast Macro) {
        let ident = node.path.get_ident().map(|n| n.to_string());
        if ident == Some("view".to_string()) {
            self.views.push(node);
        }

        // Delegate to the default impl to visit any nested functions.
        visit::visit_macro(self, node);
    }
}

pub fn span_to_stable_id(
    path: impl AsRef<Path>,
    site: proc_macro2::Span,
) -> String {
    let file = path
        .as_ref()
        .to_str()
        .unwrap_or_default()
        .replace(['/', '\\'], "-");
    let start = site.start();
    format!("{}-{:?}", file, start.line)
}