Autofmt nested rsx using syn::Visitor (#2279)
* Nested macros using visitor pattern
This commit is contained in:
1 parent
0b7c7701a3
commit
67af2d89dd
7 files changed
+321
-30
No files matched your search
@@ -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 }
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -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::EventTokens(tokens) => {
|
ElementAttrValue::AttrExpr(expr) => {
|
||||||
let location = Location::new(tokens.span().start());
|
let out = self.retrieve_formatted_expr(expr);
|
||||||
|
if out.contains('\n') {
|
||||||
let len = if let std::collections::hash_map::Entry::Vacant(e) =
|
100000
|
||||||
self.cached_formats.entry(location)
|
|
||||||
{
|
|
||||||
let formatted = unparse_expr(tokens);
|
|
||||||
let len = if formatted.contains('\n') {
|
|
||||||
10000
|
|
||||||
} else {
|
|
||||||
formatted.len()
|
|
||||||
};
|
|
||||||
e.insert(formatted);
|
|
||||||
len
|
|
||||||
} else {
|
} else {
|
||||||
self.cached_formats[&location].len()
|
out.len()
|
||||||
};
|
}
|
||||||
|
}
|
||||||
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
|
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
|
let loc = Location::new(expr.span().start());
|
||||||
.entry(Location::new(expr.span().start()))
|
|
||||||
.or_insert_with(|| unparse_expr(expr))
|
if !self.cached_formats.contains_key(&loc) {
|
||||||
.as_str()
|
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}")?;
|
||||||
|
|||||||
@@ -50,4 +50,5 @@ twoway![
|
|||||||
docsite,
|
docsite,
|
||||||
letsome,
|
letsome,
|
||||||
fat_exprs,
|
fat_exprs,
|
||||||
|
nested,
|
||||||
];
|
];
|
||||||
@@ -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?"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user