diff --git a/Cargo.toml b/Cargo.toml index 34c4a2043..483e418a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,7 @@ members = [ "packages/fermi", "packages/tui", "packages/liveview", + "packages/autofmt", "packages/rsx", "packages/rsx_interpreter", "packages/native-core", diff --git a/packages/autofmt/Cargo.toml b/packages/autofmt/Cargo.toml new file mode 100644 index 000000000..c7bf01dfa --- /dev/null +++ b/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"] } diff --git a/packages/autofmt/README.md b/packages/autofmt/README.md new file mode 100644 index 000000000..9fded3260 --- /dev/null +++ b/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. diff --git a/packages/autofmt/src/buffer.rs b/packages/autofmt/src/buffer.rs new file mode 100644 index 000000000..f962751f5 --- /dev/null +++ b/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, + 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 { + 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(()) + } +} diff --git a/packages/autofmt/src/component.rs b/packages/autofmt/src/component.rs new file mode 100644 index 000000000..32c028247 --- /dev/null +++ b/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, + ) -> 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, + 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, + ) -> Option { + 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::() + 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(()) + } +} diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs new file mode 100644 index 000000000..41abf658a --- /dev/null +++ b/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, + 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 { + 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 +} diff --git a/packages/autofmt/src/expr.rs b/packages/autofmt/src/expr.rs new file mode 100644 index 000000000..d89a5cb39 --- /dev/null +++ b/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()?; +// } +// } diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs new file mode 100644 index 000000000..5b95e989a --- /dev/null +++ b/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 { + 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 { + let mut buf = Buffer { + src: block.lines().map(|f| f.to_string()).collect(), + ..Buffer::default() + }; + + let body = syn::parse_str::(block).unwrap(); + + buf.write_body_indented(&body.roots).unwrap(); + + buf.consume() +} diff --git a/packages/autofmt/src/util.rs b/packages/autofmt/src/util.rs new file mode 100644 index 000000000..9c84f98e1 --- /dev/null +++ b/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 { + 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 +} diff --git a/packages/autofmt/tests/sample.rs b/packages/autofmt/tests/sample.rs new file mode 100644 index 000000000..6b6b7d244 --- /dev/null +++ b/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::>() + .join("\n") +} + +#[test] +fn way_and_back() { + let blocks = dioxus_autofmt::fmt_file(SRC).into_iter().next().unwrap(); + + println!("{}", blocks.formatted); +} diff --git a/packages/autofmt/tests/samples/all.rs b/packages/autofmt/tests/samples/all.rs new file mode 100644 index 000000000..b0899709c --- /dev/null +++ b/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 {} + } + } + + // 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; + } + } +} diff --git a/packages/autofmt/tests/sink.rs b/packages/autofmt/tests/sink.rs new file mode 100644 index 000000000..3654b794a --- /dev/null +++ b/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); +} diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs index 90248ad1f..f4f1a9bc6 100644 --- a/packages/rsx/src/component.rs +++ b/packages/rsx/src/component.rs @@ -27,7 +27,7 @@ use syn::{ pub struct Component { pub name: syn::Path, pub prop_gen_args: Option, - pub body: Vec, + pub fields: Vec, pub children: Vec, pub manual_props: Option, } @@ -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" => { // diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index 38ab75575..456f45b54 100644 --- a/packages/rsx/src/element.rs +++ b/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 { diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index e8b754f13..f0b7c4222 100644 --- a/packages/rsx/src/node.rs +++ b/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 {