diff --git a/packages/autofmt/src/block.rs b/packages/autofmt/src/block.rs index 7930f1e15..2dad1b780 100644 --- a/packages/autofmt/src/block.rs +++ b/packages/autofmt/src/block.rs @@ -1,79 +1,8 @@ -use crate::{util::*, write_ident}; +use crate::{util::*, FormattedBlock}; use dioxus_rsx::*; use std::fmt::Write; use triple_accel::{levenshtein_search, Match}; -#[derive(serde::Deserialize, serde::Serialize, Clone, Debug, PartialEq, Hash)] -pub struct FormattedBlock { - pub formatted: String, - pub start: usize, - pub end: usize, -} - -pub fn get_format_blocks(contents: &str) -> Vec { - let matches = levenshtein_search(b"rsx! {", contents.as_bytes()).peekable(); - - let mut formatted_blocks = Vec::new(); - let mut last_bracket_end = 0; - - // find the rsx! marker - for Match { start, end, k } in matches { - // 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; - } - - // if we have code to push, we want the code to end up on the right lines with the right indentation - let mut output = String::new(); - writeln!(output).unwrap(); - - for line in new.lines() { - writeln!(output, "{}", line).ok(); - } - - formatted_blocks.push(FormattedBlock { - formatted: output, - start: end, - end: end + bracket_end - 1, - }); - } - - formatted_blocks -} - -struct Isolate<'a> { - contents: &'a str, - start: usize, - end: usize, -} -fn isolate_body_of_rsx(contents: &str, Match { start, end, k }: triple_accel::Match) -> Isolate { - todo!() -} - -pub fn fmt_block(block: &str) -> Option { - let mut buf = String::new(); - let lines = block.split('\n').collect::>(); - - for node in &syn::parse_str::(block).ok()?.roots { - write_ident(&mut buf, &lines, node, 0).ok()?; - } - - Some(buf) -} - #[test] fn format_block_basic() { let block = r#" @@ -129,7 +58,7 @@ fn format_block_basic() { // div { class: "asdasd", p { "hello!" } } - let edits = get_format_blocks(block); + // let edits = get_format_blocks(block); - println!("{}", edits[0].formatted); + // println!("{}", edits[0].formatted); } diff --git a/packages/autofmt/src/buffer.rs b/packages/autofmt/src/buffer.rs index dfd7166ba..e6f021beb 100644 --- a/packages/autofmt/src/buffer.rs +++ b/packages/autofmt/src/buffer.rs @@ -1,17 +1,83 @@ -use std::fmt::Result; +use std::fmt::{Result, Write}; -struct Buffer { - buf: String, - cur_line: usize, - cur_indent: usize, +use dioxus_rsx::BodyNode; + +pub struct Buffer { + pub buf: String, + pub line: usize, + pub indent: usize, } impl Buffer { - fn write_tabs(&mut self, num: usize) -> Result { - todo!() + pub fn new() -> Self { + Self { + buf: String::new(), + line: 0, + indent: 0, + } } - fn new_line(&mut self) -> Result { - todo!() + // 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_indented_ident(&mut self, lines: &[&str], node: &BodyNode) -> Result { + self.write_ident(lines, node)?; + Ok(()) + } + + pub fn write_ident(&mut self, lines: &[&str], node: &BodyNode) -> Result { + match node { + BodyNode::Element(el) => self.write_element(el, lines), + BodyNode::Component(component) => self.write_component(component, lines), + BodyNode::Text(text) => self.write_text(text), + BodyNode::RawExpr(exp) => self.write_raw_expr(exp, lines), + } + } + + pub fn write_text(&mut self, text: &syn::LitStr) -> Result { + write!(self.buf, "\"{}\"", text.value()) + } + + // Push out the indent level and write each component, line by line + pub fn write_body_indented(&mut self, children: &[BodyNode], lines: &[&str]) -> Result { + self.indent += 1; + for child in children { + // Exprs handle their own indenting/line breaks + if !matches!(child, BodyNode::RawExpr(_)) { + self.tabbed_line()?; + } + + self.write_ident(lines, child)?; + } + self.indent -= 1; + Ok(()) } } diff --git a/packages/autofmt/src/component.rs b/packages/autofmt/src/component.rs index df4e14006..ec17b4c22 100644 --- a/packages/autofmt/src/component.rs +++ b/packages/autofmt/src/component.rs @@ -1,78 +1,85 @@ -use crate::{util::*, write_ident}; +use crate::Buffer; use dioxus_rsx::*; use quote::ToTokens; -use std::fmt::{self, Write}; +use std::fmt::{self, Result, Write}; -pub fn write_component( - component: &Component, - buf: &mut String, - indent: usize, - lines: &[&str], -) -> Result<(), fmt::Error> { - let Component { - name, - body, - children, - manual_props, - prop_gen_args, - } = component; - let mut name = name.to_token_stream().to_string(); - name.retain(|c| !c.is_whitespace()); - write_tabs(buf, indent)?; - write!(buf, "{name}")?; - if let Some(generics) = prop_gen_args { - let mut written = generics.to_token_stream().to_string(); - written.retain(|c| !c.is_whitespace()); - write!(buf, "{}", written)?; - } - write!(buf, " {{")?; - if !body.is_empty() || !children.is_empty() { - writeln!(buf)?; - } - for field in body { - write_tabs(buf, indent + 1)?; - let name = &field.name; - match &field.content { - ContentField::ManExpr(exp) => { - let out = prettyplease::unparse_expr(exp); - writeln!(buf, "{}: {},", name, out)?; - } - ContentField::Formatted(s) => { - writeln!(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!(buf, "{}: {}", name, first)?; - for line in lines { - writeln!(buf)?; - write_tabs(buf, indent + 1)?; - write!(buf, "{}", line)?; +impl Buffer { + pub fn write_component( + &mut self, + Component { + name, + body, + children, + manual_props, + prop_gen_args, + }: &Component, + lines: &[&str], + ) -> Result { + let mut name = name.to_token_stream().to_string(); + name.retain(|c| !c.is_whitespace()); + self.tab()?; + write!(self.buf, "{name}")?; + + if let Some(generics) = prop_gen_args { + let mut written = generics.to_token_stream().to_string(); + written.retain(|c| !c.is_whitespace()); + write!(self.buf, "{}", written)?; + } + + write!(self.buf, " {{")?; + + if !body.is_empty() || !children.is_empty() { + self.new_line()?; + } + + for field in body { + self.indented_tab()?; + let name = &field.name; + match &field.content { + ContentField::ManExpr(exp) => { + let out = prettyplease::unparse_expr(exp); + writeln!(self.buf, "{}: {},", name, out)?; + } + ContentField::Formatted(s) => { + writeln!(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)?; + } + writeln!(self.buf, ",")?; } - writeln!(buf, ",")?; } } - } - if let Some(exp) = manual_props { - write_tabs(buf, indent + 1)?; - let out = prettyplease::unparse_expr(exp); - let mut lines = out.split('\n').peekable(); - let first = lines.next().unwrap(); - write!(buf, "..{}", first)?; - for line in lines { - writeln!(buf)?; - write_tabs(buf, indent + 1)?; - write!(buf, "{}", line)?; + + if let Some(exp) = manual_props { + self.indented_tab()?; + let out = prettyplease::unparse_expr(exp); + let mut lines = out.split('\n').peekable(); + let first = lines.next().unwrap(); + write!(self.buf, "..{}", first)?; + for line in lines { + self.new_line()?; + self.indented_tab()?; + write!(self.buf, "{}", line)?; + } + self.new_line()?; } - writeln!(buf)?; + + for child in children { + self.write_indented_ident(lines, child)?; + } + + if !body.is_empty() || !children.is_empty() { + self.tab()?; + } + writeln!(self.buf, "}}")?; + Ok(()) } - for child in children { - write_ident(buf, lines, child, indent + 1)?; - } - if !body.is_empty() || !children.is_empty() { - write_tabs(buf, indent)?; - } - writeln!(buf, "}}")?; - Ok(()) } diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index 4c4a00785..0180f15a4 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -1,6 +1,6 @@ -use crate::{util::*, write_ident}; +use crate::{util::*, Buffer}; use dioxus_rsx::*; -use std::{fmt, fmt::Result, fmt::Write}; +use std::{fmt::Result, fmt::Write}; enum ShortOptimization { // Special because we want to print the closing bracket immediately @@ -16,108 +16,181 @@ enum ShortOptimization { NoOpt, } -pub fn write_element( - Element { - name, - key, - attributes, - children, - _is_static, - }: &Element, - buf: &mut String, - lines: &[&str], - indent: usize, -) -> Result { - /* - 1. Write the tag - 2. Write the key - 3. Write the attributes - 4. Write the children - */ +impl Buffer { + pub fn write_element( + &mut self, + Element { + name, + key, + attributes, + children, + _is_static, + }: &Element, + lines: &[&str], + ) -> Result { + /* + 1. Write the tag + 2. Write the key + 3. Write the attributes + 4. Write the children + */ - write!(buf, "{name} {{")?; + 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; + // 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 = is_short_children(children); + // check if we have a lot of attributes + let is_short_attr_list = is_short_attrs(attributes); + let is_small_children = is_short_children(children); - // 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; - } - - // 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 => write!(buf, "}}")?, - ShortOptimization::Oneliner => { - write!(buf, " ")?; - write_attributes(buf, attributes, true, indent)?; - - if !children.is_empty() && !attributes.is_empty() { - write!(buf, ", ")?; - } - - // write the children - for child in children { - write_ident(buf, lines, child, indent + 1)?; - } - - write!(buf, " }}")?; + // 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; } - ShortOptimization::PropsOnTop => { - write!(buf, " ")?; - write_attributes(buf, attributes, true, indent)?; - - if !children.is_empty() && !attributes.is_empty() { - write!(buf, ",")?; + // 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; } - - // write the children - for child in children { - writeln!(buf)?; - write_tabs(buf, indent + 1)?; - write_ident(buf, lines, child, indent + 1)?; - } - - writeln!(buf)?; - write_tabs(buf, indent)?; - write!(buf, "}}")?; } - ShortOptimization::NoOpt => { - // write the key + // 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; + } - // write the attributes - write_attributes(buf, attributes, false, indent)?; + // If there's nothing at all, empty optimization + if attributes.is_empty() && children.is_empty() && key.is_none() { + opt_level = ShortOptimization::Empty; + } - // write the children - for child in children { - writeln!(buf)?; - write_tabs(buf, indent + 1)?; - write_ident(buf, lines, child, indent + 1)?; + match opt_level { + ShortOptimization::Empty => write!(self.buf, "}}")?, + ShortOptimization::Oneliner => { + write!(self.buf, " ")?; + self.write_attributes(attributes, true)?; + + if !children.is_empty() && !attributes.is_empty() { + write!(self.buf, ", ")?; + } + + // write the children + for child in children { + self.write_ident(lines, child)?; + } + + write!(self.buf, " }}")?; } - writeln!(buf)?; - write_tabs(buf, indent)?; - write!(buf, "}}")?; + ShortOptimization::PropsOnTop => { + write!(self.buf, " ")?; + self.write_attributes(attributes, true)?; + + if !children.is_empty() && !attributes.is_empty() { + write!(self.buf, ",")?; + } + + // write the children + self.write_body_indented(children, lines)?; + + self.tabbed_line()?; + write!(self.buf, "}}")?; + } + + ShortOptimization::NoOpt => { + // write the key + + // write the attributes + self.write_attributes(attributes, false)?; + + self.write_body_indented(children, lines)?; + + self.tabbed_line()?; + write!(self.buf, "}}")?; + } } + + Ok(()) } - Ok(()) + fn write_attributes(&mut self, attributes: &[ElementAttrNamed], sameline: bool) -> Result { + let mut attr_iter = attributes.iter().peekable(); + + 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(()) + } } fn is_short_attrs(attrs: &[ElementAttrNamed]) -> bool { @@ -132,106 +205,29 @@ fn is_short_children(children: &[BodyNode]) -> bool { return true; } - if children.len() == 1 { - if let BodyNode::Text(ref text) = &children[0] { - return text.value().len() < 80; - } - } + match children { + [BodyNode::Text(ref text)] => text.value().len() < 80, + [BodyNode::Element(ref el)] => { + // && !el.attributes.iter().any(|f| f.attr.is_expr()) - false + extract_attr_len(&el.attributes) < 80 && is_short_children(&el.children) + } + _ => false, + } } fn write_key() { // if let Some(key) = key.as_ref().map(|f| f.value()) { // if is_long_attr_list { - // writeln!(buf)?; - // write_tabs(buf, indent + 1)?; + // self.new_line()?; + // self.write_tabs( indent + 1)?; // } else { - // write!(buf, " ")?; + // write!(self.buf, " ")?; // } - // write!(buf, "key: \"{key}\"")?; + // write!(self.buf, "key: \"{key}\"")?; // if !attributes.is_empty() { - // write!(buf, ",")?; + // write!(self.buf, ",")?; // } // } } - -fn write_attributes( - buf: &mut String, - attributes: &[ElementAttrNamed], - sameline: bool, - indent: usize, -) -> Result { - let mut attr_iter = attributes.iter().peekable(); - - while let Some(attr) = attr_iter.next() { - write_attribute(buf, attr, indent)?; - - if attr_iter.peek().is_some() { - write!(buf, ",")?; - - if sameline { - write!(buf, " ")?; - } else { - writeln!(buf)?; - write_tabs(buf, indent + 1)?; - } - } - } - - Ok(()) -} - -fn write_attribute(buf: &mut String, attr: &ElementAttrNamed, indent: usize) -> Result { - match &attr.attr { - ElementAttr::AttrText { name, value } => { - write!(buf, "{name}: \"{value}\"", value = value.value())?; - } - ElementAttr::AttrExpression { name, value } => { - let out = prettyplease::unparse_expr(value); - write!(buf, "{}: {}", name, out)?; - } - - ElementAttr::CustomAttrText { name, value } => { - write!( - buf, - "\"{name}\": \"{value}\"", - name = name.value(), - value = value.value() - )?; - } - - ElementAttr::CustomAttrExpression { name, value } => { - let out = prettyplease::unparse_expr(value); - write!(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!(buf, "{}: {}", name, first)?; - } else { - writeln!(buf, "{}: {}", name, first)?; - - while let Some(line) = lines.next() { - write_tabs(buf, indent + 1)?; - write!(buf, "{}", line)?; - if lines.peek().is_none() { - write!(buf, "")?; - } else { - writeln!(buf)?; - } - } - } - } - } - - Ok(()) -} diff --git a/packages/autofmt/src/expr.rs b/packages/autofmt/src/expr.rs index 7cc0ed17a..cf75e7d87 100644 --- a/packages/autofmt/src/expr.rs +++ b/packages/autofmt/src/expr.rs @@ -1,39 +1,53 @@ //! pretty printer for rsx! -use std::fmt::{self, Write}; +use std::fmt::{self, Result, Write}; -pub fn write_raw_expr( - exp: &syn::Expr, - indent: usize, - lines: &[&str], - buf: &mut String, -) -> Result<(), fmt::Error> { - use syn::spanned::Spanned; - let placement = exp.span(); - let start = placement.start(); - let end = placement.end(); - let num_spaces_desired = (indent * 4) as isize; - let first = lines[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; +use crate::Buffer; - for line_id in start.line - 1..end.line { - let line = lines[line_id]; +impl Buffer { + pub fn write_raw_expr(&mut self, exp: &syn::Expr, lines: &[&str]) -> Result { + /* + We want to normalize the expr to the appropriate indent level. + */ - // trim the leading whitespace + // in a perfect world, just fire up the rust pretty printer + // pretty_print_rust_code_as_if_it_were_rustfmt() - if offset < 0 { - for _ in 0..-offset { - write!(buf, " ")?; + 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 = lines[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 &lines[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)?; } - - writeln!(buf, "{}", line)?; - } else { - let offset = offset as usize; - - let right = &line[offset..]; - writeln!(buf, "{}", right)?; } - } - Ok(()) + 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/ident.rs b/packages/autofmt/src/ident.rs deleted file mode 100644 index e4dee56c9..000000000 --- a/packages/autofmt/src/ident.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::{ - component::write_component, element::write_element, expr::write_raw_expr, util::write_tabs, -}; -use dioxus_rsx::*; -use std::fmt::{self, Write}; - -pub fn write_ident( - buf: &mut String, - lines: &[&str], - node: &BodyNode, - indent: usize, -) -> fmt::Result { - match node { - BodyNode::Element(el) => write_element(el, buf, lines, indent), - BodyNode::Component(component) => write_component(component, buf, indent, lines), - BodyNode::Text(text) => write_text(text, buf, indent), - BodyNode::RawExpr(exp) => write_raw_expr(exp, indent, lines, buf), - } -} - -fn write_text(text: &syn::LitStr, buf: &mut String, indent: usize) -> fmt::Result { - write!(buf, "\"{}\"", text.value()) -} diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs index 6d6c88819..6a9e5c594 100644 --- a/packages/autofmt/src/lib.rs +++ b/packages/autofmt/src/lib.rs @@ -1,8 +1,7 @@ //! pretty printer for rsx code -use dioxus_rsx::*; -use proc_macro2::TokenStream as TokenStream2; -use quote::ToTokens; +pub use crate::buffer::*; +use crate::util::*; mod block; mod buffer; @@ -10,8 +9,75 @@ mod children; mod component; mod element; mod expr; -mod ident; mod util; -pub use block::{fmt_block, get_format_blocks}; -pub use ident::write_ident; +// pub use block::{fmt_block, get_format_blocks}; + +/// 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, k, 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::new(); + let lines = block.split('\n').collect::>(); + + for node in &syn::parse_str::(block).ok()?.roots { + buf.write_ident(&lines, node).ok()?; + } + + Some(buf.buf) +} diff --git a/packages/autofmt/src/util.rs b/packages/autofmt/src/util.rs index 9a358e959..96db23697 100644 --- a/packages/autofmt/src/util.rs +++ b/packages/autofmt/src/util.rs @@ -14,13 +14,6 @@ pub fn extract_attr_len(attributes: &[ElementAttrNamed]) -> usize { .sum() } -pub fn write_tabs(f: &mut dyn Write, num: usize) -> std::fmt::Result { - for _ in 0..num { - write!(f, " ")? - } - Ok(()) -} - pub fn find_bracket_end(contents: &str) -> Option { let mut depth = 0; let mut i = 0; diff --git a/packages/autofmt/tests/sink.rs b/packages/autofmt/tests/sink.rs index 3f4df0a4e..c42c5de9d 100644 --- a/packages/autofmt/tests/sink.rs +++ b/packages/autofmt/tests/sink.rs @@ -2,6 +2,16 @@ use dioxus_autofmt::*; use proc_macro2::TokenStream as TokenStream2; use syn::{Attribute, Meta}; +fn test_block(wrong: &str, right: &str) { + let formatted = fmt_block(wrong).unwrap(); + assert_eq!(formatted, right); +} + +fn print_block(wrong: &str) { + let formatted = fmt_block(wrong).unwrap(); + println!("{}", formatted); +} + #[test] fn formats_block() { let block = r#" @@ -46,7 +56,7 @@ fn formats_block() { let formatted = fmt_block(block).unwrap(); - print!("{formatted}"); + println!("{formatted}"); } #[test] @@ -62,6 +72,54 @@ fn parse_comment() { dbg!(parsed); } +#[test] +fn print_cases() { + print_block( + r#" + 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" + } + } + } + } + "#, + ); +} + #[test] fn formats_component() { let block = r#" @@ -76,7 +134,7 @@ fn formats_component() { let formatted = fmt_block(block).unwrap(); - print!("{formatted}"); + println!("{formatted}"); } #[test] @@ -97,7 +155,7 @@ fn formats_element() { let formatted = fmt_block(block).unwrap(); - print!("{formatted}"); + println!("{formatted}"); } #[test] @@ -118,7 +176,7 @@ fn formats_element_short() { let formatted = fmt_block(block).unwrap(); - print!("{formatted}"); + println!("{formatted}"); } #[test] @@ -132,7 +190,25 @@ fn formats_element_nested() { let formatted = fmt_block(block).unwrap(); - print!("{formatted}"); + assert_eq!( + formatted, + r#"h3 { class: "mb-2 text-xl font-bold", "Invite Member" }"# + ); +} + +#[test] +fn formats_element_props_on_top() { + let block = r#" + 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" + } +"#; + + let formatted = fmt_block(block).unwrap(); + + println!("{formatted}"); } #[test] @@ -143,7 +219,10 @@ fn formats_element_nested_no_trailing_tabs() { let formatted = fmt_block(block).unwrap(); - print!("{formatted}"); + assert_eq!( + formatted, + r#"img { class: "mb-6 mx-auto h-24", src: "artemis-assets/images/friends.png", alt: "" }"# + ); } #[test] @@ -160,20 +239,20 @@ fn formats_element_with_correct_indent() { let formatted = fmt_block(block).unwrap(); - print!("{formatted}"); + println!("{formatted}"); } #[test] fn small_elements_and_text_are_small() { let block = r###" - a { class: " text-white", + a { class: "text-white", "Send invitation" } "###; let formatted = fmt_block(block).unwrap(); - print!("{formatted}"); + assert_eq!(formatted, r#"a { class: "text-white", "Send invitation" }"#); } #[test] @@ -192,7 +271,7 @@ fn formats_component_man_props() { let formatted = fmt_block(block).unwrap(); - print!("{formatted}"); + println!("{formatted}"); } #[test] @@ -204,24 +283,24 @@ fn formats_component_tiny() { let formatted = fmt_block(block).unwrap(); - print!("{formatted}"); + println!("{formatted}"); } #[test] fn formats_exprs() { let block = r#" ul { - (0..10).map(|f| rsx!{ - li { - "hi" - } + div {} + (0..10).map(|f| rsx! { + li { "hi" } }) + div {} } "#; let formatted = fmt_block(block).unwrap(); - print!("{formatted}"); + println!("{formatted}"); } #[test] @@ -238,7 +317,7 @@ fn formats_exprs_neg_indent() { let formatted = fmt_block(block).unwrap(); - print!("{formatted}"); + println!("{formatted}"); } #[test] @@ -261,7 +340,7 @@ fn formats_exprs_handlers() { let formatted = fmt_block(block).unwrap(); - print!("{formatted}"); + println!("{formatted}"); } #[test] @@ -296,7 +375,7 @@ fn formats_complex() { let formatted = fmt_block(block).unwrap(); - print!("{formatted}"); + println!("{formatted}"); } #[test] @@ -314,9 +393,9 @@ rsx!{ "#; - let formatted = get_format_blocks(block); + let formatted = fmt_file(block); - print!("{formatted:?}"); + println!("{formatted:?}"); } #[test] @@ -332,9 +411,9 @@ rsx!{ } "#; - let formatted = get_format_blocks(block); + let formatted = fmt_file(block); - print!("{formatted:?}"); + println!("{formatted:?}"); } #[test] @@ -350,7 +429,7 @@ rsx! { } "#; - let formatted = get_format_blocks(src); + let formatted = fmt_file(src); println!("{formatted:?}"); } @@ -378,7 +457,7 @@ fn NavItem<'a>(cx: Scope, to: &'static str, children: Element<'a>, icon: Shape) "# .to_string(); - let formatted = get_format_blocks(&src); + let formatted = fmt_file(&src); let block = formatted.into_iter().next().unwrap(); @@ -446,7 +525,7 @@ fn NavItem<'a>(cx: Scope, to: &'static str, children: Element<'a>, icon: Shape) "# .to_string(); - let formatted = get_format_blocks(&src); + let formatted = fmt_file(&src); dbg!(&formatted); @@ -469,7 +548,7 @@ pub fn Alert(cx: Scope) -> Element { "### .to_string(); - let formatted = get_format_blocks(&src); + let formatted = fmt_file(&src); dbg!(&formatted); } 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 {