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