ソースを参照

Autofmt nested rsx using syn::Visitor (#2279)

* Nested macros using visitor pattern
Jonathan Kelley 1 年間 前
コミット
67af2d89dd

+ 6 - 1
packages/autofmt/Cargo.toml

@@ -14,7 +14,12 @@ keywords = ["dom", "ui", "gui", "react"]
 dioxus-rsx = { workspace = true }
 dioxus-rsx = { workspace = true }
 proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
 proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
 quote = { workspace = true }
 quote = { workspace = true }
-syn = { workspace = true, features = ["full", "extra-traits", "visit"] }
+syn = { workspace = true, features = [
+    "full",
+    "extra-traits",
+    "visit",
+    "visit-mut",
+] }
 serde = { version = "1.0.136", features = ["derive"] }
 serde = { version = "1.0.136", features = ["derive"] }
 prettyplease = { workspace = true }
 prettyplease = { workspace = true }
 
 

+ 3 - 2
packages/autofmt/src/component.rs

@@ -171,7 +171,7 @@ impl Writer<'_> {
                 }
                 }
 
 
                 ContentField::ManExpr(exp) => {
                 ContentField::ManExpr(exp) => {
-                    let out = unparse_expr(exp);
+                    let out = self.unparse_expr(exp);
                     let mut lines = out.split('\n').peekable();
                     let mut lines = out.split('\n').peekable();
                     let first = lines.next().unwrap();
                     let first = lines.next().unwrap();
                     write!(self.out, "{name}: {first}")?;
                     write!(self.out, "{name}: {first}")?;
@@ -181,6 +181,7 @@ impl Writer<'_> {
                         write!(self.out, "{line}")?;
                         write!(self.out, "{line}")?;
                     }
                     }
                 }
                 }
