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 authored and GitHub committed 2024-07-30 18:36:13 -07:00
1 parent ef0202f999
commit 828cc502f1
47 files changed
+1621 -1055

No files matched your search

-3
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};
+67 -16
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);
}
+35 -32
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()?;
+313 -146
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);
}
}
-444
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