diff --git a/packages/autofmt/Cargo.toml b/packages/autofmt/Cargo.toml index 5765c0bbe..f3c8e5a9e 100644 --- a/packages/autofmt/Cargo.toml +++ b/packages/autofmt/Cargo.toml @@ -14,7 +14,12 @@ keywords = ["dom", "ui", "gui", "react"] dioxus-rsx = { workspace = true } proc-macro2 = { version = "1.0.6", features = ["span-locations"] } 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"] } prettyplease = { workspace = true } diff --git a/packages/autofmt/src/component.rs b/packages/autofmt/src/component.rs index ad92ba345..664a539bb 100644 --- a/packages/autofmt/src/component.rs +++ b/packages/autofmt/src/component.rs @@ -171,7 +171,7 @@ impl Writer<'_> { } ContentField::ManExpr(exp) => { - let out = unparse_expr(exp); + let out = self.unparse_expr(exp); let mut lines = out.split('\n').peekable(); let first = lines.next().unwrap(); write!(self.out, "{name}: {first}")?; @@ -181,6 +181,7 @@ impl Writer<'_> { write!(self.out, "{line}")?; } } + ContentField::Formatted(s) => { write!( self.out, @@ -260,7 +261,7 @@ impl Writer<'_> { 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(); diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index ca007301c..774fac38d 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -245,7 +245,7 @@ impl Writer<'_> { write!(self.out, "{value}",)?; } ElementAttrValue::AttrExpr(value) => { - let out = unparse_expr(value); + let out = self.unparse_expr(value); let mut lines = out.split('\n').peekable(); let first = lines.next().unwrap(); diff --git a/packages/autofmt/src/prettier_please.rs b/packages/autofmt/src/prettier_please.rs index 6b6a726e2..bb9d06a5b 100644 --- a/packages/autofmt/src/prettier_please.rs +++ b/packages/autofmt/src/prettier_please.rs @@ -1,5 +1,145 @@ 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, + } + + 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 /// diff --git a/packages/autofmt/src/writer.rs b/packages/autofmt/src/writer.rs index 8d3762e38..73cc84a5e 100644 --- a/packages/autofmt/src/writer.rs +++ b/packages/autofmt/src/writer.rs @@ -1,4 +1,3 @@ -use crate::prettier_please::unparse_expr; use dioxus_rsx::{AttributeType, BodyNode, ElementAttrValue, ForLoop, IfChain}; use proc_macro2::{LineColumn, Span}; use quote::ToTokens; @@ -160,27 +159,22 @@ impl<'a> Writer<'a> { condition_len + value_len + 6 } ElementAttrValue::AttrLiteral(lit) => ifmt_to_string(lit).len(), - ElementAttrValue::AttrExpr(expr) => expr.span().line_length(), ElementAttrValue::Shorthand(expr) => expr.span().line_length(), - 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 + ElementAttrValue::AttrExpr(expr) => { + let out = self.retrieve_formatted_expr(expr); + if out.contains('\n') { + 100000 } else { - self.cached_formats[&location].len() - }; - - len + out.len() + } + } + ElementAttrValue::EventTokens(tokens) => { + let as_str = self.retrieve_formatted_expr(tokens); + if as_str.contains('\n') { + 100000 + } else { + as_str.len() + } } } } @@ -234,11 +228,16 @@ impl<'a> Writer<'a> { total } + #[allow(clippy::map_entry)] 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 { @@ -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 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 first_line = lines.next().unwrap(); write!(self.out, "{first_line}")?; diff --git a/packages/autofmt/tests/samples.rs b/packages/autofmt/tests/samples.rs index 21dd65a15..ed7d08f2e 100644 --- a/packages/autofmt/tests/samples.rs +++ b/packages/autofmt/tests/samples.rs @@ -50,4 +50,5 @@ twoway![ docsite, letsome, fat_exprs, + nested, ]; diff --git a/packages/autofmt/tests/samples/nested.rsx b/packages/autofmt/tests/samples/nested.rsx new file mode 100644 index 000000000..58ae2dd4a --- /dev/null +++ b/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?" + } + }; + } + } + } + } +}