+
                 ContentField::Formatted(s) => {
                 ContentField::Formatted(s) => {
                     write!(
                     write!(
                         self.out,
                         self.out,
@@ -260,7 +261,7 @@ impl Writer<'_> {
         We want to normalize the expr to the appropriate indent level.
         We want to normalize the expr to the appropriate indent level.
         */
         */
 
 
-        let formatted = unparse_expr(exp);
+        let formatted = self.unparse_expr(exp);
 
 
         let mut lines = formatted.lines();
         let mut lines = formatted.lines();
 
 

+ 1 - 1
packages/autofmt/src/element.rs

@@ -245,7 +245,7 @@ impl Writer<'_> {
                 write!(self.out, "{value}",)?;
                 write!(self.out, "{value}",)?;
             }
             }
             ElementAttrValue::AttrExpr(value) => {
             ElementAttrValue::AttrExpr(value) => {
-                let out = unparse_expr(value);
+                let out = self.unparse_expr(value);
                 let mut lines = out.split('\n').peekable();
                 let mut lines = out.split('\n').peekable();
                 let first = lines.next().unwrap();
                 let first = lines.next().unwrap();
 
 

+ 141 - 1
packages/autofmt/src/prettier_please.rs

@@ -1,5 +1,145 @@
 use prettyplease::unparse;
 use prettyplease::unparse;
-use syn::{Expr, File, Item};
+use syn::{visit_mut::VisitMut, Expr, File, Item};
+
+use crate::Writer;
+
+impl Writer<'_> {
+    pub fn unparse_expr(&mut self, expr: &Expr) -> String {
+        struct ReplaceMacros<'a, 'b> {
+            writer: &'a mut Writer<'b>,
+            formatted_stack: Vec<String>,
+        }
+
+        impl VisitMut for ReplaceMacros<'_, '_> {
+            fn visit_stmt_mut(&mut self, _expr: &mut syn::Stmt) {
+                if let syn::Stmt::Macro(i) = _expr {
+                    // replace the macro with a block that roughly matches the macro
+                    if let Some("rsx" | "render") = i
+                        .mac
+                        .path
+                        .segments
+                        .last()
+                        .map(|i| i.ident.to_string())
+                        .as_deref()
+                    {
+                        // format the macro in place
+                        // we'll use information about the macro to replace it with another formatted block
+                        // once we've written out the unparsed expr from prettyplease, we can replace
+                        // this dummy block with the actual formatted block
+                        let formatted = crate::fmt_block_from_expr(
+                            self.writer.raw_src,
+                            syn::ExprMacro {
+                                attrs: i.attrs.clone(),
+                                mac: i.mac.clone(),
+                            },
+                        )
+                        .unwrap();
+
+                        *_expr = syn::Stmt::Expr(
+                            syn::parse_quote!(dioxus_autofmt_block__________),
+                            i.semi_token,
+                        );
+
+                        // Save this formatted block for later, when we apply it to the original expr
+                        self.formatted_stack.push(formatted);
+                    }
+                }
+
+                syn::visit_mut::visit_stmt_mut(self, _expr);
+            }
+
+            fn visit_expr_mut(&mut self, _expr: &mut syn::Expr) {
+                if let syn::Expr::Macro(i) = _expr {
+                    // replace the macro with a block that roughly matches the macro
+                    if let Some("rsx" | "render") = i
+                        .mac
+                        .path
+                        .segments
+                        .last()
+                        .map(|i| i.ident.to_string())
+                        .as_deref()
+                    {
+                        // format the macro in place
+                        // we'll use information about the macro to replace it with another formatted block
+                        // once we've written out the unparsed expr from prettyplease, we can replace
+                        // this dummy block with the actual formatted block
+                        let formatted = crate::fmt_block_from_expr(
+                            self.writer.raw_src,
+                            syn::ExprMacro {
+                                attrs: i.attrs.clone(),
+                                mac: i.mac.clone(),
+                            },
+                        )
+                        .unwrap();
+
+                        *_expr = syn::parse_quote!(dioxus_autofmt_block__________);
+
+                        // Save this formatted block for later, when we apply it to the original expr
+                        self.formatted_stack.push(formatted);
+                    }
+                }
+
+                syn::visit_mut::visit_expr_mut(self, _expr);
+            }
+        }
+
+        // Visit the expr and replace the macros with formatted blocks
+        let mut replacer = ReplaceMacros {
+            writer: self,
+            formatted_stack: vec![],
+        };
+
+        // builds the expression stack
+        let mut modified_expr = expr.clone();
+        replacer.visit_expr_mut(&mut modified_expr);
+
+        // now unparsed with the modified expression
+        let mut unparsed = unparse_expr(&modified_expr);
+
+        // walk each line looking for the dioxus_autofmt_block__________ token
+        // if we find it, replace it with the formatted block
+        // if there's indentation we want to presreve it
+
+        // now we can replace the macros with the formatted blocks
+        for formatted in replacer.formatted_stack.drain(..) {
+            let fmted = if formatted.contains('\n') {
+                format!("rsx! {{{formatted}\n}}")
+            } else {
+                format!("rsx! {{{formatted}}}")
+            };
+            let mut out_fmt = String::new();
+            let mut whitespace = 0;
+
+            for line in unparsed.lines() {
+                if line.contains("dioxus_autofmt_block__________") {
+                    whitespace = line.chars().take_while(|c| c.is_whitespace()).count();
+                    break;
+                }
+            }
+
+            for (idx, fmt_line) in fmted.lines().enumerate() {
+                // Push the indentation
+                if idx > 0 {
+                    out_fmt.push_str(&" ".repeat(whitespace));
+                }
+
+                out_fmt.push_str(fmt_line);
+
+                // Push a newline
+                out_fmt.push('\n');
+            }
+
+            // Remove the last newline
+            out_fmt.pop();
+
+            // Replace the dioxus_autofmt_block__________ token with the formatted block
+            unparsed = unparsed.replacen("dioxus_autofmt_block__________", &out_fmt, 1);
+            continue;
+        }
+
+        unparsed
+    }
+}
 
 
 /// Unparse an expression back into a string
 /// Unparse an expression back into a string
 ///
 ///

+ 23 - 24
packages/autofmt/src/writer.rs

@@ -1,4 +1,3 @@
-use crate::prettier_please::unparse_expr;
 use dioxus_rsx::{AttributeType, BodyNode, ElementAttrValue, ForLoop, IfChain};
 use dioxus_rsx::{AttributeType, BodyNode, ElementAttrValue, ForLoop, IfChain};
 use proc_macro2::{LineColumn, Span};
 use proc_macro2::{LineColumn, Span};
 use quote::ToTokens;
 use quote::ToTokens;
@@ -160,27 +159,22 @@ impl<'a> Writer<'a> {
                 condition_len + value_len + 6
                 condition_len + value_len + 6
             }
             }
             ElementAttrValue::AttrLiteral(lit) => ifmt_to_string(lit).len(),
             ElementAttrValue::AttrLiteral(lit) => ifmt_to_string(lit).len(),
-            ElementAttrValue::AttrExpr(expr) => expr.span().line_length(),
             ElementAttrValue::Shorthand(expr) => expr.span().line_length(),
             ElementAttrValue::Shorthand(expr) => expr.span().line_length(),
+            ElementAttrValue::AttrExpr(expr) => {
+                let out = self.retrieve_formatted_expr(expr);
+                if out.contains('\n') {
+                    100000
+                } else {
+                    out.len()
+                }
+            }
             ElementAttrValue::EventTokens(tokens) => {
             ElementAttrValue::EventTokens(tokens) => {
-                let location = Location::new(tokens.span().start());
-
-                let len = if let std::collections::hash_map::Entry::Vacant(e) =
-                    self.cached_formats.entry(location)
-                {
-                    let formatted = unparse_expr(tokens);
-                    let len = if formatted.contains('\n') {
-                        10000
-                    } else {
-                        formatted.len()
-                    };
-                    e.insert(formatted);
-                    len
+                let as_str = self.retrieve_formatted_expr(tokens);
+                if as_str.contains('\n') {
+                    100000
                 } else {
                 } else {
-                    self.cached_formats[&location].len()
-                };
-
-                len
+                    as_str.len()
+                }
             }
             }
         }
         }
     }
     }
