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

* Nested macros using visitor pattern
This commit is contained in:
Jonathan Kelley 2024-04-08 22:15:52 -07:00 committed by GitHub
parent 0b7c7701a3
commit 67af2d89dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 321 additions and 30 deletions

View file

@ -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 }

View file

@ -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();

View file

@ -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();

View file

@ -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<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
///

View file

@ -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}")?;

View file

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

View file

@ -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?"
}
};
}
}
}
}
}