Fix: #2604, Fix: #2240, Fix: #2341, Fix #1355 - Better error handling and and spaces handling in autofmt (#2736)

* add new autofmt sample
* Feat: implement rustfmt::skip support for rsx
* generally improve error handling with better expect messages
* wip: nested rsx formatting and expression formatting
* nested rsx formatting works
* collapse autofmt crate
* cast indent through macros
* use proper whitespace
* no more eating comments!
* Use proper error handling
This commit is contained in:
Jonathan Kelley 2024-07-30 18:36:13 -07:00 committed by GitHub
parent ef0202f999
commit 828cc502f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 1621 additions and 1055 deletions

1
Cargo.lock generated
View file

@ -2616,6 +2616,7 @@ dependencies = [
"dioxus-autofmt",
"html_parser",
"rsx-rosetta",
"syn 2.0.72",
"wasm-bindgen",
]

View file

@ -104,7 +104,7 @@ wasm-bindgen = "0.2.92"
wasm-bindgen-futures = "0.4.42"
html_parser = "0.7.0"
thiserror = "1.0.40"
prettyplease = { version = "0.2.16", features = ["verbatim"] }
prettyplease = { version = "0.2.20", features = ["verbatim"] }
manganis-cli-support = { git = "hhttps://github.com/DioxusLabs/manganis", features = ["html"] }
manganis = { git = "https://github.com/DioxusLabs/manganis" }
const_format = "0.2.32"

View file

@ -1,8 +1,5 @@
//! The output buffer that supports some helpful methods
//! These are separate from the input so we can lend references between the two
//!
//!
//!
use std::fmt::{Result, Write};

View file

