Skip to main content

dioxus_autofmt/
prettier_please.rs

1use dioxus_rsx::CallBody;
2use syn::{parse::Parser, visit_mut::VisitMut, Expr, File, Item, MacroDelimiter};
3
4use crate::{IndentOptions, Writer};
5
6impl Writer<'_> {
7    pub fn unparse_expr(&mut self, expr: &Expr) -> String {
8        unparse_expr(expr, self.raw_src, &self.out.indent)
9    }
10}
11
12// we use weird unicode alternatives to avoid conflicts with the actual rsx! macro
13const MARKER: &str = "𝕣𝕤𝕩";
14const MARKER_REPLACE: &str = "𝕣𝕤𝕩! {}";
15
16pub fn unparse_expr(expr: &Expr, src: &str, cfg: &IndentOptions) -> String {
17    struct ReplaceMacros<'a> {
18        src: &'a str,
19        formatted_stack: Vec<String>,
20        cfg: &'a IndentOptions,
21    }
22
23    impl VisitMut for ReplaceMacros<'_> {
24        fn visit_macro_mut(&mut self, i: &mut syn::Macro) {
25            // replace the macro with a block that roughly matches the macro
26            if let Some("rsx" | "render") = i
27                .path
28                .segments
29                .last()
30                .map(|i| i.ident.to_string())
31                .as_deref()
32            {
33                // format the macro in place
34                // we'll use information about the macro to replace it with another formatted block
35                // once we've written out the unparsed expr from prettyplease, we can replace
36                // this dummy block with the actual formatted block
37                let body = CallBody::parse_strict.parse2(i.tokens.clone()).unwrap();
38                let multiline = !Writer::is_short_rsx_call(&body.body.roots);
39                let mut formatted = {
40                    let mut writer = Writer::new(self.src, self.cfg.clone());
41                    _ = writer.write_body_nodes(&body.body.roots).ok();
42                    writer.consume()
43                }
44                .unwrap();
45
46                i.path = syn::parse_str(MARKER).unwrap();
47                i.tokens = Default::default();
48
49                // make sure to transform the delimiter to a brace so the marker can be found
50                // an alternative approach would be to use multiple different markers that are not
51                // sensitive to the delimiter.
52                i.delimiter = MacroDelimiter::Brace(Default::default());
53
54                // Push out the indent level of the formatted block if it's multiline
55                if multiline || formatted.contains('\n') {
56                    formatted = formatted
57                        .lines()
58                        .map(|line| {
59                            // Don't add indentation to blank lines (avoid trailing whitespace)
60                            if line.is_empty() {
61                                String::new()
62                            } else {
63                                format!("{}{line}", self.cfg.indent_str())
64                            }
65                        })
66                        .collect::<Vec<_>>()
67                        .join("\n");
68                }
69
70                // Save this formatted block for later, when we apply it to the original expr
71                self.formatted_stack.push(formatted)
72            }
73
74            syn::visit_mut::visit_macro_mut(self, i);
75        }
76    }
77
78    // Visit the expr and replace the macros with formatted blocks
79    let mut replacer = ReplaceMacros {
80        src,
81        cfg,
82        formatted_stack: vec![],
83    };
84
85    // builds the expression stack
86    let mut modified_expr = expr.clone();
87    replacer.visit_expr_mut(&mut modified_expr);
88
89    // now unparsed with the modified expression
90    let mut unparsed = unparse_inner(&modified_expr);
91
92    // now we can replace the macros with the formatted blocks
93    for fmted in replacer.formatted_stack.drain(..) {
94        let is_multiline = fmted.ends_with('}') || fmted.contains('\n');
95        let is_empty = fmted.trim().is_empty();
96
97        let mut out_fmt = String::from("rsx! {");
98        if is_multiline {
99            out_fmt.push('\n');
100        } else if !is_empty {
101            out_fmt.push(' ');
102        }
103
104        let mut whitespace = 0;
105
106        for line in unparsed.lines() {
107            if line.contains(MARKER) {
108                whitespace = line.matches(cfg.indent_str()).count();
109                break;
110            }
111        }
112
113        let mut lines = fmted.lines().enumerate().peekable();
114
115        while let Some((_idx, fmt_line)) = lines.next() {
116            // Push the indentation (but not for blank lines - avoid trailing whitespace)
117            if is_multiline && !fmt_line.is_empty() {
118                out_fmt.push_str(&cfg.indent_str().repeat(whitespace));
119            }
120
121            // Calculate delta between indentations - the block indentation is too much
122            out_fmt.push_str(fmt_line);
123
124            // Push a newline if there's another line
125            if lines.peek().is_some() {
126                out_fmt.push('\n');
127            }
128        }
129
130        if is_multiline {
131            out_fmt.push('\n');
132            out_fmt.push_str(&cfg.indent_str().repeat(whitespace));
133        } else if !is_empty {
134            out_fmt.push(' ');
135        }
136
137        // Replace the dioxus_autofmt_block__________ token with the formatted block
138        out_fmt.push('}');
139
140        unparsed = unparsed.replacen(MARKER_REPLACE, &out_fmt, 1);
141        continue;
142    }
143
144    // stylistic choice to trim whitespace around the expr
145    if unparsed.starts_with("{ ") && unparsed.ends_with(" }") {
146        let mut out_fmt = String::new();
147        out_fmt.push('{');
148        out_fmt.push_str(&unparsed[2..unparsed.len() - 2]);
149        out_fmt.push('}');
150        out_fmt
151    } else {
152        unparsed
153    }
154}
155
156/// Unparse an expression back into a string
157///
158/// This creates a new temporary file, parses the expression into it, and then formats the file.
159/// This is a bit of a hack, but dtonlay doesn't want to support this very simple usecase, forcing us to clone the expr
160pub fn unparse_inner(expr: &Expr) -> String {
161    let file = wrapped(expr);
162    let wrapped = prettyplease::unparse(&file);
163    unwrapped(wrapped)
164}
165
166// Split off the fn main and then cut the tabs off the front
167fn unwrapped(raw: String) -> String {
168    let mut o = raw
169        .strip_prefix("fn main() {\n")
170        .unwrap()
171        .strip_suffix("}\n")
172        .unwrap()
173        .lines()
174        .map(|line| line.strip_prefix("    ").unwrap_or_default()) // todo: set this to tab level
175        .collect::<Vec<_>>()
176        .join("\n");
177
178    // remove the semicolon
179    if o.ends_with(';') {
180        o.pop();
181    }
182
183    o
184}
185
186fn wrapped(expr: &Expr) -> File {
187    File {
188        shebang: None,
189        attrs: vec![],
190        items: vec![
191            //
192            Item::Verbatim(quote::quote! {
193                fn main() {
194                    #expr;
195                }
196            }),
197        ],
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use proc_macro2::TokenStream;
205
206    fn fmt_block_from_expr(raw: &str, tokens: TokenStream, cfg: IndentOptions) -> Option<String> {
207        let body = CallBody::parse_strict.parse2(tokens).unwrap();
208        let mut writer = Writer::new(raw, cfg);
209        writer.write_body_nodes(&body.body.roots).ok()?;
210        writer.consume()
211    }
212
213    #[test]
214    fn unparses_raw() {
215        let expr = syn::parse_str("1 + 1").expect("Failed to parse");
216        let unparsed = prettyplease::unparse(&wrapped(&expr));
217        assert_eq!(unparsed, "fn main() {\n    1 + 1;\n}\n");
218    }
219
220    #[test]
221    fn weird_ifcase() {
222        let contents = r##"
223        fn main() {
224            move |_| timer.with_mut(|t| if t.started_at.is_none() { Some(Instant::now()) } else { None })
225        }
226    "##;
227
228        let expr: File = syn::parse_file(contents).unwrap();
229        let out = prettyplease::unparse(&expr);
230        println!("{}", out);
231    }
232
233    #[test]
234    fn multiline_madness() {
235        let contents = r##"
236        {
237        {children.is_some().then(|| rsx! {
238            span {
239                class: "inline-block ml-auto hover:bg-gray-500",
240                onclick: move |evt| {
241                    evt.cancel_bubble();
242                },
243                icons::icon_5 {}
244                {rsx! {
245                    icons::icon_6 {}
246                }}
247            }
248        })}
249        {children.is_some().then(|| rsx! {
250            span {
251                class: "inline-block ml-auto hover:bg-gray-500",
252                onclick: move |evt| {
253                    evt.cancel_bubble();
254                },
255                icons::icon_10 {}
256            }
257        })}
258
259        }
260
261        "##;
262
263        let expr: Expr = syn::parse_str(contents).unwrap();
264        let out = unparse_expr(&expr, contents, &IndentOptions::default());
265        println!("{}", out);
266    }
267
268    #[test]
269    fn write_body_no_indent() {
270        let src = r##"
271            span {
272                class: "inline-block ml-auto hover:bg-gray-500",
273                onclick: move |evt| {
274                    evt.cancel_bubble();
275                },
276                icons::icon_10 {}
277                icons::icon_10 {}
278                icons::icon_10 {}
279                icons::icon_10 {}
280                div { "hi" }
281                div { div {} }
282                div { div {} div {} div {} }
283                {children}
284                {
285                    some_big_long()
286                        .some_big_long()
287                        .some_big_long()
288                        .some_big_long()
289                        .some_big_long()
290                        .some_big_long()
291                }
292                div { class: "px-4", {is_current.then(|| rsx! { {children} })} }
293                Thing {
294                    field: rsx! {
295                        div { "hi" }
296                        Component {
297                            onrender: rsx! {
298                                div { "hi" }
299                                Component {
300                                    onclick: move |_| {
301                                        another_macro! {
302                                            div { class: "max-w-lg lg:max-w-2xl mx-auto mb-16 text-center",
303                                                "gomg"
304                                                "hi!!"
305                                                "womh"
306                                            }
307                                        };
308                                        rsx! {
309                                            div { class: "max-w-lg lg:max-w-2xl mx-auto mb-16 text-center",
310                                                "gomg"
311                                                "hi!!"
312                                                "womh"
313                                            }
314                                        };
315                                        println!("hi")
316                                    },
317                                    onrender: move |_| {
318                                        let _ = 12;
319                                        let r = rsx! {
320                                            div { "hi" }
321                                        };
322                                        rsx! {
323                                            div { "hi" }
324                                        }
325                                    }
326                                }
327                                {
328                                    rsx! {
329                                        BarChart {
330                                            id: "bar-plot".to_string(),
331                                            x: value,
332                                            y: label
333                                        }
334                                    }
335                                }
336                            }
337                        }
338                    }
339                }
340            }
341        "##;
342
343        let tokens: TokenStream = syn::parse_str(src).unwrap();
344        let out = fmt_block_from_expr(src, tokens, IndentOptions::default()).unwrap();
345        println!("{}", out);
346    }
347
348    #[test]
349    fn write_component_body() {
350        let src = r##"
351    div { class: "px-4", {is_current.then(|| rsx! { {children} })} }
352    "##;
353
354        let tokens: TokenStream = syn::parse_str(src).unwrap();
355        let out = fmt_block_from_expr(src, tokens, IndentOptions::default()).unwrap();
356        println!("{}", out);
357    }
358
359    #[test]
360    fn weird_macro() {
361        let contents = r##"
362        fn main() {
363            move |_| {
364                drop_macro_semi! {
365                    "something_very_long_something_very_long_something_very_long_something_very_long"
366                };
367                let _ = drop_macro_semi! {
368                    "something_very_long_something_very_long_something_very_long_something_very_long"
369                };
370                drop_macro_semi! {
371                    "something_very_long_something_very_long_something_very_long_something_very_long"
372                };
373            };
374        }
375    "##;
376
377        let expr: File = syn::parse_file(contents).unwrap();
378        let out = prettyplease::unparse(&expr);
379        println!("{}", out);
380    }
381
382    #[test]
383    fn comments_on_nodes() {
384        let src = r##"// hiasdasds
385    div {
386        attr: "value", // comment
387        div {}
388        "hi" // hello!
389        "hi" // hello!
390        "hi" // hello!
391        // hi!
392    }
393    "##;
394
395        let tokens: TokenStream = syn::parse_str(src).unwrap();
396        let out = fmt_block_from_expr(src, tokens, IndentOptions::default()).unwrap();
397        println!("{}", out);
398    }
399}