@@ -234,11 +228,16 @@ impl<'a> Writer<'a> {
         total
         total
     }
     }
 
 
+    #[allow(clippy::map_entry)]
     pub fn retrieve_formatted_expr(&mut self, expr: &Expr) -> &str {
     pub fn retrieve_formatted_expr(&mut self, expr: &Expr) -> &str {
-        self.cached_formats
-            .entry(Location::new(expr.span().start()))
-            .or_insert_with(|| unparse_expr(expr))
-            .as_str()
+        let loc = Location::new(expr.span().start());
+
+        if !self.cached_formats.contains_key(&loc) {
+            let formatted = self.unparse_expr(expr);
+            self.cached_formats.insert(loc, formatted);
+        }
+
+        self.cached_formats.get(&loc).unwrap().as_str()
     }
     }
 
 
     fn write_for_loop(&mut self, forloop: &ForLoop) -> std::fmt::Result {
     fn write_for_loop(&mut self, forloop: &ForLoop) -> std::fmt::Result {
@@ -308,7 +307,7 @@ impl<'a> Writer<'a> {
 
 
     /// An expression within a for or if block that might need to be spread out across several lines
     /// An expression within a for or if block that might need to be spread out across several lines
     fn write_inline_expr(&mut self, expr: &Expr) -> std::fmt::Result {
     fn write_inline_expr(&mut self, expr: &Expr) -> std::fmt::Result {
-        let unparsed = unparse_expr(expr);
+        let unparsed = self.unparse_expr(expr);
         let mut lines = unparsed.lines();
         let mut lines = unparsed.lines();
         let first_line = lines.next().unwrap();
         let first_line = lines.next().unwrap();
         write!(self.out, "{first_line}")?;
         write!(self.out, "{first_line}")?;

+ 1 - 0
packages/autofmt/tests/samples.rs

@@ -50,4 +50,5 @@ twoway![
     docsite,
     docsite,
     letsome,
     letsome,
     fat_exprs,
     fat_exprs,
+    nested,
 ];
 ];

+ 145 - 0
packages/autofmt/tests/samples/nested.rsx

@@ -0,0 +1,145 @@
+//! some basic test cases with nested rsx!
+
+fn App() -> Element {
+    let mut count = use_signal(|| 0);
+    let mut text = use_signal(|| "...".to_string());
+
+    rsx! {
+        div {
+            div { "hi" }
+            div {
+                header: rsx! {
+                    div { class: "max-w-lg lg:max-w-2xl mx-auto mb-16 text-center",
+                        "gomg"
+                        "hi!!"
+                        "womh"
+                    }
+                },
+                header: rsx! {
+                    div { class: "max-w-lg lg:max-w-2xl mx-auto mb-16 text-center",
+                        "gomg"
+                        "hi!!"
+                        "womh"
+                    }
+                },
+                header: rsx! {
+                    div { class: "max-w-lg lg:max-w-2xl mx-auto mb-16 text-center",
+                        "gomg"
+                        // "hi!!"
+                        "womh"
+                    }
+                },
+                onclick: move |_| {
+                    rsx! {
+                        div { class: "max-w-lg lg:max-w-2xl mx-auto mb-16 text-center",
+                            "gomg"
+                            "hi!!"
+                            "womh"
+                        }
+                    };
+                    println!("hi")
+                },
+                "hi"
+            }
+            ContentList { header, content: &BLOG_POSTS, readmore: true }
+        }
+        Component {
+            header: rsx! {
+                h1 { "hi" }
+                h1 { "hi" }
+            },
+            blah: rsx! {
+                h1 { "hi" }
+            },
+            blah: rsx! {
+                h1 { "hi" }
+            },
+            blah: rsx! {
+                h1 { "hi" }
+            },
+            blah: rsx! { "hi" },
+            blah: rsx! {
+                h1 { "hi" }
+                Component {
+                    header: rsx! {
+                        Component {
+                            header: rsx! {
+                                div { "hi" }
+                                h3 { "hi" }
+                                p { "hi" }
+                                Component {
+                                    onrender: move |_| {
+                                        count += 1;
+                                        let abc = rsx! {
+                                            div {
+                                                h1 { "hi" }
+                                                "something nested?"
+                                                Component {
+                                                    onrender: move |_| {
+                                                        count2 += 1;
+                                                        rsx! {
+                                                            div2 {
+                                                                h12 { "hi" }
+                                                                "so22mething nested?"
+                                                            }
+                                                        }
+                                                    }
+                                                }
+                                            }
+                                        };
+                                        rsx! {
+                                            div {
+                                                h1 { "hi" }
+                                                "something nested?"
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            },
+            onrender: move |_| {
+                count += 1;
+                rsx! {
+                    div {
+                        h1 { "hi" }
+                        "something nested?"
+                    }
+                    Component2 {
+                        header2: rsx! {
+                            h1 { "hi1" }
+                            h1 { "hi2" }
+                        },
+                        onrender2: move |_| {
+                            count2 += 1;
+                            rsx! {
+                                div2 {
+                                    h12 { "hi" }
+                                    "so22mething nested?"
+                                }
+                            }
+                        },
+                        {rsx! {
+                            div2 {
+                                h12 { "hi" }
+                                "so22mething nested?"
+                            }
+                        }}
+                    }
+                }
+            },
+            div {
+                onclick: move |_| {
+                    let val = rsx! {
+                        div {
+                            h1 { "hi" }
+                            "something nested?"
+                        }
+                    };
+                }
+            }
+        }
+    }
+}