@ -3,37 +3,66 @@
//! Returns all macros that match a pattern. You can use this information to autoformat them later
use proc_macro2::LineColumn;
use syn::{visit::Visit, File, Macro};
use syn::{visit::Visit, File, Macro, Meta};
type CollectedMacro<'a> = &'a Macro;
pub fn collect_from_file(file: &File) -> Vec<CollectedMacro<'_>> {
let mut macros = vec![];
MacroCollector::visit_file(
&mut MacroCollector {
macros: &mut macros,
},
file,
);
let mut collector = MacroCollector::new(&mut macros);
MacroCollector::visit_file(&mut collector, file);
macros
}
struct MacroCollector<'a, 'b> {
macros: &'a mut Vec<CollectedMacro<'b>>,
skip_count: usize,
}
impl<'a, 'b> MacroCollector<'a, 'b> {
fn new(macros: &'a mut Vec<CollectedMacro<'b>>) -> Self {
Self {
macros,
skip_count: 0,
}
}
}
impl<'a, 'b> Visit<'b> for MacroCollector<'a, 'b> {
fn visit_macro(&mut self, i: &'b Macro) {
if let Some("rsx" | "render") = i
.path
.segments
.last()
.map(|i| i.ident.to_string())
.as_deref()
{
self.macros.push(i)
// Visit the regular stuff - this will also ensure paths/attributes are visited
syn::visit::visit_macro(self, i);
let name = &i.path.segments.last().map(|i| i.ident.to_string());
if let Some("rsx" | "render") = name.as_deref() {
if self.skip_count == 0 {
self.macros.push(i)
}
}
}
// attributes can occur on stmts and items - we need to make sure the stack is reset when we exit
// this means we save the skipped length and set it back to its original length
fn visit_stmt(&mut self, i: &'b syn::Stmt) {
let skipped_len = self.skip_count;
syn::visit::visit_stmt(self, i);
self.skip_count = skipped_len;
}
fn visit_item(&mut self, i: &'b syn::Item) {
let skipped_len = self.skip_count;
syn::visit::visit_item(self, i);
self.skip_count = skipped_len;
}
fn visit_attribute(&mut self, i: &'b syn::Attribute) {
// we need to communicate that this stmt is skipped up the tree
if attr_is_rustfmt_skip(i) {
self.skip_count += 1;
}
syn::visit::visit_attribute(self, i);
}
}
pub fn byte_offset(input: &str, location: LineColumn) -> usize {
@ -49,10 +78,32 @@ pub fn byte_offset(input: &str, location: LineColumn) -> usize {
.sum::<usize>()
}
/// Check if an attribute is a rustfmt skip attribute
fn attr_is_rustfmt_skip(i: &syn::Attribute) -> bool {
match &i.meta {
Meta::Path(path) => {
path.segments.len() == 2
&& matches!(i.style, syn::AttrStyle::Outer)
&& path.segments[0].ident == "rustfmt"
&& path.segments[1].ident == "skip"
}
_ => false,
}
}
#[test]
fn parses_file_and_collects_rsx_macros() {
let contents = include_str!("../tests/samples/long.rsx");
let parsed = syn::parse_file(contents).unwrap();
let parsed = syn::parse_file(contents).expect("parse file okay");
let macros = collect_from_file(&parsed);
assert_eq!(macros.len(), 3);
}
/// Ensure that we only collect non-skipped macros
#[test]
fn dont_collect_skipped_macros() {
let contents = include_str!("../tests/samples/skip.rsx");
let parsed = syn::parse_file(contents).expect("parse file okay");
let macros = collect_from_file(&parsed);
assert_eq!(macros.len(), 2);
}

View file

@ -4,14 +4,13 @@
use crate::writer::*;
use dioxus_rsx::{BodyNode, CallBody};
use proc_macro2::LineColumn;
use syn::{parse::Parser, ExprMacro};
use proc_macro2::{LineColumn, Span};
use syn::parse::Parser;
mod buffer;
mod collect_macros;
mod indent;
mod prettier_please;
mod rsx_block;
mod writer;
pub use indent::{IndentOptions, IndentType};
@ -37,6 +36,16 @@ pub struct FormattedBlock {
pub end: usize,
}
/// Format a file into a list of `FormattedBlock`s to be applied by an IDE for autoformatting.
///
/// It accepts
#[deprecated(note = "Use try_fmt_file instead - this function panics on error.")]
pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
let parsed =
syn::parse_file(contents).expect("fmt_file should only be called on valid syn::File files");
try_fmt_file(contents, &parsed, indent).expect("Failed to format file")
}
/// 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.
@ -45,19 +54,27 @@ pub struct FormattedBlock {
/// back to the file precisely.
///
/// Nested blocks of RSX will be handled automatically
pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
///
/// This returns an error if the rsx itself is invalid.
///
/// Will early return if any of the expressions are not complete. Even though we *could* return the
/// expressions, eventually we'll want to pass off expression formatting to rustfmt which will reject
/// those.
pub fn try_fmt_file(
contents: &str,
parsed: &syn::File,
indent: IndentOptions,
) -> syn::Result<Vec<FormattedBlock>> {
let mut formatted_blocks = Vec::new();
let parsed = syn::parse_file(contents).unwrap();
let macros = collect_macros::collect_from_file(&parsed);
let macros = collect_macros::collect_from_file(parsed);
// No macros, no work to do
if macros.is_empty() {
return formatted_blocks;
return Ok(formatted_blocks);
}
let mut writer = Writer::new(contents);
writer.out.indent = indent;
let mut writer = Writer::new(contents, indent);
// Don't parse nested macros
let mut end_span = LineColumn { column: 0, line: 0 };
@ -69,14 +86,7 @@ pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
continue;
}
let body = match item.parse_body_with(CallBody::parse_strict) {
Ok(v) => v,
//there is aparsing error, we give up and don't format the rsx
Err(e) => {
eprintln!("Error while parsing rsx {:?} ", e);
return formatted_blocks;
}
};
let body = item.parse_body_with(CallBody::parse_strict)?;
let rsx_start = macro_path.span().start();
@ -86,15 +96,16 @@ pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
.count_indents(writer.src[rsx_start.line - 1]);
// TESTME
// If we fail to parse this macro then we have no choice to give up and return what we've got
// Writing *should* not fail but it's possible that it does
if writer.write_rsx_call(&body.body).is_err() {
return formatted_blocks;
let span = writer.invalid_exprs.pop().unwrap_or_else(Span::call_site);
return Err(syn::Error::new(span, "Failed emit valid rsx - likely due to partially complete expressions in the rsx! macro"));
}
// writing idents leaves the final line ended at the end of the last ident
if writer.out.buf.contains('\n') {
writer.out.new_line().unwrap();
writer.out.tab().unwrap();
_ = writer.out.new_line();
_ = writer.out.tab();
}
let span = item.delimiter.span().join();
@ -124,7 +135,7 @@ pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
});
}
formatted_blocks
Ok(formatted_blocks)
}
/// Write a Callbody (the rsx block) to a string
@ -132,14 +143,7 @@ pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
/// If the tokens can't be formatted, this returns None. This is usually due to an incomplete expression
/// that passed partial expansion but failed to parse.
pub fn write_block_out(body: &CallBody) -> Option<String> {
let mut buf = Writer::new("");
buf.write_rsx_call(&body.body).ok()?;
buf.consume()
}
pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option<String> {
let body = CallBody::parse_strict.parse2(expr.mac.tokens).unwrap();
let mut buf = Writer::new(raw);
let mut buf = Writer::new("", IndentOptions::default());
buf.write_rsx_call(&body.body).ok()?;
buf.consume()
}
@ -147,8 +151,7 @@ pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option<String> {
pub fn fmt_block(block: &str, indent_level: usize, indent: IndentOptions) -> Option<String> {
let body = CallBody::parse_strict.parse_str(block).unwrap();
let mut buf = Writer::new(block);
buf.out.indent = indent;
let mut buf = Writer::new(block, indent);
buf.out.indent_level = indent_level;
buf.write_rsx_call(&body.body).ok()?;

View file

@ -1,142 +1,141 @@
use prettyplease::unparse;
use syn::{visit_mut::VisitMut, Expr, File, Item};
use dioxus_rsx::CallBody;
use syn::{parse::Parser, visit_mut::VisitMut, Expr, File, Item};
use crate::Writer;
use crate::{IndentOptions, Writer};
impl Writer<'_> {
pub fn unparse_expr(&mut self, expr: &Expr) -> String {
struct ReplaceMacros<'a, 'b> {
writer: &'a mut Writer<'b>,
formatted_stack: Vec<String>,
}
unparse_expr(expr, self.raw_src, &self.out.indent)
}
}
impl VisitMut for ReplaceMacros<'_, '_> {
fn visit_stmt_mut(&mut self, _expr: &mut syn::Stmt) {
if let syn::Stmt::Macro(i) = _expr {
// replace the macro with a block that roughly matches the macro
if let Some("rsx" | "render") = i
.mac
.path
.segments
.last()
.map(|i| i.ident.to_string())
.as_deref()
{
// format the macro in place
// we'll use information about the macro to replace it with another formatted block
// once we've written out the unparsed expr from prettyplease, we can replace
// this dummy block with the actual formatted block
let formatted = crate::fmt_block_from_expr(
self.writer.raw_src,
syn::ExprMacro {
attrs: i.attrs.clone(),
mac: i.mac.clone(),
},
)
.unwrap();
const MARKER: &str = "dioxus_autofmt_block__________";
const MARKER_REPLACE: &str = "dioxus_autofmt_block__________! {}";
*_expr = syn::Stmt::Expr(
syn::parse_quote!(dioxus_autofmt_block__________),
i.semi_token,
);
pub fn unparse_expr(expr: &Expr, src: &str, cfg: &IndentOptions) -> String {
struct ReplaceMacros<'a> {
src: &'a str,
formatted_stack: Vec<String>,
cfg: &'a IndentOptions,
}
// Save this formatted block for later, when we apply it to the original expr
self.formatted_stack.push(formatted);
}
impl VisitMut for ReplaceMacros<'_> {
fn visit_macro_mut(&mut self, i: &mut syn::Macro) {
// replace the macro with a block that roughly matches the macro
if let Some("rsx" | "render") = i
.path
.segments
.last()
.map(|i| i.ident.to_string())
.as_deref()
{
// format the macro in place
// we'll use information about the macro to replace it with another formatted block
// once we've written out the unparsed expr from prettyplease, we can replace
// this dummy block with the actual formatted block
let body = CallBody::parse_strict.parse2(i.tokens.clone()).unwrap();
let multiline = !Writer::is_short_rsx_call(&body.body.roots);
let mut formatted = {
let mut writer = Writer::new(self.src, self.cfg.clone());
_ = writer.write_body_nodes(&body.body.roots).ok();
writer.consume()
}
.unwrap();
// always push out the rsx to require a new line
i.path = syn::parse_str(MARKER).unwrap();
i.tokens = Default::default();
// Push out the indent level of the formatted block if it's multiline
if multiline || formatted.contains('\n') {
formatted = formatted
.lines()
.map(|line| format!("{}{line}", self.cfg.indent_str()))
.collect::<Vec<_>>()
.join("\n");
}
syn::visit_mut::visit_stmt_mut(self, _expr);
// Save this formatted block for later, when we apply it to the original expr
self.formatted_stack.push(formatted)
}
fn visit_expr_mut(&mut self, _expr: &mut syn::Expr) {
if let syn::Expr::Macro(i) = _expr {
// replace the macro with a block that roughly matches the macro
if let Some("rsx" | "render") = i
.mac
.path
.segments
.last()
.map(|i| i.ident.to_string())
.as_deref()
{
// format the macro in place
// we'll use information about the macro to replace it with another formatted block
// once we've written out the unparsed expr from prettyplease, we can replace
// this dummy block with the actual formatted block
let formatted = crate::fmt_block_from_expr(
self.writer.raw_src,
syn::ExprMacro {
attrs: i.attrs.clone(),
mac: i.mac.clone(),
},
)
.unwrap();
syn::visit_mut::visit_macro_mut(self, i);
}
}
*_expr = syn::parse_quote!(dioxus_autofmt_block__________);
// Visit the expr and replace the macros with formatted blocks
let mut replacer = ReplaceMacros {
src,
cfg,
formatted_stack: vec![],
};
// Save this formatted block for later, when we apply it to the original expr
self.formatted_stack.push(formatted);
}
}
// builds the expression stack
let mut modified_expr = expr.clone();
replacer.visit_expr_mut(&mut modified_expr);
syn::visit_mut::visit_expr_mut(self, _expr);
// now unparsed with the modified expression
let mut unparsed = unparse_inner(&modified_expr);
// now we can replace the macros with the formatted blocks
for fmted in replacer.formatted_stack.drain(..) {
let is_multiline = fmted.contains('{');
let mut out_fmt = String::from("rsx! {");
if is_multiline {
out_fmt.push('\n');
} else {
out_fmt.push(' ');
}
let mut whitespace = 0;
for line in unparsed.lines() {
if line.contains(MARKER) {
whitespace = line.matches(cfg.indent_str()).count();
break;
}
}
// Visit the expr and replace the macros with formatted blocks
let mut replacer = ReplaceMacros {
writer: self,
formatted_stack: vec![],
};
let mut lines = fmted.lines().enumerate().peekable();
// builds the expression stack
let mut modified_expr = expr.clone();
replacer.visit_expr_mut(&mut modified_expr);
// now unparsed with the modified expression
let mut unparsed = unparse_expr(&modified_expr);
// walk each line looking for the dioxus_autofmt_block__________ token
// if we find it, replace it with the formatted block
// if there's indentation we want to presreve it
// now we can replace the macros with the formatted blocks
for formatted in replacer.formatted_stack.drain(..) {
let fmted = if formatted.contains('\n') {
format!("rsx! {{{formatted}\n}}")
} else {
format!("rsx! {{{formatted}}}")
};
let mut out_fmt = String::new();
let mut whitespace = 0;
for line in unparsed.lines() {
if line.contains("dioxus_autofmt_block__________") {
whitespace = line.chars().take_while(|c| c.is_whitespace()).count();
break;
}
while let Some((_idx, fmt_line)) = lines.next() {
// Push the indentation
if is_multiline {
out_fmt.push_str(&cfg.indent_str().repeat(whitespace));
}
for (idx, fmt_line) in fmted.lines().enumerate() {
// Push the indentation
if idx > 0 {
out_fmt.push_str(&" ".repeat(whitespace));
}
// Calculate delta between indentations - the block indentation is too much
out_fmt.push_str(fmt_line);
out_fmt.push_str(fmt_line);
// Push a newline
// Push a newline if there's another line
if lines.peek().is_some() {
out_fmt.push('\n');
}
// Remove the last newline
out_fmt.pop();
// Replace the dioxus_autofmt_block__________ token with the formatted block
unparsed = unparsed.replacen("dioxus_autofmt_block__________", &out_fmt, 1);
continue;
}
if is_multiline {
out_fmt.push('\n');
out_fmt.push_str(&cfg.indent_str().repeat(whitespace));
} else {
out_fmt.push(' ');
}
// Replace the dioxus_autofmt_block__________ token with the formatted block
out_fmt.push('}');
unparsed = unparsed.replacen(MARKER_REPLACE, &out_fmt, 1);
continue;
}
// stylistic choice to trim whitespace around the expr
if unparsed.starts_with("{ ") && unparsed.ends_with(" }") {
let mut out_fmt = String::new();
out_fmt.push('{');
out_fmt.push_str(&unparsed[2..unparsed.len() - 2]);
out_fmt.push('}');
out_fmt
} else {
unparsed
}
}
@ -145,9 +144,9 @@ impl Writer<'_> {
///
/// 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 {
pub fn unparse_inner(expr: &Expr) -> String {
let file = wrapped(expr);
let wrapped = unparse(&file);
let wrapped = prettyplease::unparse(&file);
unwrapped(wrapped)
}
@ -164,7 +163,9 @@ fn unwrapped(raw: String) -> String {
.join("\n");
// remove the semicolon
o.pop();
if o.ends_with(';') {
o.pop();
}
o
}
@ -184,36 +185,202 @@ fn wrapped(expr: &Expr) -> File {
}
}
#[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");
}
#[cfg(test)]
mod tests {
use super::*;
use proc_macro2::TokenStream;
#[test]
fn unparses_completely() {
let expr = syn::parse_str("1 + 1").unwrap();
let unparsed = unparse_expr(&expr);
assert_eq!(unparsed, "1 + 1");
}
#[test]
fn unparses_let_guard() {
let expr = syn::parse_str("let Some(url) = &link.location").unwrap();
let unparsed = unparse_expr(&expr);
assert_eq!(unparsed, "let Some(url) = &link.location");
}
#[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 })
fn fmt_block_from_expr(raw: &str, tokens: TokenStream, cfg: IndentOptions) -> Option<String> {
let body = CallBody::parse_strict.parse2(tokens).unwrap();
let mut writer = Writer::new(raw, cfg);
writer.write_body_nodes(&body.body.roots).ok()?;
writer.consume()
}
"##;
let expr: File = syn::parse_file(contents).unwrap();
let out = unparse(&expr);
println!("{}", out);
#[test]
fn unparses_raw() {
let expr = syn::parse_str("1 + 1").expect("Failed to parse");
let unparsed = prettyplease::unparse(&wrapped(&expr));
assert_eq!(unparsed, "fn main() {\n 1 + 1;\n}\n");
}
#[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 = prettyplease::unparse(&expr);
println!("{}", out);
}
#[test]
fn multiline_madness() {
let contents = r##"
{
{children.is_some().then(|| rsx! {
span {
class: "inline-block ml-auto hover:bg-gray-500",
onclick: move |evt| {
evt.cancel_bubble();
},
icons::icon_5 {}
{rsx! {
icons::icon_6 {}
}}
}
})}
{children.is_some().then(|| rsx! {
span {
class: "inline-block ml-auto hover:bg-gray-500",
onclick: move |evt| {
evt.cancel_bubble();
},
icons::icon_10 {}
}
})}
}
"##;
let expr: Expr = syn::parse_str(contents).unwrap();
let out = unparse_expr(&expr, contents, &IndentOptions::default());
println!("{}", out);
}
#[test]
fn write_body_no_indent() {
let src = r##"
span {
class: "inline-block ml-auto hover:bg-gray-500",
onclick: move |evt| {
evt.cancel_bubble();
},
icons::icon_10 {}
icons::icon_10 {}
icons::icon_10 {}
icons::icon_10 {}
div { "hi" }
div { div {} }
div { div {} div {} div {} }
{children}
{
some_big_long()
.some_big_long()
.some_big_long()
.some_big_long()
.some_big_long()
.some_big_long()
}
div { class: "px-4", {is_current.then(|| rsx! { {children} })} }
Thing {
field: rsx! {
div { "hi" }
Component {
onrender: rsx! {
div { "hi" }
Component {
onclick: move |_| {
another_macro! {
div { class: "max-w-lg lg:max-w-2xl mx-auto mb-16 text-center",
"gomg"
"hi!!"
"womh"
}
};
rsx! {
div { class: "max-w-lg lg:max-w-2xl mx-auto mb-16 text-center",
"gomg"
"hi!!"
"womh"
}
};
println!("hi")
},
onrender: move |_| {
let _ = 12;
let r = rsx! {
div { "hi" }
};
rsx! {
div { "hi" }
}
}
}
{
rsx! {
BarChart {
id: "bar-plot".to_string(),
x: value,
y: label
}
}
}
}
}
}
}
}
"##;
let tokens: TokenStream = syn::parse_str(src).unwrap();
let out = fmt_block_from_expr(src, tokens, IndentOptions::default()).unwrap();
println!("{}", out);
}
#[test]
fn write_component_body() {
let src = r##"
div { class: "px-4", {is_current.then(|| rsx! { {children} })} }
"##;
let tokens: TokenStream = syn::parse_str(src).unwrap();
let out = fmt_block_from_expr(src, tokens, IndentOptions::default()).unwrap();
println!("{}", out);
}
#[test]
fn weird_macro() {
let contents = r##"
fn main() {
move |_| {
drop_macro_semi! {
"something_very_long_something_very_long_something_very_long_something_very_long"
};
let _ = drop_macro_semi! {
"something_very_long_something_very_long_something_very_long_something_very_long"
};
drop_macro_semi! {
"something_very_long_something_very_long_something_very_long_something_very_long"
};
};
}
"##;
let expr: File = syn::parse_file(contents).unwrap();
let out = prettyplease::unparse(&expr);
println!("{}", out);
}
#[test]
fn comments_on_nodes() {
let src = r##"// hiasdasds
div {
attr: "value", // comment
div {}
"hi" // hello!
"hi" // hello!
"hi" // hello!
// hi!
}
"##;
let tokens: TokenStream = syn::parse_str(src).unwrap();
let out = fmt_block_from_expr(src, tokens, IndentOptions::default()).unwrap();
println!("{}", out);
}
}

View file

@ -1,444 +0,0 @@
use crate::{prettier_please::unparse_expr, Writer};
use dioxus_rsx::*;
use proc_macro2::Span;
use quote::ToTokens;
use std::{
fmt::Result,
fmt::{self, Write},
};
use syn::{spanned::Spanned, token::Brace, Expr};
#[derive(Debug)]
enum ShortOptimization {
/// Special because we want to print the closing bracket immediately
///
/// IE
/// `div {}` instead of `div { }`
Empty,
/// Special optimization to put everything on the same line and add some buffer spaces
///
/// IE
///
/// `div { "asdasd" }` instead of a multiline variant
Oneliner,
/// Optimization where children flow but props remain fixed on top
PropsOnTop,
/// The noisiest optimization where everything flows
NoOpt,
}
impl Writer<'_> {
/// Basically elements and components are the same thing
///
/// This writes the contents out for both in one function, centralizing the annoying logic like
/// key handling, breaks, closures, etc
pub fn write_rsx_block(
&mut self,
attributes: &[Attribute],
spreads: &[Spread],
children: &[BodyNode],
brace: &Brace,
) -> Result {
// 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 attr_len = self.is_short_attrs(attributes, spreads);
let is_short_attr_list = (attr_len + self.out.indent_level * 4) < 80;
let children_len = self.is_short_children(children);
let is_small_children = children_len.is_some();
// if we have one long attribute 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
// However if we have childrne we need to just spread them out for readability
if !is_short_attr_list && attributes.len() <= 1 && spreads.is_empty() {
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 {
if children_len.unwrap() + attr_len + self.out.indent_level * 4 < 100 {
opt_level = ShortOptimization::Oneliner;
} else {
opt_level = ShortOptimization::PropsOnTop;
}
}
// If there's nothing at all, empty optimization
if attributes.is_empty() && children.is_empty() && spreads.is_empty() {
opt_level = ShortOptimization::Empty;
// Write comments if they exist
self.write_todo_body(brace)?;
}
// multiline handlers bump everything down
if attr_len > 1000 || self.out.indent.split_line_attributes() {
opt_level = ShortOptimization::NoOpt;
}
let has_children = !children.is_empty();
match opt_level {
ShortOptimization::Empty => {}
ShortOptimization::Oneliner => {
write!(self.out, " ")?;
self.write_attributes(attributes, spreads, true, brace, has_children)?;
if !children.is_empty() && !attributes.is_empty() {
write!(self.out, " ")?;
}
for child in children.iter() {
self.write_ident(child)?;
}
write!(self.out, " ")?;
}
ShortOptimization::PropsOnTop => {
if !attributes.is_empty() {
write!(self.out, " ")?;
}
self.write_attributes(attributes, spreads, true, brace, has_children)?;
if !children.is_empty() {
self.write_body_indented(children)?;
}
self.out.tabbed_line()?;
}
ShortOptimization::NoOpt => {
self.write_attributes(attributes, spreads, false, brace, has_children)?;
if !children.is_empty() {
self.write_body_indented(children)?;
}
self.out.tabbed_line()?;
}
}
Ok(())
}
fn write_attributes(
&mut self,
attributes: &[Attribute],
spreads: &[Spread],
props_same_line: bool,
brace: &Brace,
has_children: bool,
) -> Result {
enum AttrType<'a> {
Attr(&'a Attribute),
Spread(&'a Spread),
}
let mut attr_iter = attributes
.iter()
.map(AttrType::Attr)
.chain(spreads.iter().map(AttrType::Spread))
.peekable();
while let Some(attr) = attr_iter.next() {
self.out.indent_level += 1;
if !props_same_line {
self.write_attr_comments(
brace,
match attr {
AttrType::Attr(attr) => attr.span(),
AttrType::Spread(attr) => attr.expr.span(),
},
)?;
}
self.out.indent_level -= 1;
if !props_same_line {
self.out.indented_tabbed_line()?;
}
match attr {
AttrType::Attr(attr) => self.write_attribute(attr)?,
AttrType::Spread(attr) => self.write_spread_attribute(&attr.expr)?,
}
if attr_iter.peek().is_some() {
write!(self.out, ",")?;
if props_same_line {
write!(self.out, " ")?;
}
}
}
let has_attributes = !attributes.is_empty() || !spreads.is_empty();
if has_attributes && has_children {
write!(self.out, ",")?;
}
Ok(())
}
fn write_attribute(&mut self, attr: &Attribute) -> Result {
self.write_attribute_name(&attr.name)?;
// if the attribute is a shorthand, we don't need to write the colon, just the name
if !attr.can_be_shorthand() {
write!(self.out, ": ")?;
self.write_attribute_value(&attr.value)?;
}
Ok(())
}
fn write_attribute_name(&mut self, attr: &AttributeName) -> Result {
match attr {
AttributeName::BuiltIn(name) => {
write!(self.out, "{}", name)?;
}
AttributeName::Custom(name) => {
write!(self.out, "{}", name.to_token_stream())?;
}
AttributeName::Spread(_) => unreachable!(),
}
Ok(())
}
fn write_attribute_value(&mut self, value: &AttributeValue) -> Result {
match value {
AttributeValue::IfExpr(if_chain) => {
self.write_attribute_if_chain(if_chain)?;
}
AttributeValue::AttrLiteral(value) => {
write!(self.out, "{value}")?;
}
AttributeValue::Shorthand(value) => {
write!(self.out, "{value}")?;
}
AttributeValue::EventTokens(closure) => {
self.write_partial_closure(closure)?;
}
AttributeValue::AttrExpr(value) => {
let Ok(expr) = value.as_expr() else {
return Err(fmt::Error);
};
let pretty_expr = self.retrieve_formatted_expr(&expr).to_string();
self.write_mulitiline_tokens(pretty_expr)?;
}
}
Ok(())
}
fn write_attribute_if_chain(&mut self, if_chain: &IfAttributeValue) -> Result {
write!(self.out, "if {} {{ ", unparse_expr(&if_chain.condition))?;
self.write_attribute_value(&if_chain.then_value)?;
write!(self.out, " }}")?;
match if_chain.else_value.as_deref() {
Some(AttributeValue::IfExpr(else_if_chain)) => {
write!(self.out, "else ")?;
self.write_attribute_if_chain(else_if_chain)?;
}
Some(other) => {
write!(self.out, "else {{")?;
self.write_attribute_value(other)?;
write!(self.out, " }}")?;
}
None => {}
}
Ok(())
}
fn write_mulitiline_tokens(&mut self, out: String) -> Result {
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.out, "{first}")?;
} else {
writeln!(self.out, "{first}")?;
while let Some(line) = lines.next() {
self.out.indented_tab()?;
write!(self.out, "{line}")?;
if lines.peek().is_none() {
write!(self.out, "")?;
} else {
writeln!(self.out)?;
}
}
}
Ok(())
}
/// Write out the special PartialClosure type from the rsx crate
/// Basically just write token by token until we hit the block and then try and format *that*
/// We can't just ToTokens
fn write_partial_closure(&mut self, closure: &PartialClosure) -> Result {
// Write the pretty version of the closure
if let Ok(expr) = closure.as_expr() {
let pretty_expr = self.retrieve_formatted_expr(&expr).to_string();
self.write_mulitiline_tokens(pretty_expr)?;
return Ok(());
}
// If we can't parse the closure, writing it is also a failure
// rustfmt won't be able to parse it either so no point in trying
Err(fmt::Error)
}
fn write_spread_attribute(&mut self, attr: &Expr) -> Result {
let formatted = unparse_expr(attr);
let mut lines = formatted.lines();
let first_line = lines.next().unwrap();
write!(self.out, "..{first_line}")?;
for line in lines {
self.out.indented_tabbed_line()?;
write!(self.out, "{line}")?;
}
Ok(())
}
// make sure the comments are actually relevant to this element.
// test by making sure this element is the primary element on this line
pub fn current_span_is_primary(&self, location: Span) -> bool {
let start = location.start();
let line_start = start.line - 1;
let beginning = self
.src
.get(line_start)
.filter(|this_line| this_line.len() > start.column)
.map(|this_line| this_line[..start.column].trim())
.unwrap_or_default();
beginning.is_empty()
}
// 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(&mut self, children: &[BodyNode]) -> Option<usize> {
if children.is_empty() {
// todo: allow elements with comments but no children
// like div { /* comment */ }
// or
// div {
// // some helpful
// }
return Some(0);
}
// Any comments push us over the limit automatically
if self.children_have_comments(children) {
return None;
}
match children {
[BodyNode::Text(ref text)] => Some(text.input.to_string_with_quotes().len()),
// TODO: let rawexprs to be inlined
[BodyNode::RawExpr(ref expr)] => Some(get_expr_length(expr.span())),
// TODO: let rawexprs to be inlined
[BodyNode::Component(ref comp)] if comp.fields.is_empty() => Some(
comp.name
.segments
.iter()
.map(|s| s.ident.to_string().len() + 2)
.sum::<usize>(),
),
// Feedback on discord indicates folks don't like combining multiple children on the same line
// We used to do a lot of math to figure out if we should expand out the line, but folks just
// don't like it.
_ => None,
}
}
fn children_have_comments(&self, children: &[BodyNode]) -> bool {
for child in children {
if self.current_span_is_primary(child.span()) {
'line: for line in self.src[..child.span().start().line - 1].iter().rev() {
match (line.trim().starts_with("//"), line.is_empty()) {
(true, _) => return true,
(_, true) => continue 'line,
_ => break 'line,
}
}
}
}
false
}
/// empty everything except for some comments
fn write_todo_body(&mut self, brace: &Brace) -> fmt::Result {
let span = brace.span.span();
let start = span.start();
let end = span.end();
if start.line == end.line {
return Ok(());
}
writeln!(self.out)?;
for idx in start.line..end.line {
let line = &self.src[idx];
if line.trim().starts_with("//") {
for _ in 0..self.out.indent_level + 1 {
write!(self.out, " ")?
}
writeln!(self.out, "{}", line.trim()).unwrap();
}
}
for _ in 0..self.out.indent_level {
write!(self.out, " ")?
}
Ok(())
}
}
fn get_expr_length(span: Span) -> usize {
let (start, end) = (span.start(), span.end());
if start.line == end.line {
end.column - start.column
} else {
10000
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,21 @@
#[test]
fn no_parse() {
let src = include_str!("./partials/no_parse.rsx");
assert!(syn::parse_file(src).is_err());
}
#[test]
fn parses_but_fmt_fails() {
let src = include_str!("./partials/wrong.rsx");
let file = syn::parse_file(src).unwrap();
let formatted = dioxus_autofmt::try_fmt_file(src, &file, Default::default());
assert!(&formatted.is_err());
}
#[test]
fn parses_and_is_okay() {
let src = include_str!("./partials/okay.rsx");
let file = syn::parse_file(src).unwrap();
let formatted = dioxus_autofmt::try_fmt_file(src, &file, Default::default()).unwrap();
assert_ne!(formatted.len(), 0);
}

View file

@ -0,0 +1,8 @@
#[component]
fn SidebarSection() -> Element {
rsx! {
div {
{ .doesnt_work) }
}
}
}

View file

@ -0,0 +1,10 @@
#[component]
fn SidebarSection() -> Element {
rsx! {
div {
onclick: move |_| {
works()
}
}
}
}

View file

@ -0,0 +1,10 @@
#[component]
fn SidebarSection() -> Element {
rsx! {
div {
onclick: move |_| {
.doesnt_work()
}
}
}
}

View file

@ -1,3 +1,5 @@
#![allow(deprecated)]
macro_rules! twoway {
(
$(
@ -22,35 +24,38 @@ macro_rules! twoway {
)*
};
}
twoway![
attributes,
basic_expr,
collapse_expr,
comments,
commentshard,
complex,
docsite,
emoji,
fat_exprs,
ifchain_forloop,
immediate_expr,
key,
letsome,
long_exprs,
long,
manual_props,
many_exprs,
messy_indent,
misplaced,
multirsx,
nested,
raw_strings,
reallylong,
shorthand,
simple,
skip,
spaces,
staged,
t2,
tiny,
tinynoopt,
trailing_expr,
many_exprs,
shorthand,
docsite,
letsome,
fat_exprs,
nested,
staged,
misplaced
oneline
];

View file

@ -30,7 +30,7 @@ rsx! {
a: "123",
a: "123",
a: "123",
a: "123"
a: "123",
}
div {
@ -42,6 +42,6 @@ rsx! {
a: "123",
a: "123",
a: "123",
a: "123"
a: "123",
}
}

View file

@ -0,0 +1,8 @@
fn itworks() {
rsx! {
div {
"hi"
{children}
}
}
}

View file

@ -33,4 +33,30 @@ rsx! {
class: "asd",
"Jon"
}
// comments inline
div { // inline
// Collapse
class: "asd", // super inline
class: "asd", // super inline
"Jon" // all the inline
// Comments at the end too
}
// please dont eat me 1
div { // please dont eat me 2
// please dont eat me 3
}
// please dont eat me 1
div { // please dont eat me 2
// please dont eat me 3
abc: 123,
}
// please dont eat me 1
div {
// please dont eat me 3
abc: 123,
}
}

View file

@ -36,7 +36,7 @@ rsx! {
class: "hello world",
// todo some work in here
class: "hello world"
class: "hello world",
}
div {

View file

@ -22,14 +22,17 @@ 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 })} }
div { class: "px-4",
{is_current.then(|| rsx! {
{children}
})}
}
}
// No nesting
@ -37,7 +40,7 @@ rsx! {
adsasd: "asd",
onclick: move |_| {
let blah = 120;
}
},
}
// Component path
@ -45,7 +48,7 @@ rsx! {
adsasd: "asd",
onclick: move |_| {
let blah = 120;
}
},
}
for i in 0..10 {

View file

@ -15,7 +15,7 @@ pub(crate) fn Nav() -> Element {
MaterialIcon {
name: "menu",
size: 24,
color: MaterialIconColor::Dark
color: MaterialIconColor::Dark,
}
}
div { class: "flex z-50 md:flex-1 px-2", LinkList {} }
@ -59,7 +59,7 @@ pub(crate) fn Nav() -> Element {
Link { to: Route::Homepage {},
img {
src: "https://avatars.githubusercontent.com/u/10237910?s=40&v=4",
class: "ml-4 h-10 rounded-full w-auto"
class: "ml-4 h-10 rounded-full w-auto",
}
}
}

View file

@ -24,4 +24,15 @@ rsx! {
} else {
h3 {}
}
div {
class: "asdasd",
class: if expr { "asdasd" } else { "asdasd" },
class: if expr { "asdasd" },
class: if expr { "asdasd" } else if expr { "asdasd" } else { "asdasd" },
// comments?
class: if expr { "asdasd" } else if expr { "asdasd" } else { "asdasd" }, // comments!!?
// comments?
}
}

