From fa756ba245449caf83705cc4a474ff7f420e5ee7 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 28 Jun 2022 17:16:26 -0400 Subject: [PATCH] feat: manual props --- packages/autofmt/src/buffer.rs | 4 + packages/autofmt/src/component.rs | 198 ++++++++++++++++++++++++++---- packages/autofmt/src/element.rs | 33 ++--- packages/autofmt/src/expr.rs | 1 - packages/autofmt/tests/sink.rs | 58 +++++++-- packages/rsx/src/component.rs | 8 +- 6 files changed, 245 insertions(+), 57 deletions(-) diff --git a/packages/autofmt/src/buffer.rs b/packages/autofmt/src/buffer.rs index b6769d8e2..0884657ef 100644 --- a/packages/autofmt/src/buffer.rs +++ b/packages/autofmt/src/buffer.rs @@ -83,6 +83,10 @@ impl Buffer { } } + 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]; diff --git a/packages/autofmt/src/component.rs b/packages/autofmt/src/component.rs index 5b312b739..de6e146bf 100644 --- a/packages/autofmt/src/component.rs +++ b/packages/autofmt/src/component.rs @@ -2,45 +2,151 @@ 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, - body, + 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); + 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()); - self.tab()?; + write!(self.buf, "{name}")?; - if let Some(generics) = prop_gen_args { + 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, " {{")?; - if !body.is_empty() || !children.is_empty() { - self.new_line()?; - } + 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()?; + } - 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)?; + write!(self.buf, "{}: {}", name, out)?; } ContentField::Formatted(s) => { - writeln!(self.buf, "{}: \"{}\",", name, s.value())?; + write!(self.buf, "{}: \"{}\"", name, s.value())?; } ContentField::OnHandlerRaw(exp) => { let out = prettyplease::unparse_expr(exp); @@ -52,33 +158,71 @@ impl Buffer { self.indented_tab()?; write!(self.buf, "{}", line)?; } - writeln!(self.buf, ",")?; + } + } + + if field_iter.peek().is_some() || manual_props.is_some() { + write!(self.buf, ",")?; + + if sameline { + write!(self.buf, " ")?; } } } 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)?; + if !sameline { + self.indented_tabbed_line()?; } - self.new_line()?; + self.write_manual_props(exp)?; } - for child in children { - self.write_indented_ident(child)?; + Ok(()) + } + fn is_short_fields(&self, fields: &[ComponentField], manual_props: &Option) -> bool { + 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 false; + } + let mut lines = content.lines(); + lines.next().unwrap(); + + lines.next().is_none() + } + None => attr_len < 80, + } + } + + fn write_manual_props(&mut self, exp: &syn::Expr) -> Result { + /* + We want to normalize the expr to the appropriate indent level. + */ + + use syn::spanned::Spanned; + + 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}")?; } - if !body.is_empty() || !children.is_empty() { - self.tab()?; - } - writeln!(self.buf, "}}")?; Ok(()) } } diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index a4af77249..ba8a17b5d 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -42,7 +42,7 @@ impl Buffer { // 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); + 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 { @@ -201,36 +201,37 @@ impl Buffer { // 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 - fn is_short_children(&self, children: &[BodyNode]) -> bool { + // 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 true; + return Some(0); } for child in children { 'line: for line in self.src[..child.span().start().line - 1].iter().rev() { - if line.trim().starts_with("//") { - return false; - } else if line.is_empty() { - continue; - } else { - break 'line; + match (line.trim().starts_with("//"), line.is_empty()) { + (true, _) => return None, + (_, true) => continue 'line, + _ => break 'line, } } } match children { - [BodyNode::Text(ref text)] => text.value().len() < 80, - [BodyNode::Element(ref el)] => { - extract_attr_len(&el.attributes) < 80 && self.is_short_children(&el.children) - } - _ => false, + [BodyNode::Text(ref text)] => Some(text.value().len()), + [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 { - let total_attr_len = extract_attr_len(attrs); - total_attr_len < 80 + extract_attr_len(attrs) < 80 } diff --git a/packages/autofmt/src/expr.rs b/packages/autofmt/src/expr.rs index 41a866ad9..d89a5cb39 100644 --- a/packages/autofmt/src/expr.rs +++ b/packages/autofmt/src/expr.rs @@ -19,7 +19,6 @@ impl Buffer { let num_spaces_desired = (self.indent * 4) as isize; let first = &self.src[start.line - 1]; - // 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; diff --git a/packages/autofmt/tests/sink.rs b/packages/autofmt/tests/sink.rs index 1b10c9799..025b3f59e 100644 --- a/packages/autofmt/tests/sink.rs +++ b/packages/autofmt/tests/sink.rs @@ -1,11 +1,5 @@ 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(); @@ -165,10 +159,12 @@ fn format_comments() { fn formats_component() { let block = r#" Component { - adsasd: "asd", // this is a comment + adsasd: "asd", + + // this is a comment onclick: move |_| { let blah = 120; - let blah = 120; + let blah = 122; }, } "#; @@ -178,6 +174,49 @@ fn formats_component() { println!("{formatted}"); } +#[test] +fn formats_component_complex() { + let block = r#" + 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" + } + } +"#; + + let formatted = fmt_block(block).unwrap(); + + println!("{formatted}"); +} + #[test] fn formats_element() { let block = r#" @@ -239,6 +278,7 @@ fn formats_element_nested() { #[test] fn formats_element_props_on_top() { + // Gets compressed because there's no real comments 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", @@ -580,7 +620,7 @@ fn NavItem<'a>(cx: Scope, to: &'static str, children: Element<'a>, icon: Shape) #[test] fn empty_blocks() { - let mut src = r###" + let src = r###" pub fn Alert(cx: Scope) -> Element { cx.render(rsx! { div { } 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" => { //