mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-25 21:50:20 +00:00
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:
parent
ef0202f999
commit
828cc502f1
47 changed files with 1621 additions and 1055 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2616,6 +2616,7 @@ dependencies = [
|
|||
"dioxus-autofmt",
|
||||
"html_parser",
|
||||
"rsx-rosetta",
|
||||
"syn 2.0.72",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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()?;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
21
packages/autofmt/tests/error_handling.rs
Normal file
21
packages/autofmt/tests/error_handling.rs
Normal 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);
|
||||
}
|
8
packages/autofmt/tests/partials/no_parse.rsx
Normal file
8
packages/autofmt/tests/partials/no_parse.rsx
Normal file
|
@ -0,0 +1,8 @@
|
|||
#[component]
|
||||
fn SidebarSection() -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
{ .doesnt_work) }
|
||||
}
|
||||
}
|
||||
}
|
10
packages/autofmt/tests/partials/okay.rsx
Normal file
10
packages/autofmt/tests/partials/okay.rsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
#[component]
|
||||
fn SidebarSection() -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
onclick: move |_| {
|
||||
works()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
packages/autofmt/tests/partials/wrong.rsx
Normal file
10
packages/autofmt/tests/partials/wrong.rsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
#[component]
|
||||
fn SidebarSection() -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
onclick: move |_| {
|
||||
.doesnt_work()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
];
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
|
8
packages/autofmt/tests/samples/basic_expr.rsx
Normal file
8
packages/autofmt/tests/samples/basic_expr.rsx
Normal file
|
@ -0,0 +1,8 @@
|
|||
fn itworks() {
|
||||
rsx! {
|
||||
div {
|
||||
"hi"
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ rsx! {
|
|||
class: "hello world",
|
||||
|
||||
// todo some work in here
|
||||
class: "hello world"
|
||||
class: "hello world",
|
||||
}
|
||||
|
||||
div {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
})}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ rsx! {
|
|||
Component {
|
||||
asdasd: "asdasd",
|
||||
asdasd: "asdasdasdasdasdasdasdasdasdasd",
|
||||
..Props { a: 10, b: 20 }
|
||||
..Props { a: 10, b: 20 },
|
||||
}
|
||||
Component {
|
||||
asdasd: "asdasd",
|
||||
|
|
|
@ -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())),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {} }
|
||||
|
|
|
@ -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?"
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
1
packages/autofmt/tests/samples/oneline.rsx
Normal file
1
packages/autofmt/tests/samples/oneline.rsx
Normal file
|
@ -0,0 +1 @@
|
|||
rsx! { "hello world" }
|
|
@ -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}"###,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ rsx! {
|
|||
id: "{a}",
|
||||
class: "ban",
|
||||
style: "color: red",
|
||||
value: "{b}"
|
||||
value: "{b}",
|
||||
}
|
||||
|
||||
// Nested one level
|
||||
|
|
36
packages/autofmt/tests/samples/skip.rsx
Normal file
36
packages/autofmt/tests/samples/skip.rsx
Normal 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 {}
|
||||
}
|
||||
}
|
||||
}
|
14
packages/autofmt/tests/samples/spaces.rsx
Normal file
14
packages/autofmt/tests/samples/spaces.rsx
Normal 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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
})}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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())),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ fn main() {
|
|||
None
|
||||
};
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
36
packages/autofmt/tests/wrong/skipfail.rsx
Normal file
36
packages/autofmt/tests/wrong/skipfail.rsx
Normal 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 {}
|
||||
}
|
||||
}
|
||||
}
|
32
packages/autofmt/tests/wrong/skipfail.wrong.rsx
Normal file
32
packages/autofmt/tests/wrong/skipfail.wrong.rsx
Normal 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 {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}*/
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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![],
|
||||
});
|
||||
|
|
|
@ -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![]),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
Loading…
Reference in a new issue