View file

@ -6,7 +6,9 @@ rsx! {
section { class: "body-font overflow-hidden dark:bg-ideblack",
div { class: "container px-6 mx-auto",
div { class: "-my-8 divide-y-2 divide-gray-100",
{POSTS.iter().enumerate().map(|(id, post)| rsx! { BlogPostItem { post: post, id: id } })}
{POSTS.iter().enumerate().map(|(id, post)| rsx! {
BlogPostItem { post, id }
})}
}
}
}

View file

@ -10,7 +10,7 @@ rsx! {
Component {
asdasd: "asdasd",
asdasd: "asdasdasdasdasdasdasdasdasdasd",
..Props { a: 10, b: 20 }
..Props { a: 10, b: 20 },
}
Component {
asdasd: "asdasd",

View file

@ -103,12 +103,21 @@ fn app() -> Element {
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)
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 {
@ -119,7 +128,7 @@ fn app() -> Element {
value: format!("{:02}", timer.read().hours),
oninput: move |e| {
timer.write().hours = e.value().parse().unwrap_or(0);
}
},
}
input {
@ -129,7 +138,7 @@ fn app() -> Element {
value: format!("{:02}", timer.read().minutes),
oninput: move |e| {
timer.write().minutes = e.value().parse().unwrap_or(0);
}
},
}
input {
@ -139,7 +148,7 @@ fn app() -> Element {
value: format!("{:02}", timer.read().seconds),
oninput: move |e| {
timer.write().seconds = e.value().parse().unwrap_or(0);
}
},
}
}
@ -155,7 +164,7 @@ fn app() -> Element {
}
})
},
{ timer.with(|t| if t.started_at.is_none() { "Start" } else { "Stop" }) }
{timer.with(|t| if t.started_at.is_none() { "Start" } else { "Stop" })}
}
div { id: "app",
button {
@ -165,7 +174,10 @@ fn app() -> Element {
window_preferences.write().with_decorations = !decorations;
},
{
format!("with decorations{}", if window_preferences.read().with_decorations { " ✓" } else { "" }).to_string()
format!(
"with decorations{}",
if window_preferences.read().with_decorations { " ✓" } else { "" },
)
}
}
button {
@ -178,16 +190,28 @@ fn app() -> Element {
},
width: 100,
{
format!("always on top{}", if window_preferences.read().always_on_top { " ✓" } else { "" })
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()))) }
}
|trigger, delay| {
rsx! {
{
format!(
"{:0.1?}",
trigger
.read()
.map(|inst| (delay.as_secs_f32() - inst.elapsed().as_secs_f32())),
)
}
}
},
)
}
}

