mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-22 20:23:09 +00:00
Autofmt nested rsx using syn::Visitor (#2279)
* Nested macros using visitor pattern
This commit is contained in:
parent
0b7c7701a3
commit
67af2d89dd
7 changed files with 321 additions and 30 deletions
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
///
|
||||
|
|
|
@ -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}")?;
|
||||
|
|
|
@ -50,4 +50,5 @@ twoway![
|
|||
docsite,
|
||||
letsome,
|
||||
fat_exprs,
|
||||
nested,
|
||||
];
|
||||
|
|
145
packages/autofmt/tests/samples/nested.rsx
Normal file
145
packages/autofmt/tests/samples/nested.rsx
Normal 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?"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue