Merge pull request #454 from DioxusLabs/jk/autofmt

Add autoformatting package
This commit is contained in:
Jon Kelley authored and GitHub committed 2022-07-02 22:57:28 -04:00
commit c70cd04835
15 files changed
+1199 -5

No files matched your search

+1
View File
@@ -66,6 +66,7 @@ members = [
"packages/fermi",
"packages/tui",
"packages/liveview",
"packages/autofmt",
"packages/rsx",
"packages/rsx_interpreter",
"packages/native-core",
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "dioxus-autofmt"
version = "0.0.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
quote = "1.0"
syn = { version = "1.0.11", features = ["full", "extra-traits"] }
dioxus-rsx = { path = "../rsx" }
triple_accel = "0.4.0"
serde = { version = "1.0.136", features = ["derive"] }
prettyplease = { git = "https://github.com/DioxusLabs/prettyplease-macro-fmt.git", features = ["verbatim"] }
+5
View File
@@ -0,0 +1,5 @@
# This crate autofmts blocks of rsx!
This crate formats rsx! by parsing call bodies and pretty-printing them back out.
It also incorporates a fork of prettyplease to allow formatting arbitrary rust code too. Prettyplease rejected a suggestion to allow arbitrary expression formatting - something our fork lets us do.
+109
View File
@@ -0,0 +1,109 @@
use std::fmt::{Result, Write};
use dioxus_rsx::BodyNode;
#[derive(Default, Debug)]
pub struct Buffer {
pub src: Vec<String>,
pub buf: String,
pub indent: usize,
}
impl Buffer {
// Create a new line and tab it to the current tab level
pub fn tabbed_line(&mut self) -> Result {
self.new_line()?;
self.tab()
}
// Create a new line and tab it to the current tab level
pub fn indented_tabbed_line(&mut self) -> Result {
self.new_line()?;
self.indented_tab()
}
pub fn tab(&mut self) -> Result {
self.write_tabs(self.indent)
}
pub fn indented_tab(&mut self) -> Result {
self.write_tabs(self.indent + 1)
}
pub fn write_tabs(&mut self, num: usize) -> std::fmt::Result {
for _ in 0..num {
write!(self.buf, " ")?
}
Ok(())
}
pub fn new_line(&mut self) -> Result {
writeln!(self.buf)
}
pub fn write_ident(&mut self, node: &BodyNode) -> Result {
match node {
BodyNode::Element(el) => self.write_element(el),
BodyNode::Component(component) => self.write_component(component),
BodyNode::Text(text) => self.write_text(text),
BodyNode::RawExpr(exp) => self.write_raw_expr(exp),
}
}
pub fn write_text(&mut self, text: &syn::LitStr) -> Result {
write!(self.buf, "\"{}\"", text.value())
}
pub fn consume(self) -> Option<String> {
Some(self.buf)
}
// Push out the indent level and write each component, line by line
pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result {
self.indent += 1;
let mut comments = Vec::new();
for child in children {
// Exprs handle their own indenting/line breaks
if !matches!(child, BodyNode::RawExpr(_)) {
// collect all comments upwards
let start = child.span().start().line;
for (id, line) in self.src[..start - 1].iter().enumerate().rev() {
if line.trim().starts_with("//") || line.is_empty() {
comments.push(id);
} else {
break;
}
}
if comments.len() == 1 && self.src[comments[0]].is_empty() {
comments.pop();
}
let mut last_was_empty = false;
for comment_line in comments.drain(..).rev() {
let line = &self.src[comment_line];
if line.is_empty() {
if !last_was_empty {
self.new_line()?;
}
last_was_empty = true;
} else {
last_was_empty = false;
self.tabbed_line()?;
write!(self.buf, "{}", self.src[comment_line].trim())?;
}
}
self.tabbed_line()?;
}
self.write_ident(child)?;
}
self.indent -= 1;
Ok(())
}
}
+234
View File
@@ -0,0 +1,234 @@
use crate::Buffer;
use dioxus_rsx::*;
use quote::ToTokens;
use std::fmt::{Result, Write};
use syn::AngleBracketedGenericArguments;
enum ShortOptimization {
// Special because we want to print the closing bracket immediately
Empty,
// Special optimization to put everything on the same line
Oneliner,
// Optimization where children flow but props remain fixed on top
PropsOnTop,
// The noisiest optimization where everything flows
NoOpt,
}
impl Buffer {
pub fn write_component(
&mut self,
Component {
name,
fields,
children,
manual_props,
prop_gen_args,
}: &Component,
) -> Result {
self.write_component_name(name, prop_gen_args)?;
// decide if we have any special optimizations
// Default with none, opt the cases in one-by-one
let mut opt_level = ShortOptimization::NoOpt;
// check if we have a lot of attributes
let is_short_attr_list = self.is_short_fields(fields, manual_props).is_some();
let is_small_children = self.is_short_children(children).is_some();
// if we have few attributes and a lot of children, place the attrs on top
if is_short_attr_list && !is_small_children {
opt_level = ShortOptimization::PropsOnTop;
}
// even if the attr is long, it should be put on one line
if !is_short_attr_list && (fields.len() <= 1 && manual_props.is_none()) {
if children.is_empty() {
opt_level = ShortOptimization::Oneliner;
} else {
opt_level = ShortOptimization::PropsOnTop;
}
}
// if we have few children and few attributes, make it a one-liner
if is_short_attr_list && is_small_children {
opt_level = ShortOptimization::Oneliner;
}
// If there's nothing at all, empty optimization
if fields.is_empty() && children.is_empty() {
opt_level = ShortOptimization::Empty;
}
match opt_level {
ShortOptimization::Empty => {}
ShortOptimization::Oneliner => {
write!(self.buf, " ")?;
self.write_component_fields(fields, manual_props, true)?;
if !children.is_empty() && !fields.is_empty() {
write!(self.buf, ", ")?;
}
for child in children {
self.write_ident(child)?;
}
write!(self.buf, " ")?;
}
ShortOptimization::PropsOnTop => {
write!(self.buf, " ")?;
self.write_component_fields(fields, manual_props, true)?;
if !children.is_empty() && !fields.is_empty() {
write!(self.buf, ",")?;
}
self.write_body_indented(children)?;
self.tabbed_line()?;
}
ShortOptimization::NoOpt => {
self.write_component_fields(fields, manual_props, false)?;
self.write_body_indented(children)?;
self.tabbed_line()?;
}
}
write!(self.buf, "}}")?;
Ok(())
}
fn write_component_name(
&mut self,
name: &syn::Path,
generics: &Option<AngleBracketedGenericArguments>,
) -> Result {
let mut name = name.to_token_stream().to_string();
name.retain(|c| !c.is_whitespace());
write!(self.buf, "{name}")?;
if let Some(generics) = generics {
let mut written = generics.to_token_stream().to_string();
written.retain(|c| !c.is_whitespace());
write!(self.buf, "{}", written)?;
}
write!(self.buf, " {{")?;
Ok(())
}
fn write_component_fields(
&mut self,
fields: &[ComponentField],
manual_props: &Option<syn::Expr>,
sameline: bool,
) -> Result {
let mut field_iter = fields.iter().peekable();
while let Some(field) = field_iter.next() {
if !sameline {
self.indented_tabbed_line()?;
}
let name = &field.name;
match &field.content {
ContentField::ManExpr(exp) => {
let out = prettyplease::unparse_expr(exp);
write!(self.buf, "{}: {}", name, out)?;
}
ContentField::Formatted(s) => {
write!(self.buf, "{}: \"{}\"", name, s.value())?;
}
ContentField::OnHandlerRaw(exp) => {
let out = prettyplease::unparse_expr(exp);
let mut lines = out.split('\n').peekable();
let first = lines.next().unwrap();
write!(self.buf, "{}: {}", name, first)?;
for line in lines {
self.new_line()?;
self.indented_tab()?;
write!(self.buf, "{}", line)?;
}
}
}
if field_iter.peek().is_some() || manual_props.is_some() {
write!(self.buf, ",")?;
if sameline {
write!(self.buf, " ")?;
}
}
}
if let Some(exp) = manual_props {
if !sameline {
self.indented_tabbed_line()?;
}
self.write_manual_props(exp)?;
}
Ok(())
}
pub fn is_short_fields(
&self,
fields: &[ComponentField],
manual_props: &Option<syn::Expr>,
) -> Option<usize> {
let attr_len = fields
.iter()
.map(|field| match &field.content {
ContentField::ManExpr(exp) => exp.to_token_stream().to_string().len(),
ContentField::Formatted(s) => s.value().len() ,
ContentField::OnHandlerRaw(_) => 100000,
} + 10)
.sum::<usize>() + self.indent * 4;
match manual_props {
Some(p) => {
let content = prettyplease::unparse_expr(p);
if content.len() + attr_len > 80 {
return None;
}
let mut lines = content.lines();
lines.next().unwrap();
if lines.next().is_none() {
Some(attr_len + content.len())
} else {
None
}
}
None => Some(attr_len),
}
}
fn write_manual_props(&mut self, exp: &syn::Expr) -> Result {
/*
We want to normalize the expr to the appropriate indent level.
*/
let formatted = prettyplease::unparse_expr(exp);
let mut lines = formatted.lines();
let first_line = lines.next().unwrap();
write!(self.buf, "..{first_line}")?;
for line in lines {
self.indented_tabbed_line()?;
write!(self.buf, "{line}")?;
}
Ok(())
}
}
+259
View File
@@ -0,0 +1,259 @@
use crate::{util::*, Buffer};
use dioxus_rsx::*;
use std::{fmt::Result, fmt::Write};
enum ShortOptimization {
// Special because we want to print the closing bracket immediately
Empty,
// Special optimization to put everything on the same line
Oneliner,
// Optimization where children flow but props remain fixed on top
PropsOnTop,
// The noisiest optimization where everything flows
NoOpt,
}
impl Buffer {
pub fn write_element(
&mut self,
Element {
name,
key,
attributes,
children,
_is_static,
}: &Element,
) -> Result {
/*
1. Write the tag
2. Write the key
3. Write the attributes
4. Write the children
*/
write!(self.buf, "{name} {{")?;
// decide if we have any special optimizations
// Default with none, opt the cases in one-by-one
let mut opt_level = ShortOptimization::NoOpt;
// check if we have a lot of attributes
let is_short_attr_list = is_short_attrs(attributes);
let is_small_children = self.is_short_children(children).is_some();
// if we have few attributes and a lot of children, place the attrs on top
if is_short_attr_list && !is_small_children {
opt_level = ShortOptimization::PropsOnTop;
}
// even if the attr is long, it should be put on one line
if !is_short_attr_list && attributes.len() <= 1 {
if children.is_empty() {
opt_level = ShortOptimization::Oneliner;
} else {
opt_level = ShortOptimization::PropsOnTop;
}
}
// if we have few children and few attributes, make it a one-liner
if is_short_attr_list && is_small_children {
opt_level = ShortOptimization::Oneliner;
}
// If there's nothing at all, empty optimization
if attributes.is_empty() && children.is_empty() && key.is_none() {
opt_level = ShortOptimization::Empty;
}
match opt_level {
ShortOptimization::Empty => {}
ShortOptimization::Oneliner => {
write!(self.buf, " ")?;
self.write_attributes(attributes, key, true)?;
if !children.is_empty() && !attributes.is_empty() {
write!(self.buf, ", ")?;
}
for child in children {
self.write_ident(child)?;
}
write!(self.buf, " ")?;
}
ShortOptimization::PropsOnTop => {
write!(self.buf, " ")?;
self.write_attributes(attributes, key, true)?;
if !children.is_empty() && !attributes.is_empty() {
write!(self.buf, ",")?;
}
self.write_body_indented(children)?;
self.tabbed_line()?;
}
ShortOptimization::NoOpt => {
self.write_attributes(attributes, key, false)?;
self.write_body_indented(children)?;
self.tabbed_line()?;
}
}
write!(self.buf, "}}")?;
Ok(())
}
fn write_attributes(
&mut self,
attributes: &[ElementAttrNamed],
key: &Option<syn::LitStr>,
sameline: bool,
) -> Result {
let mut attr_iter = attributes.iter().peekable();
if let Some(key) = key {
if !sameline {
self.indented_tabbed_line()?;
}
write!(self.buf, "key: \"{}\"", key.value())?;
if !attributes.is_empty() {
write!(self.buf, ", ")?;
}
}
while let Some(attr) = attr_iter.next() {
if !sameline {
self.indented_tabbed_line()?;
}
self.write_attribute(attr)?;
if attr_iter.peek().is_some() {
write!(self.buf, ",")?;
if sameline {
write!(self.buf, " ")?;
}
}
}
Ok(())
}
fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result {
match &attr.attr {
ElementAttr::AttrText { name, value } => {
write!(self.buf, "{name}: \"{value}\"", value = value.value())?;
}
ElementAttr::AttrExpression { name, value } => {
let out = prettyplease::unparse_expr(value);
write!(self.buf, "{}: {}", name, out)?;
}
ElementAttr::CustomAttrText { name, value } => {
write!(
self.buf,
"\"{name}\": \"{value}\"",
name = name.value(),
value = value.value()
)?;
}
ElementAttr::CustomAttrExpression { name, value } => {
let out = prettyplease::unparse_expr(value);
write!(self.buf, "\"{}\": {}", name.value(), out)?;
}
ElementAttr::EventTokens { name, tokens } => {
let out = prettyplease::unparse_expr(tokens);
let mut lines = out.split('\n').peekable();
let first = lines.next().unwrap();
// a one-liner for whatever reason
// Does not need a new line
if lines.peek().is_none() {
write!(self.buf, "{}: {}", name, first)?;
} else {
writeln!(self.buf, "{}: {}", name, first)?;
while let Some(line) = lines.next() {
self.indented_tab()?;
write!(self.buf, "{}", line)?;
if lines.peek().is_none() {
write!(self.buf, "")?;
} else {
writeln!(self.buf)?;
}
}
}
}
}
Ok(())
}
// check if the children are short enough to be on the same line
// We don't have the notion of current line depth - each line tries to be < 80 total
// returns the total line length if it's short
// returns none if the length exceeds the limit
// I think this eventually becomes quadratic :(
pub fn is_short_children(&self, children: &[BodyNode]) -> Option<usize> {
if children.is_empty() {
// todo: allow elements with comments but no children
// like div { /* comment */ }
return Some(0);
}
for child in children {
'line: for line in self.src[..child.span().start().line - 1].iter().rev() {
match (line.trim().starts_with("//"), line.is_empty()) {
(true, _) => return None,
(_, true) => continue 'line,
_ => break 'line,
}
}
}
match children {
[BodyNode::Text(ref text)] => Some(text.value().len()),
[BodyNode::Component(ref comp)] => {
let is_short_child = self.is_short_children(&comp.children);
let is_short_attrs = self.is_short_fields(&comp.fields, &comp.manual_props);
match (is_short_child, is_short_attrs) {
(Some(child_len), Some(attrs_len)) => Some(child_len + attrs_len),
(Some(child_len), None) => Some(child_len),
(None, Some(attrs_len)) => Some(attrs_len),
(None, None) => None,
}
}
[BodyNode::RawExpr(ref _expr)] => {
// TODO: let rawexprs to be inlined
// let span = syn::spanned::Spanned::span(&text);
// let (start, end) = (span.start(), span.end());
// if start.line == end.line {
// Some(end.column - start.column)
// } else {
// None
// }
None
}
[BodyNode::Element(ref el)] => self
.is_short_children(&el.children)
.map(|f| f + extract_attr_len(&el.attributes))
.and_then(|new_len| if new_len > 80 { None } else { Some(new_len) }),
_ => None,
}
}
}
fn is_short_attrs(attrs: &[ElementAttrNamed]) -> bool {
extract_attr_len(attrs) < 80
}
+53
View File
@@ -0,0 +1,53 @@
//! pretty printer for rsx!
use std::fmt::{Result, Write};
use crate::Buffer;
impl Buffer {
pub fn write_raw_expr(&mut self, exp: &syn::Expr) -> Result {
/*
We want to normalize the expr to the appropriate indent level.
*/
// in a perfect world, just fire up the rust pretty printer
// pretty_print_rust_code_as_if_it_were_rustfmt()
use syn::spanned::Spanned;
let placement = exp.span();
let start = placement.start();
let end = placement.end();
let num_spaces_desired = (self.indent * 4) as isize;
let first = &self.src[start.line - 1];
let num_spaces_real = first.chars().take_while(|c| c.is_whitespace()).count() as isize;
let offset = num_spaces_real - num_spaces_desired;
for line in &self.src[start.line - 1..end.line] {
writeln!(self.buf)?;
// trim the leading whitespace
if offset < 0 {
for _ in 0..-offset {
write!(self.buf, " ")?;
}
write!(self.buf, "{}", line)?;
} else {
let offset = offset as usize;
let right = &line[offset..];
write!(self.buf, "{}", right)?;
}
}
Ok(())
}
}
// :(
// fn pretty_print_rust_code_as_if_it_were_rustfmt(code: &str) -> String {
// let formatted = prettyplease::unparse_expr(exp);
// for line in formatted.lines() {
// write!(self.buf, "{}", line)?;
// self.new_line()?;
// }
// }
+79
View File
@@ -0,0 +1,79 @@
use crate::buffer::*;
use crate::util::*;
mod buffer;
mod component;
mod element;
mod expr;
mod util;
/// A modification to the original file to be applied by an IDE
///
/// Right now this re-writes entire rsx! blocks at a time, instead of precise line-by-line changes.
///
/// In a "perfect" world we would have tiny edits to preserve things like cursor states and selections. The API here makes
/// it possible to migrate to a more precise modification approach in the future without breaking existing code.
///
/// Note that this is tailored to VSCode's TextEdit API and not a general Diff API. Line numbers are not accurate if
/// multiple edits are applied in a single file without tracking text shifts.
#[derive(serde::Deserialize, serde::Serialize, Clone, Debug, PartialEq, Hash)]
pub struct FormattedBlock {
/// The new contents of the block
pub formatted: String,
/// The line number of the first line of the block.
pub start: usize,
/// The end of the block, exclusive.
pub end: usize,
}
/// Format a file into a list of `FormattedBlock`s to be applied by an IDE for autoformatting.
///
/// This function expects a complete file, not just a block of code. To format individual rsx! blocks, use fmt_block instead.
pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
let mut formatted_blocks = Vec::new();
let mut last_bracket_end = 0;
use triple_accel::{levenshtein_search, Match};
for Match { end, start, .. } in levenshtein_search(b"rsx! {", contents.as_bytes()) {
// ensure the marker is not nested
if start < last_bracket_end {
continue;
}
let remaining = &contents[end - 1..];
let bracket_end = find_bracket_end(remaining).unwrap();
let sub_string = &contents[end..bracket_end + end - 1];
last_bracket_end = bracket_end + end - 1;
let new = fmt_block(sub_string).unwrap();
let stripped = &contents[end + 1..bracket_end + end - 1];
if stripped == new {
continue;
}
formatted_blocks.push(FormattedBlock {
formatted: new,
start: end,
end: end + bracket_end - 1,
});
}
formatted_blocks
}
pub fn fmt_block(block: &str) -> Option<String> {
let mut buf = Buffer {
src: block.lines().map(|f| f.to_string()).collect(),
..Buffer::default()
};
let body = syn::parse_str::<dioxus_rsx::CallBody>(block).unwrap();
buf.write_body_indented(&body.roots).unwrap();
buf.consume()
}
+33
View File
@@ -0,0 +1,33 @@
use dioxus_rsx::*;
// todo: use recursive or complete sizeing
pub fn extract_attr_len(attributes: &[ElementAttrNamed]) -> usize {
attributes
.iter()
.map(|attr| match &attr.attr {
ElementAttr::AttrText { value, .. } => value.value().len(),
ElementAttr::AttrExpression { .. } => 10,
ElementAttr::CustomAttrText { value, .. } => value.value().len(),
ElementAttr::CustomAttrExpression { .. } => 10,
ElementAttr::EventTokens { .. } => 1000000,
})
.sum()
}
pub fn find_bracket_end(contents: &str) -> Option<usize> {
let mut depth = 0;
for (i, c) in contents.chars().enumerate() {
if c == '{' {
depth += 1;
} else if c == '}' {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
}
None
}
+23
View File
@@ -0,0 +1,23 @@
const SRC: &str = include_str!("./samples/all.rs");
fn body() -> &'static str {
&SRC[6..SRC.len() - 3]
}
fn unindented_body() -> String {
body()
.lines()
.map(|line| match line.strip_prefix(" ") {
Some(line) => line,
None => line,
})
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn way_and_back() {
let blocks = dioxus_autofmt::fmt_file(SRC).into_iter().next().unwrap();
println!("{}", blocks.formatted);
}
+225
View File
@@ -0,0 +1,225 @@
rsx! {
// Simple case
div { key: "a", class: "ban", style: "color: red" }
// Many attributes
div {
div {
key: "ddd",
class: "asd",
class: "asd",
class: "asd",
class: "asd",
class: "asd",
class: "asd",
blah: 123,
onclick: move |_| {
let blah = 120;
true
},
onclick: move |_| {
let blah = 120;
true
},
onclick: move |_| {
let blah = 120;
true
},
onclick: move |_| {
let blah = 120;
true
},
div {
div { "hi" }
h2 { class: "asd" }
}
Component {}
// Generics
Component<Generic> {}
}
}
// Intertwined comments
div { adsasd: "asd",
h1 { "asd" }
div {
div { "hello" }
div { "goodbye" }
div { class: "broccoli", div { "hi" } }
div { class: "broccolibroccolibroccolibroccolibroccolibroccolibroccolibroccolibroccolibroccoli",
div { "hi" }
}
div {
class: "alksdjasd",
onclick: move |_| {
liberty!();
},
div { "hi" }
}
commented {
// is unparalled
class: "asdasd",
// My genius
div { "hi" }
div {}
}
}
}
// Regular comments
div { adsasd: "asd", block: "asd",
// this is a comment
"hello"
// this is a comment 1
// this is a comment 2
"hello"
div {
// this is a comment
"hello"
}
div { key: "asd", class: "asdsda",
div {}
h3 {}
}
}
// Components
Component {
adsasd: "asd",
// this is a comment
onclick: move |_| {
let blah = 120;
let blah = 122;
}
}
// Manual props
div {
Component {
adsasd: "asd",
onclick: move |_| {
let a = a;
},
div { "thing" }
}
Component {
asdasd: "asdasd",
asdasd: "asdasdasdasdasdasdasdasdasdasd",
..Props {
a: 10,
b: 20
}
}
Component {
asdasd: "asdasd",
..Props {
a: 10,
b: 20,
c: {
fn main() {}
},
}
"content"
}
}
// Long attributes
div {
a: "1234567891012345678910123456789101234567891012345678910123456789101234567891012345678910123456789101234567891012345678910",
a: "123",
a: "123",
a: "123",
a: "123",
a: "123",
a: "123",
a: "123",
a: "123"
}
// Short attributes
div { a: "123", a: "123", a: "123", a: "123", a: "123", a: "123", a: "123", a: "123", a: "123" }
// Compression
h3 { class: "mb-2 text-xl font-bold", "Invite Member" }
a { class: "text-white", "Send invitation" }
// Props on tops
h3 { class: "mb-2 text-xl font-bold mb-2 text-xl font-bold mb-2 text-xl font-bold mb-2 text-xl font-bold mb-2 text-xl font-bold",
"Invite Member"
}
// No children, minimal props
img { class: "mb-6 mx-auto h-24", src: "artemis-assets/images/friends.png", alt: "" }
// One level compression
div {
a { class: "py-2 px-3 bg-indigo-500 hover:bg-indigo-600 rounded text-xs text-white", href: "#", "Send invitation" }
}
// Tiny component
Component { a: 123 }
// Expressions
ul {
div {}
(0..10).map(|f| rsx! {
li { "hi" }
})
div {}
}
// Complex nesting with components
button {
class: "flex items-center pl-3 py-3 pr-2 text-gray-500 hover:bg-indigo-50 rounded",
onclick: move |evt| {
show_user_menu.set(!show_user_menu.get());
evt.cancel_bubble();
},
onclick: move |evt| show_user_menu.set(!show_user_menu.get()),
span { class: "inline-block mr-4", icons::icon_14 {} }
span { "Settings" }
}
// Complex nesting with handlers
li {
Link { class: "flex items-center pl-3 py-3 pr-4 {active_class} rounded", to: "{to}",
span { class: "inline-block mr-3", icons::icon_0 {} }
span { "{name}" }
children.is_some().then(|| rsx! {
span {
class: "inline-block ml-auto hover:bg-gray-500",
onclick: move |evt| {
// open.set(!open.get());
evt.cancel_bubble();
},
icons::icon_8 {}
}
})
}
div { class: "px-4",
is_current.then(|| rsx!{ children })
}
}
// No nesting
Component {
adsasd: "asd",
onclick: move |_| {
let blah = 120;
}
}
// Component path
my::thing::Component {
adsasd: "asd",
onclick: move |_| {
let blah = 120;
}
}
}
+138
View File
@@ -0,0 +1,138 @@
use dioxus_autofmt::*;
#[test]
fn formats_valid_rust_src() {
let src = r#"
//
rsx! {
div {}
div {
h3 {"asd"
}
}
}
"#;
let formatted = fmt_file(src);
println!("{formatted:?}");
}
#[test]
fn formats_valid_rust_src_with_indents() {
let mut src = r#"
#[inline_props]
fn NavItem<'a>(cx: Scope, to: &'static str, children: Element<'a>, icon: Shape) -> Element {
const ICON_SIZE: u32 = 36;
rsx! {
div {
h1 {"thing"}
}
}
}
"#
.to_string();
let formatted = fmt_file(&src);
let block = formatted.into_iter().next().unwrap();
src.replace_range(
block.start - 1..block.end + 1,
&format!("{{ {} }}", &block.formatted),
);
}
#[test]
fn formats_multiple_blocks() {
let mut src = r#"
#[inline_props]
fn NavItem<'a>(cx: Scope, to: &'static str, children: Element<'a>, icon: Shape) -> Element {
const ICON_SIZE: u32 = 36;
rsx! {
div {
h1 {"thing"}
}
}
rsx! {
div {
Ball {
a: rsx!{
"asdasd"
}
}
}
}
}
#[inline_props]
fn NavItem<'a>(cx: Scope, to: &'static str, children: Element<'a>, icon: Shape) -> Element {
const ICON_SIZE: u32 = 36;
rsx! {
div {
h1 {"thing"}
}
}
rsx! {
div {
Ball {
a: rsx!{
"asdasd"
}
}
}
}
}
"#
.to_string();
let formatted = fmt_file(&src);
dbg!(&formatted);
let block = formatted.into_iter().next().unwrap();
src.replace_range(
block.start - 1..block.end + 1,
&format!("{{ {} }}", &block.formatted),
);
}
#[test]
fn empty_blocks() {
let src = r###"
pub fn Alert(cx: Scope) -> Element {
cx.render(rsx! {
div { }
})
}
"###
.to_string();
let formatted = fmt_file(&src);
dbg!(&formatted);
}
+4 -4
View File
@@ -27,7 +27,7 @@ use syn::{
pub struct Component {
pub name: syn::Path,
pub prop_gen_args: Option<AngleBracketedGenericArguments>,
pub body: Vec<ComponentField>,
pub fields: Vec<ComponentField>,
pub children: Vec<BodyNode>,
pub manual_props: Option<Expr>,
}
@@ -105,7 +105,7 @@ impl Parse for Component {
Ok(Self {
name,
prop_gen_args,
body,
fields: body,
children,
manual_props,
})
@@ -124,7 +124,7 @@ impl ToTokens for Component {
let mut toks = quote! {
let mut __manual_props = #manual_props;
};
for field in &self.body {
for field in &self.fields {
if field.name == "key" {
has_key = Some(field);
} else {
@@ -147,7 +147,7 @@ impl ToTokens for Component {
Some(gen_args) => quote! { fc_to_builder #gen_args(#name) },
None => quote! { fc_to_builder(#name) },
};
for field in &self.body {
for field in &self.fields {
match field.name.to_string().as_str() {
"key" => {
//
+10
View File
@@ -209,6 +209,16 @@ pub enum ElementAttr {
/// onclick: {}
EventTokens { name: Ident, tokens: Expr },
}
impl ElementAttr {
pub fn is_expr(&self) -> bool {
matches!(
self,
ElementAttr::AttrExpression { .. }
| ElementAttr::CustomAttrExpression { .. }
| ElementAttr::EventTokens { .. }
)
}
}
#[derive(PartialEq, Eq)]
pub struct ElementAttrNamed {
+11 -1
View File
@@ -1,9 +1,10 @@
use super::*;
use proc_macro2::TokenStream as TokenStream2;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{
parse::{Parse, ParseStream},
spanned::Spanned,
token, Expr, LitStr, Result,
};
@@ -27,6 +28,15 @@ impl BodyNode {
pub fn is_litstr(&self) -> bool {
matches!(self, BodyNode::Text(_))
}
pub fn span(&self) -> Span {
match self {
BodyNode::Element(el) => el.name.span(),
BodyNode::Component(component) => component.name.span(),
BodyNode::Text(text) => text.span(),
BodyNode::RawExpr(exp) => exp.span(),
}
}
}
impl Parse for BodyNode {