View file

@ -15,7 +15,7 @@ pub(crate) fn Nav() -> Element {
MaterialIcon {
name: "menu",
size: 24,
color: MaterialIconColor::Dark
color: MaterialIconColor::Dark,
}
}
div { class: "flex z-50 md:flex-1 px-2", LinkList {} }

View file

@ -36,7 +36,7 @@ fn App() -> Element {
"hi!!"
"womh"
}
};
}
println!("hi")
},
"hi"
@ -83,7 +83,7 @@ fn App() -> Element {
"so22mething nested?"
}
}
}
},
}
}
};
@ -93,11 +93,11 @@ fn App() -> Element {
"something nested?"
}
}
}
},
}
}
},
}
}
},
}
},
onrender: move |_| {
@ -121,12 +121,14 @@ fn App() -> Element {
}
}
},
{rsx! {
div2 {
h12 { "hi" }
"so22mething nested?"
{
rsx! {
div2 {
h12 { "hi" }
"so22mething nested?"
}
}
}}
}
}
}
},
@ -138,7 +140,7 @@ fn App() -> Element {
"something nested?"
}
};
}
},
}
}
}

View file

@ -0,0 +1 @@
rsx! { "hello world" }

View file

@ -12,6 +12,6 @@ rsx! {
width: r#"10px"#,
height: r##"{10}px"##,
"raw-attr": r###"raw-attr"###,
"raw-attr2": r###"{100}"###
"raw-attr2": r###"{100}"###,
}
}

