소스 검색

Fix #2104: fmt incorrectly using 1-indexing for columns (#2106)

* Fix #2104: fmt incorrectly using 1-indexing for columns

* Clippy...
Jonathan Kelley 1 년 전
부모
커밋
d8942a255b

+ 2 - 12
Cargo.lock

@@ -1982,8 +1982,8 @@ name = "dioxus-autofmt"
 version = "0.5.0-alpha.2"
 dependencies = [
  "dioxus-rsx",
- "prettier-please",
  "pretty_assertions",
+ "prettyplease",
  "proc-macro2",
  "quote",
  "serde",
@@ -2045,7 +2045,7 @@ dependencies = [
  "mlua",
  "notify",
  "open",
- "prettier-please",
+ "prettyplease",
  "rayon",
  "reqwest",
  "rsx-rosetta",
@@ -6670,16 +6670,6 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
 
-[[package]]
-name = "prettier-please"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22020dfcf177fcc7bf5deaf7440af371400c67c0de14c399938d8ed4fb4645d3"
-dependencies = [
- "proc-macro2",
- "syn 2.0.52",
-]
-
 [[package]]
 name = "pretty_assertions"
 version = "1.4.0"

+ 2 - 8
Cargo.toml

@@ -89,16 +89,10 @@ rustc-hash = "1.1.0"
 wasm-bindgen = "0.2.92"
 html_parser = "0.7.0"
 thiserror = "1.0.40"
-prettyplease = { package = "prettier-please", version = "0.2", features = [
-    "verbatim",
-] }
-manganis-cli-support = { version = "0.2.1", features = [
-    "html",
-] }
+prettyplease = { version = "0.2.16", features = ["verbatim"] }
+manganis-cli-support = { version = "0.2.1", features = ["html"] }
 manganis = { version = "0.2.1" }
-
 interprocess = { version = "1.2.1" }
-# interprocess = { git = "https://github.com/kotauskas/interprocess" }
 
 lru = "0.12.2"
 async-trait = "0.1.77"

+ 6 - 6
packages/autofmt/src/component.rs

@@ -1,4 +1,4 @@
-use crate::{ifmt_to_string, writer::Location, Writer};
+use crate::{ifmt_to_string, prettier_please::unparse_expr, writer::Location, Writer};
 use dioxus_rsx::*;
 use quote::ToTokens;
 use std::fmt::{Result, Write};
@@ -164,7 +164,7 @@ impl Writer<'_> {
             let name = &field.name;
             match &field.content {
                 ContentField::ManExpr(exp) => {
-                    let out = prettyplease::unparse_expr(exp);
+                    let out = unparse_expr(exp);
                     let mut lines = out.split('\n').peekable();
                     let first = lines.next().unwrap();
                     write!(self.out, "{name}: {first}")?;
@@ -186,7 +186,7 @@ impl Writer<'_> {
                     write!(self.out, "{}", e.to_token_stream())?;
                 }
                 ContentField::OnHandlerRaw(exp) => {
-                    let out = prettyplease::unparse_expr(exp);
+                    let out = unparse_expr(exp);
                     let mut lines = out.split('\n').peekable();
                     let first = lines.next().unwrap();
                     write!(self.out, "{name}: {first}")?;
@@ -228,7 +228,7 @@ impl Writer<'_> {
                 ContentField::Formatted(s) => ifmt_to_string(s).len() ,
                 ContentField::Shorthand(e) => e.to_token_stream().to_string().len(),
                 ContentField::OnHandlerRaw(exp) | ContentField::ManExpr(exp) => {
-                    let formatted = prettyplease::unparse_expr(exp);
+                    let formatted = unparse_expr(exp);
                     let len = if formatted.contains('\n') {
                         10000
                     } else {
@@ -242,7 +242,7 @@ impl Writer<'_> {
 
         match manual_props {
             Some(p) => {
-                let content = prettyplease::unparse_expr(p);
+                let content = unparse_expr(p);
                 if content.len() + attr_len > 80 {
                     return 100000;
                 }
@@ -264,7 +264,7 @@ impl Writer<'_> {
         We want to normalize the expr to the appropriate indent level.
         */
 
-        let formatted = prettyplease::unparse_expr(exp);
+        let formatted = unparse_expr(exp);
 
         let mut lines = formatted.lines();
 

+ 11 - 8
packages/autofmt/src/element.rs

@@ -1,4 +1,4 @@
-use crate::{ifmt_to_string, Writer};
+use crate::{ifmt_to_string, prettier_please::unparse_expr, Writer};
 use dioxus_rsx::*;
 use proc_macro2::Span;
 use quote::ToTokens;
@@ -112,7 +112,7 @@ impl Writer<'_> {
             ShortOptimization::Oneliner => {
                 write!(self.out, " ")?;
 
-                self.write_attributes(attributes, key, true)?;
+                self.write_attributes(brace, attributes, key, true)?;
 
                 if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
                     write!(self.out, ", ")?;
@@ -132,7 +132,7 @@ impl Writer<'_> {
                 if !attributes.is_empty() || key.is_some() {
                     write!(self.out, " ")?;
                 }
-                self.write_attributes(attributes, key, true)?;
+                self.write_attributes(brace, attributes, key, true)?;
 
                 if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
                     write!(self.out, ",")?;
@@ -145,7 +145,7 @@ impl Writer<'_> {
             }
 
             ShortOptimization::NoOpt => {
-                self.write_attributes(attributes, key, false)?;
+                self.write_attributes(brace, attributes, key, false)?;
 
                 if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
                     write!(self.out, ",")?;
@@ -166,6 +166,7 @@ impl Writer<'_> {
 
     fn write_attributes(
         &mut self,
+        brace: &Brace,
         attributes: &[AttributeType],
         key: &Option<IfmtInput>,
         sameline: bool,
@@ -187,9 +188,11 @@ impl Writer<'_> {
 
         while let Some(attr) = attr_iter.next() {
             self.out.indent_level += 1;
+
             if !sameline {
-                self.write_comments(attr.start())?;
+                self.write_attr_comments(brace, attr.start())?;
             }
+
             self.out.indent_level -= 1;
 
             if !sameline {
@@ -229,7 +232,7 @@ impl Writer<'_> {
                 write!(
                     self.out,
                     "if {condition} {{ ",
-                    condition = prettyplease::unparse_expr(condition),
+                    condition = unparse_expr(condition),
                 )?;
                 self.write_attribute_value(value)?;
                 write!(self.out, " }}")?;
@@ -241,7 +244,7 @@ impl Writer<'_> {
                 write!(self.out, "{value}",)?;
             }
             ElementAttrValue::AttrExpr(value) => {
-                let out = prettyplease::unparse_expr(value);
+                let out = unparse_expr(value);
                 let mut lines = out.split('\n').peekable();
                 let first = lines.next().unwrap();
 
@@ -308,7 +311,7 @@ impl Writer<'_> {
 
     fn write_spread_attribute(&mut self, attr: &Expr) -> Result {
         write!(self.out, "..")?;
-        write!(self.out, "{}", prettyplease::unparse_expr(attr))?;
+        write!(self.out, "{}", unparse_expr(attr))?;
 
         Ok(())
     }

+ 2 - 1
packages/autofmt/src/expr.rs

@@ -27,12 +27,13 @@ impl Writer<'_> {
         // If the expr is multiline, we want to collect all of its lines together and write them out properly
         // This involves unshifting the first line if it's aligned
         let first_line = &self.src[start.line - 1];
-        write!(self.out, "{}", &first_line[start.column - 1..].trim_start())?;
+        write!(self.out, "{}", &first_line[start.column..].trim_start())?;
 
         let prev_block_indent_level = self.out.indent.count_indents(first_line);
 
         for (id, line) in self.src[start.line..end.line].iter().enumerate() {
             writeln!(self.out)?;
+
             // check if this is the last line
             let line = {
                 if id == (end.line - start.line) - 1 {

+ 1 - 0
packages/autofmt/src/lib.rs

@@ -17,6 +17,7 @@ mod component;
 mod element;
 mod expr;
 mod indent;
+mod prettier_please;
 mod writer;
 
 pub use indent::{IndentOptions, IndentType};

+ 66 - 0
packages/autofmt/src/prettier_please.rs

@@ -0,0 +1,66 @@
+use prettyplease::unparse;
+use syn::{Expr, File, Item};
+
+/// Unparse an expression back into a string
+///
+/// This creates a new temporary file, parses the expression into it, and then formats the file.
+/// This is a bit of a hack, but dtonlay doesn't want to support this very simple usecase, forcing us to clone the expr
+pub fn unparse_expr(expr: &Expr) -> String {
+    let file = wrapped(expr);
+    let wrapped = unparse(&file);
+    unwrapped(wrapped)
+}
+
+// Split off the fn main and then cut the tabs off the front
+fn unwrapped(raw: String) -> String {
+    raw.strip_prefix("fn main() {\n")
+        .unwrap()
+        .strip_suffix("}\n")
+        .unwrap()
+        .lines()
+        .map(|line| line.strip_prefix("    ").unwrap()) // todo: set this to tab level
+        .collect::<Vec<_>>()
+        .join("\n")
+}
+
+fn wrapped(expr: &Expr) -> File {
+    File {
+        shebang: None,
+        attrs: vec![],
+        items: vec![
+            //
+            Item::Verbatim(quote::quote! {
+                fn main() {
+                    #expr
+                }
+            }),
+        ],
+    }
+}
+
+#[test]
+fn unparses_raw() {
+    let expr = syn::parse_str("1 + 1").unwrap();
+    let unparsed = unparse(&wrapped(&expr));
+    assert_eq!(unparsed, "fn main() {\n    1 + 1\n}\n");
+}
+
+#[test]
+fn unparses_completely() {
+    let expr = syn::parse_str("1 + 1").unwrap();
+    let unparsed = unparse_expr(&expr);
+    assert_eq!(unparsed, "1 + 1");
+}
+
+#[test]
+fn weird_ifcase() {
+    let contents = r##"
+    fn main() {
+        move |_| timer.with_mut(|t| if t.started_at.is_none() { Some(Instant::now()) } else { None })
+    }
+"##;
+
+    let expr: File = syn::parse_file(contents).unwrap();
+    let out = unparse(&expr);
+    println!("{}", out);
+}

+ 24 - 5
packages/autofmt/src/writer.rs

@@ -1,3 +1,4 @@
+use crate::prettier_please::unparse_expr;
 use dioxus_rsx::{AttributeType, BodyNode, ElementAttrValue, ForLoop, IfChain};
 use proc_macro2::{LineColumn, Span};
 use quote::ToTokens;
@@ -5,7 +6,7 @@ use std::{
     collections::{HashMap, VecDeque},
     fmt::{Result, Write},
 };
-use syn::{spanned::Spanned, Expr};
+use syn::{spanned::Spanned, token::Brace, Expr};
 
 use crate::buffer::Buffer;
 use crate::ifmt_to_string;
@@ -61,8 +62,26 @@ impl<'a> Writer<'a> {
         Some(self.out.buf)
     }
 
+    pub fn write_attr_comments(&mut self, brace: &Brace, attr_span: Span) -> Result {
+        // There's a chance this line actually shares the same line as the previous
+        // Only write comments if the comments actually belong to this line
+        //
+        // to do this, we check if the attr span starts on the same line as the brace
+        // if it doesn't, we write the comments
+        let brace_line = brace.span.span().start().line;
+        let attr_line = attr_span.start().line;
+
+        if brace_line != attr_line {
+            self.write_comments(attr_span)?;
+        }
+
+        Ok(())
+    }
+
     pub fn write_comments(&mut self, child: Span) -> Result {
         // collect all comments upwards
+        // make sure we don't collect the comments of the node that we're currently under.
+
         let start = child.start();
         let line_start = start.line - 1;
 
@@ -149,7 +168,7 @@ impl<'a> Writer<'a> {
                 let len = if let std::collections::hash_map::Entry::Vacant(e) =
                     self.cached_formats.entry(location)
                 {
-                    let formatted = prettyplease::unparse_expr(tokens);
+                    let formatted = unparse_expr(tokens);
                     let len = if formatted.contains('\n') {
                         10000
                     } else {
@@ -207,7 +226,7 @@ impl<'a> Writer<'a> {
     pub fn retrieve_formatted_expr(&mut self, expr: &Expr) -> &str {
         self.cached_formats
             .entry(Location::new(expr.span().start()))
-            .or_insert_with(|| prettyplease::unparse_expr(expr))
+            .or_insert_with(|| unparse_expr(expr))
             .as_str()
     }
 
@@ -216,7 +235,7 @@ impl<'a> Writer<'a> {
             self.out,
             "for {} in {} {{",
             forloop.pat.clone().into_token_stream(),
-            prettyplease::unparse_expr(&forloop.expr)
+            unparse_expr(&forloop.expr)
         )?;
 
         if forloop.body.is_empty() {
@@ -249,7 +268,7 @@ impl<'a> Writer<'a> {
                 self.out,
                 "{} {} {{",
                 if_token.to_token_stream(),
-                prettyplease::unparse_expr(cond)
+                unparse_expr(cond)
             )?;
 
             self.write_body_indented(then_branch)?;

+ 1 - 0
packages/autofmt/tests/samples.rs

@@ -45,4 +45,5 @@ twoway![
     tiny,
     tinynoopt,
     trailing_expr,
+    many_exprs,
 ];

+ 195 - 0
packages/autofmt/tests/samples/many_exprs.rsx

@@ -0,0 +1,195 @@
+#![allow(dead_code, unused)]
+use dioxus::desktop::use_window;
+use dioxus::prelude::*;
+use std::{
+    process::exit,
+    time::{Duration, Instant},
+};
+use tokio::time::sleep;
+
+fn main() {
+    LaunchBuilder::desktop().launch(app);
+}
+
+struct WindowPreferences {
+    always_on_top: bool,
+    with_decorations: bool,
+    exiting: Option<Instant>,
+}
+
+impl Default for WindowPreferences {
+    fn default() -> Self {
+        Self {
+            with_decorations: true,
+            always_on_top: false,
+            exiting: None,
+        }
+    }
+}
+
+impl WindowPreferences {
+    fn new() -> Self {
+        Self::default()
+    }
+}
+
+#[derive(Default)]
+struct Timer {
+    hours: u8,
+    minutes: u8,
+    seconds: u8,
+    started_at: Option<Instant>,
+}
+
+impl Timer {
+    fn new() -> Self {
+        Self::default()
+    }
+
+    fn duration(&self) -> Duration {
+        Duration::from_secs(
+            (self.hours as u64 * 60 + self.minutes as u64) * 60 + self.seconds as u64,
+        )
+    }
+}
+
+const UPD_FREQ: Duration = Duration::from_millis(100);
+
+fn exit_button(
+    delay: Duration,
+    label: fn(Signal<Option<Instant>>, Duration) -> Option<VNode>,
+) -> Element {
+    let mut trigger: Signal<Option<Instant>> = use_signal(|| None);
+    use_future(move || async move {
+        loop {
+            sleep(UPD_FREQ).await;
+            if let Some(true) = trigger.read().map(|e| e.elapsed() > delay) {
+                exit(0);
+            }
+        }
+    });
+    let stuff: Option<VNode> = rsx! {
+        button {
+            onmouseup: move |_| {
+                trigger.set(None);
+            },
+            onmousedown: move |_| {
+                trigger.set(Some(Instant::now()));
+            },
+            width: 100,
+            {label(trigger, delay)}
+        }
+    };
+    stuff
+}
+
+fn app() -> Element {
+    let mut timer = use_signal(Timer::new);
+    let mut window_preferences = use_signal(WindowPreferences::new);
+
+    use_future(move || async move {
+        loop {
+            sleep(UPD_FREQ).await;
+            timer.with_mut(|t| {
+                if let Some(started_at) = t.started_at {
+                    if t.duration().saturating_sub(started_at.elapsed()) == Duration::ZERO {
+                        t.started_at = None;
+                    }
+                }
+            });
+        }
+    });
+
+    rsx! {
+        div {
+            {
+                let millis = timer.with(|t| t.duration().saturating_sub(t.started_at.map(|x| x.elapsed()).unwrap_or(Duration::ZERO)).as_millis());
+                format!("{:02}:{:02}:{:02}.{:01}",
+                        millis / 1000 / 3600 % 3600,
+                        millis / 1000 / 60 % 60,
+                        millis / 1000 % 60,
+                        millis / 100 % 10)
+            }
+        }
+        div {
+            input {
+                r#type: "number",
+                min: 0,
+                max: 99,
+                value: format!("{:02}", timer.read().hours),
+                oninput: move |e| {
+                    timer.write().hours = e.value().parse().unwrap_or(0);
+                }
+            }
+
+            input {
+                r#type: "number",
+                min: 0,
+                max: 59,
+                value: format!("{:02}", timer.read().minutes),
+                oninput: move |e| {
+                    timer.write().minutes = e.value().parse().unwrap_or(0);
+                }
+            }
+
+            input {
+                r#type: "number",
+                min: 0,
+                max: 59,
+                value: format!("{:02}", timer.read().seconds),
+                oninput: move |e| {
+                    timer.write().seconds = e.value().parse().unwrap_or(0);
+                }
+            }
+        }
+
+        button {
+            id: "start_stop",
+            onclick: move |_| {
+                timer
+                    .with_mut(|t| {
+                        t
+                            .started_at = if t.started_at.is_none() {
+                            Some(Instant::now())
+                        } else {
+                            None
+                        }
+                    })
+            },
+            { timer.with(|t| if t.started_at.is_none() { "Start" } else { "Stop" }) }
+        }
+        div { id: "app",
+            button {
+                onclick: move |_| {
+                    let decorations = window_preferences.read().with_decorations;
+                    use_window().set_decorations(!decorations);
+                    window_preferences.write().with_decorations = !decorations;
+                },
+                {
+                    format!("with decorations{}", if window_preferences.read().with_decorations { " ✓" } else { "" }).to_string()
+                }
+            }
+            button {
+                onclick: move |_| {
+                    window_preferences
+                        .with_mut(|wp| {
+                            use_window().set_always_on_top(!wp.always_on_top);
+                            wp.always_on_top = !wp.always_on_top;
+                        })
+                },
+                width: 100,
+                {
+                    format!("always on top{}", if window_preferences.read().always_on_top { " ✓" } else { "" })
+                }
+            }
+        }
+        {
+            exit_button(
+                Duration::from_secs(3),
+                |trigger, delay| rsx! {
+                    {format!("{:0.1?}", trigger.read().map(|inst| (delay.as_secs_f32() - inst.elapsed().as_secs_f32()))) }
+                }
+            )
+        }
+    }
+}

+ 3 - 0
packages/autofmt/tests/wrong.rs

@@ -26,3 +26,6 @@ twoway!("multi-tab" => multi_tab (IndentOptions::new(IndentType::Tabs, 4, false)
 
 twoway!("multiexpr-4sp" => multiexpr_4sp (IndentOptions::new(IndentType::Spaces, 4, false)));
 twoway!("multiexpr-tab" => multiexpr_tab (IndentOptions::new(IndentType::Tabs, 4, false)));
+twoway!("multiexpr-many" => multiexpr_many (IndentOptions::new(IndentType::Spaces, 4, false)));
+twoway!("simple-combo-expr" => simple_combo_expr (IndentOptions::new(IndentType::Spaces, 4, false)));
+twoway!("oneline-expand" => online_expand (IndentOptions::new(IndentType::Spaces, 4, false)));

+ 195 - 0
packages/autofmt/tests/wrong/multiexpr-many.rsx

@@ -0,0 +1,195 @@
+#![allow(dead_code, unused)]
+use dioxus::desktop::use_window;
+use dioxus::prelude::*;
+use std::{
+    process::exit,
+    time::{Duration, Instant},
+};
+use tokio::time::sleep;
+
+fn main() {
+    LaunchBuilder::desktop().launch(app);
+}
+
+struct WindowPreferences {
+    always_on_top: bool,
+    with_decorations: bool,
+    exiting: Option<Instant>,
+}
+
+impl Default for WindowPreferences {
+    fn default() -> Self {
+        Self {
+            with_decorations: true,
+            always_on_top: false,
+            exiting: None,
+        }
+    }
+}
+
+impl WindowPreferences {
+    fn new() -> Self {
+        Self::default()
+    }
+}
+
+#[derive(Default)]
+struct Timer {
+    hours: u8,
+    minutes: u8,
+    seconds: u8,
+    started_at: Option<Instant>,
+}
+
+impl Timer {
+    fn new() -> Self {
+        Self::default()
+    }
+
+    fn duration(&self) -> Duration {
+        Duration::from_secs(
+            (self.hours as u64 * 60 + self.minutes as u64) * 60 + self.seconds as u64,
+        )
+    }
+}
+
+const UPD_FREQ: Duration = Duration::from_millis(100);
+
+fn exit_button(
+    delay: Duration,
+    label: fn(Signal<Option<Instant>>, Duration) -> Option<VNode>,
+) -> Element {
+    let mut trigger: Signal<Option<Instant>> = use_signal(|| None);
+    use_future(move || async move {
+        loop {
+            sleep(UPD_FREQ).await;
+            if let Some(true) = trigger.read().map(|e| e.elapsed() > delay) {
+                exit(0);
+            }
+        }
+    });
+    let stuff: Option<VNode> = rsx! {
+        button {
+            onmouseup: move |_| {
+                trigger.set(None);
+            },
+            onmousedown: move |_| {
+                trigger.set(Some(Instant::now()));
+            },
+            width: 100,
+            {label(trigger, delay)}
+        }
+    };
+    stuff
+}
+
+fn app() -> Element {
+    let mut timer = use_signal(Timer::new);
+    let mut window_preferences = use_signal(WindowPreferences::new);
+
+    use_future(move || async move {
+        loop {
+            sleep(UPD_FREQ).await;
+            timer.with_mut(|t| {
+                if let Some(started_at) = t.started_at {
+                    if t.duration().saturating_sub(started_at.elapsed()) == Duration::ZERO {
+                        t.started_at = None;
+                    }
+                }
+            });
+        }
+    });
+
+    rsx! {
+        div {
+            {
+                let millis = timer.with(|t| t.duration().saturating_sub(t.started_at.map(|x| x.elapsed()).unwrap_or(Duration::ZERO)).as_millis());
+                format!("{:02}:{:02}:{:02}.{:01}",
+                        millis / 1000 / 3600 % 3600,
+                        millis / 1000 / 60 % 60,
+                        millis / 1000 % 60,
+                        millis / 100 % 10)
+            }
+        }
+        div {
+            input {
+                r#type: "number",
+                min: 0,
+                max: 99,
+                value: format!("{:02}", timer.read().hours),
+                oninput: move |e| {
+                    timer.write().hours = e.value().parse().unwrap_or(0);
+                }
+            }
+
+            input {
+                r#type: "number",
+                min: 0,
+                max: 59,
+                value: format!("{:02}", timer.read().minutes),
+                oninput: move |e| {
+                    timer.write().minutes = e.value().parse().unwrap_or(0);
+                }
+            }
+
+            input {
+                r#type: "number",
+                min: 0,
+                max: 59,
+                value: format!("{:02}", timer.read().seconds),
+                oninput: move |e| {
+                    timer.write().seconds = e.value().parse().unwrap_or(0);
+                }
+            }
+        }
+
+        button {
+            id: "start_stop",
+            onclick: move |_| {
+                timer
+                    .with_mut(|t| {
+                        t
+                            .started_at = if t.started_at.is_none() {
+                            Some(Instant::now())
+                        } else {
+                            None
+                        };
+                    })
+            },
+            { timer.with(|t| if t.started_at.is_none() { "Start" } else { "Stop" }) }
+        }
+        div { id: "app",
+            button {
+                onclick: move |_| {
+                    let decorations = window_preferences.read().with_decorations;
+                    use_window().set_decorations(!decorations);
+                    window_preferences.write().with_decorations = !decorations;
+                },
+                {
+                    format!("with decorations{}", if window_preferences.read().with_decorations { " ✓" } else { "" }).to_string()
+                }
+            }
+            button {
+                onclick: move |_| {
+                    window_preferences
+                        .with_mut(|wp| {
+                            use_window().set_always_on_top(!wp.always_on_top);
+                            wp.always_on_top = !wp.always_on_top;
+                        })
+                },
+                width: 100,
+                {
+                    format!("always on top{}", if window_preferences.read().always_on_top { " ✓" } else { "" })
+                }
+            }
+        }
+        {
+            exit_button(
+                Duration::from_secs(3),
+                |trigger, delay| rsx! {
+                    {format!("{:0.1?}", trigger.read().map(|inst| (delay.as_secs_f32() - inst.elapsed().as_secs_f32()))) }
+                }
+            )
+        }
+    }
+}

+ 164 - 0
packages/autofmt/tests/wrong/multiexpr-many.wrong.rsx

@@ -0,0 +1,164 @@
+#![allow(dead_code, unused)]
+use dioxus::desktop::use_window;
+use dioxus::prelude::*;
+use std::{
+    process::exit,
+    time::{Duration, Instant},
+};
+use tokio::time::sleep;
+
+fn main() {
+    LaunchBuilder::desktop().launch(app);
+}
+
+struct WindowPreferences {
+    always_on_top: bool,
+    with_decorations: bool,
+    exiting: Option<Instant>,
+}
+
+impl Default for WindowPreferences {
+    fn default() -> Self {
+        Self {
+            with_decorations: true,
+            always_on_top: false,
+            exiting: None,
+        }
+    }
+}
+
+impl WindowPreferences {
+    fn new() -> Self {
+        Self::default()
+    }
+}
+
+#[derive(Default)]
+struct Timer {
+    hours: u8,
+    minutes: u8,
+    seconds: u8,
+    started_at: Option<Instant>,
+}
+
+impl Timer {
+    fn new() -> Self {
+        Self::default()
+    }
+
+    fn duration(&self) -> Duration {
+        Duration::from_secs(
+            (self.hours as u64 * 60 + self.minutes as u64) * 60 + self.seconds as u64,
+        )
+    }
+}
+
+const UPD_FREQ: Duration = Duration::from_millis(100);
+
+fn exit_button(
+    delay: Duration,
+    label: fn(Signal<Option<Instant>>, Duration) -> Option<VNode>,
+) -> Element {
+    let mut trigger: Signal<Option<Instant>> = use_signal(|| None);
+    use_future(move || async move {
+        loop {
+            sleep(UPD_FREQ).await;
+            if let Some(true) = trigger.read().map(|e| e.elapsed() > delay) {
+                exit(0);
+            }
+        }
+    });
+    let stuff: Option<VNode> = rsx! {
+        button {
+            onmouseup: move |_| {
+                trigger.set(None);
+            },
+            onmousedown: move |_| {
+                trigger.set(Some(Instant::now()));
+            },
+            width: 100,
+            {label(trigger, delay)},
+        }
+    };
+    stuff
+}
+
+fn app() -> Element {
+    let mut timer = use_signal(Timer::new);
+    let mut window_preferences = use_signal(WindowPreferences::new);
+
+    use_future(move || async move {
+        loop {
+            sleep(UPD_FREQ).await;
+            timer.with_mut(|t| {
+                if let Some(started_at) = t.started_at {
+                    if t.duration().saturating_sub(started_at.elapsed()) == Duration::ZERO {
+                        t.started_at = None;
+                    }
+                }
+            });
+        }
+    });
+
+    rsx! {
+        div {{
+            let millis = timer.with(|t| t.duration().saturating_sub(t.started_at.map(|x| x.elapsed()).unwrap_or(Duration::ZERO)).as_millis());
+            format!("{:02}:{:02}:{:02}.{:01}",
+                    millis / 1000 / 3600 % 3600,
+                    millis / 1000 / 60 % 60,
+                    millis / 1000 % 60,
+                    millis / 100 % 10)
+        }}
+        div {
+            input { r#type: "number", min: 0, max: 99, value: format!("{:02}",timer.read().hours), oninput: move |e| {
+                timer.write().hours = e.value().parse().unwrap_or(0);
+                }
+            }
+
+            input { r#type: "number", min: 0, max: 59, value: format!("{:02}",timer.read().minutes), oninput: move |e| {
+                timer.write().minutes = e.value().parse().unwrap_or(0);
+                }
+            }
+
+            input { r#type: "number", min: 0, max: 59, value: format!("{:02}",timer.read().seconds), oninput: move |e| {
+                timer.write().seconds = e.value().parse().unwrap_or(0);
+                }
+            }
+        }
+
+        button {
+            id: "start_stop",
+            onclick: move |_| timer.with_mut(|t| t.started_at = if t.started_at.is_none() { Some(Instant::now()) } else { None } ),
+            { timer.with(|t| if t.started_at.is_none() { "Start" } else { "Stop" }) },
+        }
+        div { id: "app",
+            button { onclick: move |_| {
+                let decorations = window_preferences.read().with_decorations;
+                use_window().set_decorations(!decorations);
+                window_preferences.write().with_decorations = !decorations;
+                }, {
+                    format!("with decorations{}", if window_preferences.read().with_decorations { " ✓" } else { "" }).to_string()
+                }
+            }
+            button {
+                onclick: move |_| {
+                    window_preferences.with_mut(|wp| {
+                        use_window().set_always_on_top(!wp.always_on_top);
+                        wp.always_on_top = !wp.always_on_top;
+                    })},
+                width: 100,
+                {
+                    format!("always on top{}", if window_preferences.read().always_on_top { " ✓" } else { "" })
+                }
+            }
+        }
+        {
+            exit_button(
+                Duration::from_secs(3),
+                |trigger, delay| rsx! {
+                    {format!("{:0.1?}", trigger.read().map(|inst| (delay.as_secs_f32() - inst.elapsed().as_secs_f32()))) }
+                }
+            )
+        }
+    }
+}

+ 18 - 0
packages/autofmt/tests/wrong/oneline-expand.rsx

@@ -0,0 +1,18 @@
+fn main() {
+    rsx! {
+        button {
+            id: "start_stop",
+            onclick: move |_| {
+                timer
+                    .with_mut(|t| {
+                        t
+                            .started_at = if t.started_at.is_none() {
+                            Some(Instant::now())
+                        } else {
+                            None
+                        };
+                    })
+            }
+        }
+    }
+}

+ 8 - 0
packages/autofmt/tests/wrong/oneline-expand.wrong.rsx

@@ -0,0 +1,8 @@
+fn main() {
+    rsx! {
+        button {
+            id: "start_stop",
+            onclick: move |_| timer.with_mut(|t| t.started_at = if t.started_at.is_none() { Some(Instant::now()) } else { None } )
+        }
+    }
+}

+ 35 - 0
packages/autofmt/tests/wrong/simple-combo-expr.rsx

@@ -0,0 +1,35 @@
+fn main() {
+    rsx! {
+        div {
+            {
+                let millis = timer.with(|t| t.duration().saturating_sub(t.started_at.map(|x| x.elapsed()).unwrap_or(Duration::ZERO)).as_millis());
+                format!("{:02}:{:02}:{:02}.{:01}",
+                        millis / 1000 / 3600 % 3600,
+                        millis / 1000 / 60 % 60,
+                        millis / 1000 % 60,
+                        millis / 100 % 10)
+            }
+        }
+        div {
+            input {
+                r#type: "number",
+                min: 0,
+                max: 99,
+                value: format!("{:02}", timer.read().hours),
+                oninput: move |e| {
+                    timer.write().hours = e.value().parse().unwrap_or(0);
+                }
+            }
+            // some comment
+            input {
+                r#type: "number",
+                min: 0,
+                max: 99,
+                value: format!("{:02}", timer.read().hours),
+                oninput: move |e| {
+                    timer.write().hours = e.value().parse().unwrap_or(0);
+                }
+            }
+        }
+    }
+}

+ 23 - 0
packages/autofmt/tests/wrong/simple-combo-expr.wrong.rsx

@@ -0,0 +1,23 @@
+fn main() {
+    rsx! {
+        div {{
+            let millis = timer.with(|t| t.duration().saturating_sub(t.started_at.map(|x| x.elapsed()).unwrap_or(Duration::ZERO)).as_millis());
+            format!("{:02}:{:02}:{:02}.{:01}",
+                    millis / 1000 / 3600 % 3600,
+                    millis / 1000 / 60 % 60,
+                    millis / 1000 % 60,
+                    millis / 100 % 10)
+        }}
+        div {
+            input { r#type: "number", min: 0, max: 99, value: format!("{:02}",timer.read().hours), oninput: move |e| {
+                timer.write().hours = e.value().parse().unwrap_or(0);
+                }
+            }
+            // some comment
+            input { r#type: "number", min: 0, max: 99, value: format!("{:02}",timer.read().hours), oninput: move |e| {
+                timer.write().hours = e.value().parse().unwrap_or(0);
+                }
+            }
+        }
+    }
+}