Ver código fonte

feat: manual props

Jonathan Kelley 3 anos atrás
pai
commit
fa756ba245

+ 4 - 0
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];

+ 170 - 26
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<AngleBracketedGenericArguments>,
     ) -> 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<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()?;
+            }
 
-        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<syn::Expr>) -> 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::<usize>() + 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;
 
-        if !body.is_empty() || !children.is_empty() {
-            self.tab()?;
+        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}")?;
         }
-        writeln!(self.buf, "}}")?;
+
         Ok(())
     }
 }

+ 17 - 16
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<usize> {
         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
 }

+ 0 - 1
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;

+ 49 - 9
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 { }

+ 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" => {
                             //