View file

@ -16,7 +16,7 @@ rsx! {
id: "{a}",
class: "ban",
style: "color: red",
value: "{b}"
value: "{b}",
}
// Nested one level

View file

@ -0,0 +1,36 @@
/// dont format this component
#[rustfmt::skip]
#[component]
fn SidebarSection() -> Element {
rsx! {
div {
"hi" div {} div {}
}
}
}
/// dont format this component
#[component]
fn SidebarSection() -> Element {
// format this
rsx! {
div { "hi" }
}
// and this
rsx! {
div {
"hi"
div {}
div {}
}
}
// but not this
#[rustfmt::skip]
rsx! {
div {
"hi" div {} div {}
}
}
}

View file

@ -0,0 +1,14 @@
rsx! {
if let Some(Some(record)) = &*records.read_unchecked() {
{
let (label, value): (Vec<String>, Vec<f64>) = record
.iter()
.rev()
.map(|d| (d.model.clone().expect("work"), d.row_total))
.collect();
rsx! {
BarChart { id: "bar-plot".to_string(), x: value, y: label }
}
}
}
}

View file

@ -9,18 +9,20 @@ rsx! {
div { {some_expr} }
div {
{
POSTS.iter().enumerate().map(|(id, post)| rsx! {
BlogPostItem { post, id }
})
}
{POSTS.iter().enumerate().map(|(id, post)| rsx! {
BlogPostItem { post, id }
})}
}
div { class: "123123123123123123123123123123123123",
{some_really_long_expr_some_really_long_expr_some_really_long_expr_some_really_long_expr_}
{
some_really_long_expr_some_really_long_expr_some_really_long_expr_some_really_long_expr_
}
}
div { class: "-my-8 divide-y-2 divide-gray-100",
{POSTS.iter().enumerate().map(|(id, post)| rsx! { BlogPostItem { post: post, id: id } })}
{POSTS.iter().enumerate().map(|(id, post)| rsx! {
BlogPostItem { post, id }
})}
}
}

