1
0
Эх сурвалжийг харах

Merge pull request #454 from DioxusLabs/jk/autofmt

Add autoformatting package
Jon Kelley 3 жил өмнө
parent
commit
c70cd04835

+ 1 - 0
Cargo.toml

@@ -66,6 +66,7 @@ members = [
     "packages/fermi",
     "packages/tui",
     "packages/liveview",
+    "packages/autofmt",
     "packages/rsx",
     "packages/rsx_interpreter",
     "packages/native-core",

+ 15 - 0
packages/autofmt/Cargo.toml

@@ -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 - 0
packages/autofmt/README.md

@@ -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 - 0
packages/autofmt/src/buffer.rs

@@ -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 - 0
packages/autofmt/src/component.rs

@@ -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 - 0
packages/autofmt/src/element.rs

@@ -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 - 0
packages/autofmt/src/expr.rs

@@ -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 - 0
packages/autofmt/src/lib.rs

@@ -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 - 0
packages/autofmt/src/util.rs

@@ -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 - 0
packages/autofmt/tests/sample.rs

@@ -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 - 0
packages/autofmt/tests/samples/all.rs

@@ -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 - 0
packages/autofmt/tests/sink.rs

@@ -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
packages/rsx/src/component.rs

@@ -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 - 0
packages/rsx/src/element.rs

@@ -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
packages/rsx/src/node.rs

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