View file

@ -1,3 +1,5 @@
#![allow(deprecated)]
use dioxus_autofmt::{IndentOptions, IndentType};
macro_rules! twoway {
@ -6,7 +8,12 @@ macro_rules! twoway {
fn $name() {
let src_right = include_str!(concat!("./wrong/", $val, ".rsx"));
let src_wrong = include_str!(concat!("./wrong/", $val, ".wrong.rsx"));
let formatted = dioxus_autofmt::fmt_file(src_wrong, $indent);
let parsed = syn::parse_file(src_wrong)
.expect("fmt_file should only be called on valid syn::File files");
let formatted =
dioxus_autofmt::try_fmt_file(src_wrong, &parsed, $indent).unwrap_or_default();
let out = dioxus_autofmt::apply_formats(src_wrong, formatted);
// normalize line endings
@ -31,3 +38,4 @@ twoway!("simple-combo-expr" => simple_combo_expr (IndentOptions::new(IndentType:
twoway!("oneline-expand" => online_expand (IndentOptions::new(IndentType::Spaces, 4, false)));
twoway!("shortened" => shortened (IndentOptions::new(IndentType::Spaces, 4, false)));
twoway!("syntax_error" => syntax_error (IndentOptions::new(IndentType::Spaces, 4, false)));
twoway!("skipfail" => skipfail (IndentOptions::new(IndentType::Spaces, 4, false)));

View file

@ -103,12 +103,22 @@ fn app() -> Element {
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)
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 {
@ -119,7 +129,7 @@ fn app() -> Element {
value: format!("{:02}", timer.read().hours),
oninput: move |e| {
timer.write().hours = e.value().parse().unwrap_or(0);
}
},
}
input {
@ -129,7 +139,7 @@ fn app() -> Element {
value: format!("{:02}", timer.read().minutes),
oninput: move |e| {
timer.write().minutes = e.value().parse().unwrap_or(0);
}
},
}
input {
@ -139,7 +149,7 @@ fn app() -> Element {
value: format!("{:02}", timer.read().seconds),
oninput: move |e| {
timer.write().seconds = e.value().parse().unwrap_or(0);
}
},
}
}
@ -155,7 +165,7 @@ fn app() -> Element {
};
})
},
{ timer.with(|t| if t.started_at.is_none() { "Start" } else { "Stop" }) }
{timer.with(|t| if t.started_at.is_none() { "Start" } else { "Stop" })}
}
div { id: "app",
button {
@ -165,7 +175,11 @@ fn app() -> Element {
window_preferences.write().with_decorations = !decorations;
},
{
format!("with decorations{}", if window_preferences.read().with_decorations { " ✓" } else { "" }).to_string()
format!(
"with decorations{}",
if window_preferences.read().with_decorations { " ✓" } else { "" },
)
.to_string()
}
}
button {
@ -178,7 +192,10 @@ fn app() -> Element {
},
width: 100,
{
format!("always on top{}", if window_preferences.read().always_on_top { " ✓" } else { "" })
format!(
"always on top{}",
if window_preferences.read().always_on_top { " ✓" } else { "" },
)
}
}
}
@ -186,8 +203,15 @@ fn app() -> Element {
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()))) }
}
{
format!(
"{:0.1?}",
trigger
.read()
.map(|inst| (delay.as_secs_f32() - inst.elapsed().as_secs_f32())),
)
}
},
)
}
}

View file

@ -11,7 +11,7 @@ fn main() {
None
};
})
}
},
}
}
}

View file

@ -2,12 +2,22 @@ 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)
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 {
@ -18,7 +28,7 @@ fn main() {
value: format!("{:02}", timer.read().hours),
oninput: move |e| {
timer.write().hours = e.value().parse().unwrap_or(0);
}
},
}
// some comment
input {
@ -28,7 +38,7 @@ fn main() {
value: format!("{:02}", timer.read().hours),
oninput: move |e| {
timer.write().hours = e.value().parse().unwrap_or(0);
}
},
}
}
}

View file

@ -0,0 +1,36 @@
/// dont format this component
#[rustfmt::skip]
#[component]
fn SidebarSection() -> Element {
rsx! {
div {
"hi" div {} div {}
}
}
}
/// dont format this component
#[component]
fn SidebarSection() -> Element {
// format this
rsx! {
div { "hi" }
}
// and this
rsx! {
div {
"hi"
div {}
div {}
}
}
// but not this
#[rustfmt::skip]
rsx! {
div {
"hi" div {} div {}
}
}
}

View file

@ -0,0 +1,32 @@
/// dont format this component
#[rustfmt::skip]
#[component]
fn SidebarSection() -> Element {
rsx! {
div {
"hi" div {} div {}
}
}
}
/// dont format this component
#[component]
fn SidebarSection() -> Element {
// format this
rsx! {
div { "hi" }
}
// and this
rsx! {
div { "hi" div {} div {} }
}
// but not this
#[rustfmt::skip]
rsx! {
div {
"hi" div {} div {}
}
}
}

View file

@ -115,7 +115,13 @@ fn refactor_file(
s = format_rust(&s)?;
}
let edits = dioxus_autofmt::fmt_file(&s, indent);
let Ok(Ok(edits)) =
syn::parse_file(&s).map(|file| dioxus_autofmt::try_fmt_file(&s, &file, indent))
else {
eprintln!("failed to format file: {}", s);
exit(1);
};
let out = dioxus_autofmt::apply_formats(&s, edits);
if file == "-" {
@ -159,7 +165,10 @@ fn format_file(
}
}
let edits = dioxus_autofmt::fmt_file(&contents, indent);
let parsed = syn::parse_file(&contents)
.map_err(|err| Error::ParseError(format!("Failed to parse file: {}", err)))?;
let edits = dioxus_autofmt::try_fmt_file(&contents, &parsed, indent)
.map_err(|err| Error::ParseError(format!("Failed to format file: {}", err)))?;
let len = edits.len();
if !edits.is_empty() {
@ -313,29 +322,3 @@ async fn test_auto_fmt() {
fmt.autoformat().unwrap();
}
/*#[test]
fn spawn_properly() {
let out = Command::new("dioxus")
.args([
"fmt",
"-f",
r#"
//
rsx! {
div {}
}
//
//
//
"#,
])
.output()
.expect("failed to execute process");
dbg!(out);
}*/

View file

@ -11,6 +11,7 @@ wasm-bindgen = { workspace = true }
dioxus-autofmt = { workspace = true }
rsx-rosetta = { workspace = true }
html_parser = { workspace = true }
syn ={ workspace = true }
[lib]
crate-type = ["cdylib", "rlib"]

View file

@ -65,18 +65,26 @@ impl FormatBlockInstance {
#[wasm_bindgen]
pub fn format_file(contents: String, use_tabs: bool, indent_size: usize) -> FormatBlockInstance {
let _edits = dioxus_autofmt::fmt_file(
&contents,
IndentOptions::new(
if use_tabs {
IndentType::Tabs
} else {
IndentType::Spaces
},
indent_size,
false,
),
// todo: use rustfmt for this instead
let options = IndentOptions::new(
if use_tabs {
IndentType::Tabs
} else {
IndentType::Spaces
},
indent_size,
false,
);
let Ok(Ok(_edits)) = syn::parse_file(&contents)
.map(|file| dioxus_autofmt::try_fmt_file(&contents, &file, options))
else {
return FormatBlockInstance {
new: contents,
_edits: Vec::new(),
};
};
let out = dioxus_autofmt::apply_formats(&contents, _edits.clone());
FormatBlockInstance { new: out, _edits }
}

View file

@ -115,7 +115,7 @@ function fmtSelection() {
try {
let formatted = dioxus.format_selection(unformatted, !editor.options.insertSpaces, tabSize, base_indentation);
for(let i = 0; i <= base_indentation; i++) {
for (let i = 0; i <= base_indentation; i++) {
formatted = (editor.options.insertSpaces ? " ".repeat(tabSize) : "\t") + formatted;
}
if (formatted.length > 0) {

View file

@ -129,7 +129,7 @@ pub fn collect_svgs(children: &mut [BodyNode], out: &mut Vec<BodyNode>) {
diagnostics: Default::default(),
fields: vec![],
children: TemplateBody::new(vec![]),
brace: Default::default(),
brace: Some(Default::default()),
dyn_idx: Default::default(),
component_literal_dyn_idx: vec![],
});

View file

@ -34,7 +34,7 @@ pub struct Component {
pub fields: Vec<Attribute>,
pub component_literal_dyn_idx: Vec<DynIdx>,
pub spreads: Vec<Spread>,
pub brace: token::Brace,
pub brace: Option<token::Brace>,
pub children: TemplateBody,
pub dyn_idx: DynIdx,
pub diagnostics: Diagnostics,
@ -69,8 +69,8 @@ impl Parse for Component {
name,
generics,
fields,
brace: Some(brace),
component_literal_dyn_idx,
brace,
spreads,
diagnostics,
};
@ -308,7 +308,7 @@ impl Component {
Component {
name,
generics,
brace: token::Brace::default(),
brace: None,
fields: vec![],
spreads: vec![],
children: TemplateBody::new(vec![]),

View file

@ -1,7 +1,7 @@
use super::*;
use location::DynIdx;
use proc_macro2::TokenStream as TokenStream2;
use syn::{braced, Expr, Pat};
use syn::{braced, token::Brace, Expr, Pat};
#[non_exhaustive]
#[derive(PartialEq, Eq, Clone, Debug)]
@ -10,6 +10,7 @@ pub struct ForLoop {
pub pat: Pat,
pub in_token: Token![in],
pub expr: Box<Expr>,
pub brace: Brace,
pub body: TemplateBody,
pub dyn_idx: DynIdx,
}
@ -24,13 +25,14 @@ impl Parse for ForLoop {
let expr = input.call(Expr::parse_without_eager_brace)?;
let content;
let _brace = braced!(content in input);
let brace = braced!(content in input);
let body = content.parse()?;
Ok(Self {
for_token,
pat,
in_token,
brace,
expr: Box::new(expr),
body,
dyn_idx: DynIdx::default(),

View file

@ -4,6 +4,7 @@ use quote::quote;
use quote::{ToTokens, TokenStreamExt};
use syn::{
parse::{Parse, ParseStream},
token::Brace,
Expr, Result, Token,
};
@ -14,8 +15,10 @@ use crate::TemplateBody;
pub struct IfChain {
pub if_token: Token![if],
pub cond: Box<Expr>,
pub then_brace: Brace,
pub then_branch: TemplateBody,
pub else_if_branch: Option<Box<IfChain>>,
pub else_brace: Option<Brace>,
pub else_branch: Option<TemplateBody>,
pub dyn_idx: DynIdx,
}
@ -42,10 +45,11 @@ impl Parse for IfChain {
let cond = Box::new(input.call(Expr::parse_without_eager_brace)?);
let content;
syn::braced!(content in input);
let then_brace = syn::braced!(content in input);
let then_branch = content.parse()?;
let mut else_brace = None;
let mut else_branch = None;
let mut else_if_branch = None;
@ -56,7 +60,7 @@ impl Parse for IfChain {
else_if_branch = Some(Box::new(input.parse::<IfChain>()?));
} else {
let content;
syn::braced!(content in input);
else_brace = Some(syn::braced!(content in input));
else_branch = Some(content.parse()?);
}
}
@ -67,6 +71,8 @@ impl Parse for IfChain {
then_branch,
else_if_branch,
else_branch,
then_brace,
else_brace,
dyn_idx: DynIdx::default(),
})
}

View file

@ -17,12 +17,6 @@ pub struct IfmtInput {
pub segments: Vec<Segment>,
}
impl Default for IfmtInput {
fn default() -> Self {
Self::new(Span::call_site())
}
}
impl IfmtInput {
pub fn new(span: Span) -> Self {
Self {
@ -213,7 +207,7 @@ impl ToTokens for IfmtInput {
fn to_tokens(&self, tokens: &mut TokenStream) {
// If the input is a string literal, we can just return it
if let Some(static_str) = self.to_static() {
return static_str.to_tokens(tokens);
return quote_spanned! { self.span() => #static_str }.to_tokens(tokens);
}
// Try to turn it into a single _.to_string() call

View file

@ -151,6 +151,14 @@ pub struct HotReloadFormattedSegment {
pub dynamic_node_indexes: Vec<DynIdx>,
}
impl HotReloadFormattedSegment {
/// This method is very important!
/// Deref + Spanned + .span() methods leads to name collisions
pub fn span(&self) -> Span {
self.formatted_input.span()
}
}
impl Deref for HotReloadFormattedSegment {
type Target = IfmtInput;