dioxus_autofmt/
writer.rs

1use crate::{buffer::Buffer, IndentOptions};
2use dioxus_rsx::*;
3use proc_macro2::{LineColumn, Span};
4use quote::ToTokens;
5use regex::Regex;
6use std::{
7    borrow::Cow,
8    collections::{HashMap, VecDeque},
9    fmt::{Result, Write},
10};
11use syn::{spanned::Spanned, token::Brace, Expr};
12
13#[derive(Debug)]
14pub struct Writer<'a> {
15    pub raw_src: &'a str,
16    pub src: Vec<&'a str>,
17    pub cached_formats: HashMap<LineColumn, String>,
18    pub out: Buffer,
19    pub invalid_exprs: Vec<Span>,
20}
21
22impl<'a> Writer<'a> {
23    pub fn new(raw_src: &'a str, indent: IndentOptions) -> Self {
24        Self {
25            src: raw_src.lines().collect(),
26            raw_src,
27            out: Buffer {
28                indent,
29                ..Default::default()
30            },
31            cached_formats: HashMap::new(),
32            invalid_exprs: Vec::new(),
33        }
34    }
35
36    pub fn consume(self) -> Option<String> {
37        Some(self.out.buf)
38    }
39
40    pub fn write_rsx_call(&mut self, body: &CallBody) -> Result {
41        if body.body.roots.is_empty() {
42            return Ok(());
43        }
44
45        if Self::is_short_rsx_call(&body.body.roots) {
46            write!(self.out, " ")?;
47            self.write_ident(&body.body.roots[0])?;
48            write!(self.out, " ")?;
49        } else {
50            self.out.new_line()?;
51            self.write_body_indented(&body.body.roots)?;
52            self.write_trailing_body_comments(body)?;
53        }
54
55        Ok(())
56    }
57
58    fn write_trailing_body_comments(&mut self, body: &CallBody) -> Result {
59        if let Some(span) = body.span {
60            self.out.indent_level += 1;
61            let comments = self.accumulate_comments(span.span().end());
62            if !comments.is_empty() {
63                self.out.new_line()?;
64                self.apply_comments(comments)?;
65                self.out.buf.pop(); // remove the trailing newline, forcing us to end at the end of the comment
66            }
67            self.out.indent_level -= 1;
68        }
69        Ok(())
70    }
71
72    // Expects to be written directly into place
73    pub fn write_ident(&mut self, node: &BodyNode) -> Result {
74        match node {
75            BodyNode::Element(el) => self.write_element(el),
76            BodyNode::Component(component) => self.write_component(component),
77            BodyNode::Text(text) => self.write_text_node(text),
78            BodyNode::RawExpr(expr) => self.write_expr_node(expr),
79            BodyNode::ForLoop(forloop) => self.write_for_loop(forloop),
80            BodyNode::IfChain(ifchain) => self.write_if_chain(ifchain),
81        }?;
82
83        let span = Self::final_span_of_node(node);
84
85        self.write_inline_comments(span.end(), 0)?;
86
87        Ok(())
88    }
89
90    /// Check if the rsx call is short enough to be inlined
91    pub(crate) fn is_short_rsx_call(roots: &[BodyNode]) -> bool {
92        // eventually I want to use the _text length, so shutup now
93        #[allow(clippy::match_like_matches_macro)]
94        match roots {
95            [] => true,
96            [BodyNode::Text(_text)] => true,
97            _ => false,
98        }
99    }
100
101    fn write_element(&mut self, el: &Element) -> Result {
102        let Element {
103            name,
104            raw_attributes: attributes,
105            children,
106            spreads,
107            brace,
108            ..
109        } = el;
110
111        write!(self.out, "{name} ")?;
112        self.write_rsx_block(attributes, spreads, children, &brace.unwrap_or_default())?;
113
114        Ok(())
115    }
116
117    fn write_component(
118        &mut self,
119        Component {
120            name,
121            fields,
122            children,
123            generics,
124            spreads,
125            brace,
126            ..
127        }: &Component,
128    ) -> Result {
129        // Write the path by to_tokensing it and then removing all whitespace
130        let mut name = name.to_token_stream().to_string();
131        name.retain(|c| !c.is_whitespace());
132        write!(self.out, "{name}")?;
133
134        // Same idea with generics, write those via the to_tokens method and then remove all whitespace
135        if let Some(generics) = generics {
136            let mut written = generics.to_token_stream().to_string();
137            written.retain(|c| !c.is_whitespace());
138            write!(self.out, "{written}")?;
139        }
140
141        write!(self.out, " ")?;
142        self.write_rsx_block(fields, spreads, &children.roots, &brace.unwrap_or_default())?;
143
144        Ok(())
145    }
146
147    fn write_text_node(&mut self, text: &TextNode) -> Result {
148        self.out.write_text(&text.input)
149    }
150
151    fn write_expr_node(&mut self, expr: &ExprNode) -> Result {
152        self.write_partial_expr(expr.expr.as_expr(), expr.span())
153    }
154
155    fn write_for_loop(&mut self, forloop: &ForLoop) -> std::fmt::Result {
156        write!(
157            self.out,
158            "for {} in ",
159            forloop.pat.clone().into_token_stream(),
160        )?;
161
162        self.write_inline_expr(&forloop.expr)?;
163
164        if forloop.body.is_empty() {
165            write!(self.out, "}}")?;
166            return Ok(());
167        }
168
169        self.out.new_line()?;
170        self.write_body_indented(&forloop.body.roots)?;
171
172        self.out.tabbed_line()?;
173        write!(self.out, "}}")?;
174
175        Ok(())
176    }
177
178    fn write_if_chain(&mut self, ifchain: &IfChain) -> std::fmt::Result {
179        // Recurse in place by setting the next chain
180        let mut branch = Some(ifchain);
181
182        while let Some(chain) = branch {
183            let IfChain {
184                if_token,
185                cond,
186                then_branch,
187                else_if_branch,
188                else_branch,
189                ..
190            } = chain;
191
192            write!(self.out, "{} ", if_token.to_token_stream(),)?;
193
194            self.write_inline_expr(cond)?;
195
196            self.out.new_line()?;
197            self.write_body_indented(&then_branch.roots)?;
198
199            if let Some(else_if_branch) = else_if_branch {
200                // write the closing bracket and else
201                self.out.tabbed_line()?;
202                write!(self.out, "}} else ")?;
203
204                branch = Some(else_if_branch);
205            } else if let Some(else_branch) = else_branch {
206                self.out.tabbed_line()?;
207                write!(self.out, "}} else {{")?;
208
209                self.out.new_line()?;
210                self.write_body_indented(&else_branch.roots)?;
211                branch = None;
212            } else {
213                branch = None;
214            }
215        }
216
217        self.out.tabbed_line()?;
218        write!(self.out, "}}")?;
219
220        Ok(())
221    }
222
223    /// An expression within a for or if block that might need to be spread out across several lines
224    fn write_inline_expr(&mut self, expr: &Expr) -> std::fmt::Result {
225        let unparsed = self.unparse_expr(expr);
226        let mut lines = unparsed.lines();
227        let first_line = lines.next().ok_or(std::fmt::Error)?;
228
229        write!(self.out, "{first_line}")?;
230
231        let mut was_multiline = false;
232
233        for line in lines {
234            was_multiline = true;
235            self.out.tabbed_line()?;
236            write!(self.out, "{line}")?;
237        }
238
239        if was_multiline {
240            self.out.tabbed_line()?;
241            write!(self.out, "{{")?;
242        } else {
243            write!(self.out, " {{")?;
244        }
245
246        Ok(())
247    }
248
249    // Push out the indent level and write each component, line by line
250    fn write_body_indented(&mut self, children: &[BodyNode]) -> Result {
251        self.out.indent_level += 1;
252        self.write_body_nodes(children)?;
253        self.out.indent_level -= 1;
254        Ok(())
255    }
256
257    pub fn write_body_nodes(&mut self, children: &[BodyNode]) -> Result {
258        let mut iter = children.iter().peekable();
259
260        while let Some(child) = iter.next() {
261            if self.current_span_is_primary(child.span().start()) {
262                self.write_comments(child.span().start())?;
263            };
264            self.out.tab()?;
265            self.write_ident(child)?;
266            if iter.peek().is_some() {
267                self.out.new_line()?;
268            }
269        }
270
271        Ok(())
272    }
273
274    /// Basically elements and components are the same thing
275    ///
276    /// This writes the contents out for both in one function, centralizing the annoying logic like
277    /// key handling, breaks, closures, etc
278    fn write_rsx_block(
279        &mut self,
280        attributes: &[Attribute],
281        spreads: &[Spread],
282        children: &[BodyNode],
283        brace: &Brace,
284    ) -> Result {
285        #[derive(Debug)]
286        enum ShortOptimization {
287            /// Special because we want to print the closing bracket immediately
288            ///
289            /// IE
290            /// `div {}` instead of `div { }`
291            Empty,
292
293            /// Special optimization to put everything on the same line and add some buffer spaces
294            ///
295            /// IE
296            ///
297            /// `div { "asdasd" }` instead of a multiline variant
298            Oneliner,
299
300            /// Optimization where children flow but props remain fixed on top
301            PropsOnTop,
302
303            /// The noisiest optimization where everything flows
304            NoOpt,
305        }
306
307        // Write the opening brace
308        write!(self.out, "{{")?;
309
310        // decide if we have any special optimizations
311        // Default with none, opt the cases in one-by-one
312        let mut opt_level = ShortOptimization::NoOpt;
313
314        // check if we have a lot of attributes
315        let attr_len = self.is_short_attrs(attributes, spreads);
316        let is_short_attr_list = (attr_len + self.out.indent_level * 4) < 80;
317        let children_len = self
318            .is_short_children(children)
319            .map_err(|_| std::fmt::Error)?;
320        let has_trailing_comments = self.has_trailing_comments(children, brace);
321        let is_small_children = children_len.is_some() && !has_trailing_comments;
322
323        // if we have one long attribute and a lot of children, place the attrs on top
324        if is_short_attr_list && !is_small_children {
325            opt_level = ShortOptimization::PropsOnTop;
326        }
327
328        // even if the attr is long, it should be put on one line
329        // However if we have childrne we need to just spread them out for readability
330        if !is_short_attr_list
331            && attributes.len() <= 1
332            && spreads.is_empty()
333            && !has_trailing_comments
334        {
335            if children.is_empty() {
336                opt_level = ShortOptimization::Oneliner;
337            } else {
338                opt_level = ShortOptimization::PropsOnTop;
339            }
340        }
341
342        // if we have few children and few attributes, make it a one-liner
343        if is_short_attr_list && is_small_children {
344            if children_len.unwrap() + attr_len + self.out.indent_level * 4 < 100 {
345                opt_level = ShortOptimization::Oneliner;
346            } else {
347                opt_level = ShortOptimization::PropsOnTop;
348            }
349        }
350
351        // If there's nothing at all, empty optimization
352        if attributes.is_empty()
353            && children.is_empty()
354            && spreads.is_empty()
355            && !has_trailing_comments
356        {
357            opt_level = ShortOptimization::Empty;
358
359            // Write comments if they exist
360            self.write_inline_comments(brace.span.span().start(), 1)?;
361            self.write_todo_body(brace)?;
362        }
363
364        // multiline handlers bump everything down
365        if attr_len > 1000 || self.out.indent.split_line_attributes() {
366            opt_level = ShortOptimization::NoOpt;
367        }
368
369        let has_children = !children.is_empty();
370
371        match opt_level {
372            ShortOptimization::Empty => {}
373            ShortOptimization::Oneliner => {
374                write!(self.out, " ")?;
375
376                self.write_attributes(attributes, spreads, true, brace, has_children)?;
377
378                if !children.is_empty() && !attributes.is_empty() {
379                    write!(self.out, " ")?;
380                }
381
382                let mut children_iter = children.iter().peekable();
383                while let Some(child) = children_iter.next() {
384                    self.write_ident(child)?;
385                    if children_iter.peek().is_some() {
386                        write!(self.out, " ")?;
387                    }
388                }
389
390                write!(self.out, " ")?;
391            }
392
393            ShortOptimization::PropsOnTop => {
394                if !attributes.is_empty() {
395                    write!(self.out, " ")?;
396                }
397
398                self.write_attributes(attributes, spreads, true, brace, has_children)?;
399
400                if !children.is_empty() {
401                    self.out.new_line()?;
402                    self.write_body_indented(children)?;
403                }
404
405                self.out.tabbed_line()?;
406            }
407
408            ShortOptimization::NoOpt => {
409                self.write_inline_comments(brace.span.span().start(), 1)?;
410                self.out.new_line()?;
411                self.write_attributes(attributes, spreads, false, brace, has_children)?;
412
413                if !children.is_empty() {
414                    self.out.new_line()?;
415                    self.write_body_indented(children)?;
416                }
417
418                self.out.tabbed_line()?;
419            }
420        }
421
422        // Write trailing comments
423        if matches!(
424            opt_level,
425            ShortOptimization::NoOpt | ShortOptimization::PropsOnTop
426        ) && self.leading_row_is_empty(brace.span.span().end())
427        {
428            let comments = self.accumulate_comments(brace.span.span().end());
429            if !comments.is_empty() {
430                self.apply_comments(comments)?;
431                self.out.tab()?;
432            }
433        }
434
435        write!(self.out, "}}")?;
436
437        Ok(())
438    }
439
440    fn write_attributes(
441        &mut self,
442        attributes: &[Attribute],
443        spreads: &[Spread],
444        props_same_line: bool,
445        brace: &Brace,
446        has_children: bool,
447    ) -> Result {
448        enum AttrType<'a> {
449            Attr(&'a Attribute),
450            Spread(&'a Spread),
451        }
452
453        let mut attr_iter = attributes
454            .iter()
455            .map(AttrType::Attr)
456            .chain(spreads.iter().map(AttrType::Spread))
457            .peekable();
458
459        let has_attributes = !attributes.is_empty() || !spreads.is_empty();
460
461        while let Some(attr) = attr_iter.next() {
462            self.out.indent_level += 1;
463
464            if !props_same_line {
465                self.write_attr_comments(
466                    brace,
467                    match attr {
468                        AttrType::Attr(attr) => attr.span(),
469                        AttrType::Spread(attr) => attr.expr.span(),
470                    },
471                )?;
472            }
473
474            self.out.indent_level -= 1;
475
476            if !props_same_line {
477                self.out.indented_tab()?;
478            }
479
480            match attr {
481                AttrType::Attr(attr) => self.write_attribute(attr)?,
482                AttrType::Spread(attr) => self.write_spread_attribute(&attr.expr)?,
483            }
484
485            let span = match attr {
486                AttrType::Attr(attr) => attr
487                    .comma
488                    .as_ref()
489                    .map(|c| c.span())
490                    .unwrap_or_else(|| self.final_span_of_attr(attr)),
491                AttrType::Spread(attr) => attr.span(),
492            };
493
494            let has_more = attr_iter.peek().is_some();
495            let should_finish_comma = has_attributes && has_children || !props_same_line;
496
497            if has_more || should_finish_comma {
498                write!(self.out, ",")?;
499            }
500
501            if !props_same_line {
502                self.write_inline_comments(span.end(), 0)?;
503            }
504
505            if props_same_line && !has_more {
506                self.write_inline_comments(span.end(), 0)?;
507            }
508
509            if props_same_line && has_more {
510                write!(self.out, " ")?;
511            }
512
513            if !props_same_line && has_more {
514                self.out.new_line()?;
515            }
516        }
517
518        Ok(())
519    }
520
521    fn write_attribute(&mut self, attr: &Attribute) -> Result {
522        self.write_attribute_name(&attr.name)?;
523
524        // if the attribute is a shorthand, we don't need to write the colon, just the name
525        if !attr.can_be_shorthand() {
526            write!(self.out, ": ")?;
527            self.write_attribute_value(&attr.value)?;
528        }
529
530        Ok(())
531    }
532
533    fn write_attribute_name(&mut self, attr: &AttributeName) -> Result {
534        match attr {
535            AttributeName::BuiltIn(name) => write!(self.out, "{}", name),
536            AttributeName::Custom(name) => write!(self.out, "{}", name.to_token_stream()),
537            AttributeName::Spread(_) => unreachable!(),
538        }
539    }
540
541    fn write_attribute_value(&mut self, value: &AttributeValue) -> Result {
542        match value {
543            AttributeValue::IfExpr(if_chain) => {
544                self.write_attribute_if_chain(if_chain)?;
545            }
546            AttributeValue::AttrLiteral(value) => {
547                write!(self.out, "{value}")?;
548            }
549            AttributeValue::Shorthand(value) => {
550                write!(self.out, "{value}")?;
551            }
552            AttributeValue::EventTokens(closure) => {
553                self.out.indent_level += 1;
554                self.write_partial_expr(closure.as_expr(), closure.span())?;
555                self.out.indent_level -= 1;
556            }
557            AttributeValue::AttrExpr(value) => {
558                self.out.indent_level += 1;
559                self.write_partial_expr(value.as_expr(), value.span())?;
560                self.out.indent_level -= 1;
561            }
562        }
563
564        Ok(())
565    }
566
567    fn write_attribute_if_chain(&mut self, if_chain: &IfAttributeValue) -> Result {
568        let cond = self.unparse_expr(&if_chain.condition);
569        write!(self.out, "if {cond} {{ ")?;
570        self.write_attribute_value(&if_chain.then_value)?;
571        write!(self.out, " }}")?;
572        match if_chain.else_value.as_deref() {
573            Some(AttributeValue::IfExpr(else_if_chain)) => {
574                write!(self.out, " else ")?;
575                self.write_attribute_if_chain(else_if_chain)?;
576            }
577            Some(other) => {
578                write!(self.out, " else {{ ")?;
579                self.write_attribute_value(other)?;
580                write!(self.out, " }}")?;
581            }
582            None => {}
583        }
584
585        Ok(())
586    }
587
588    fn write_attr_comments(&mut self, brace: &Brace, attr_span: Span) -> Result {
589        // There's a chance this line actually shares the same line as the previous
590        // Only write comments if the comments actually belong to this line
591        //
592        // to do this, we check if the attr span starts on the same line as the brace
593        // if it doesn't, we write the comments
594        let brace_line = brace.span.span().start().line;
595        let attr_line = attr_span.start().line;
596
597        if brace_line != attr_line {
598            self.write_comments(attr_span.start())?;
599        }
600
601        Ok(())
602    }
603
604    fn write_inline_comments(&mut self, final_span: LineColumn, offset: usize) -> Result {
605        let line = final_span.line;
606        let column = final_span.column;
607        let Some(src_line) = self.src.get(line - 1) else {
608            return Ok(());
609        };
610
611        // the line might contain emoji or other unicode characters - this will cause issues
612        let Some(mut whitespace) = src_line.get(column..).map(|s| s.trim()) else {
613            return Ok(());
614        };
615
616        if whitespace.is_empty() {
617            return Ok(());
618        }
619
620        whitespace = whitespace[offset..].trim();
621
622        if whitespace.starts_with("//") {
623            write!(self.out, " {whitespace}")?;
624        }
625
626        Ok(())
627    }
628    fn accumulate_comments(&mut self, loc: LineColumn) -> VecDeque<usize> {
629        // collect all comments upwards
630        // make sure we don't collect the comments of the node that we're currently under.
631        let start = loc;
632        let line_start = start.line - 1;
633
634        let mut comments = VecDeque::new();
635
636        let Some(lines) = self.src.get(..line_start) else {
637            return comments;
638        };
639
640        for (id, line) in lines.iter().enumerate().rev() {
641            if line.trim().starts_with("//") || line.is_empty() && id != 0 {
642                if id != 0 {
643                    comments.push_front(id);
644                }
645            } else {
646                break;
647            }
648        }
649
650        comments
651    }
652    fn apply_comments(&mut self, mut comments: VecDeque<usize>) -> Result {
653        while let Some(comment_line) = comments.pop_front() {
654            let Some(line) = self.src.get(comment_line) else {
655                continue;
656            };
657
658            let line = &line.trim();
659
660            if line.is_empty() {
661                self.out.new_line()?;
662            } else {
663                self.out.tab()?;
664                writeln!(self.out, "{}", line.trim())?;
665            }
666        }
667        Ok(())
668    }
669
670    fn write_comments(&mut self, loc: LineColumn) -> Result {
671        let comments = self.accumulate_comments(loc);
672        self.apply_comments(comments)?;
673
674        Ok(())
675    }
676
677    fn attr_value_len(&mut self, value: &AttributeValue) -> usize {
678        match value {
679            AttributeValue::IfExpr(if_chain) => {
680                let condition_len = self.retrieve_formatted_expr(&if_chain.condition).len();
681                let value_len = self.attr_value_len(&if_chain.then_value);
682                let if_len = 2;
683                let brace_len = 2;
684                let space_len = 2;
685                let else_len = if_chain
686                    .else_value
687                    .as_ref()
688                    .map(|else_value| self.attr_value_len(else_value) + 1)
689                    .unwrap_or_default();
690                condition_len + value_len + if_len + brace_len + space_len + else_len
691            }
692            AttributeValue::AttrLiteral(lit) => lit.to_string().len(),
693            AttributeValue::Shorthand(expr) => {
694                let span = &expr.span();
695                span.end().line - span.start().line
696            }
697            AttributeValue::AttrExpr(expr) => expr
698                .as_expr()
699                .map(|expr| self.attr_expr_len(&expr))
700                .unwrap_or(100000),
701            AttributeValue::EventTokens(closure) => closure
702                .as_expr()
703                .map(|expr| self.attr_expr_len(&expr))
704                .unwrap_or(100000),
705        }
706    }
707
708    fn attr_expr_len(&mut self, expr: &Expr) -> usize {
709        let out = self.retrieve_formatted_expr(expr);
710        if out.contains('\n') {
711            100000
712        } else {
713            out.len()
714        }
715    }
716
717    fn is_short_attrs(&mut self, attributes: &[Attribute], spreads: &[Spread]) -> usize {
718        let mut total = 0;
719
720        // No more than 3 attributes before breaking the line
721        if attributes.len() > 3 {
722            return 100000;
723        }
724
725        for attr in attributes {
726            if self.current_span_is_primary(attr.span().start()) {
727                if let Some(lines) = self.src.get(..attr.span().start().line - 1) {
728                    'line: for line in lines.iter().rev() {
729                        match (line.trim().starts_with("//"), line.is_empty()) {
730                            (true, _) => return 100000,
731                            (_, true) => continue 'line,
732                            _ => break 'line,
733                        }
734                    }
735                };
736            }
737
738            let name_len = match &attr.name {
739                AttributeName::BuiltIn(name) => {
740                    let name = name.to_string();
741                    name.len()
742                }
743                AttributeName::Custom(name) => name.value().len() + 2,
744                AttributeName::Spread(_) => unreachable!(),
745            };
746            total += name_len;
747
748            if attr.can_be_shorthand() {
749                total += 2;
750            } else {
751                total += self.attr_value_len(&attr.value);
752            }
753
754            total += 6;
755        }
756
757        for spread in spreads {
758            let expr_len = self.retrieve_formatted_expr(&spread.expr).len();
759            total += expr_len + 3;
760        }
761
762        total
763    }
764
765    fn write_todo_body(&mut self, brace: &Brace) -> std::fmt::Result {
766        let span = brace.span.span();
767        let start = span.start();
768        let end = span.end();
769
770        if start.line == end.line {
771            return Ok(());
772        }
773
774        writeln!(self.out)?;
775
776        for idx in start.line..end.line {
777            let Some(line) = self.src.get(idx) else {
778                continue;
779            };
780            if line.trim().starts_with("//") {
781                for _ in 0..self.out.indent_level + 1 {
782                    write!(self.out, "    ")?
783                }
784                writeln!(self.out, "{}", line.trim())?;
785            }
786        }
787
788        for _ in 0..self.out.indent_level {
789            write!(self.out, "    ")?
790        }
791
792        Ok(())
793    }
794
795    fn write_partial_expr(&mut self, expr: syn::Result<Expr>, src_span: Span) -> Result {
796        let Ok(expr) = expr else {
797            self.invalid_exprs.push(src_span);
798            return Err(std::fmt::Error);
799        };
800
801        thread_local! {
802            static COMMENT_REGEX: Regex = Regex::new("\"[^\"]*\"|(//.*)").unwrap();
803        }
804
805        let pretty_expr = self.retrieve_formatted_expr(&expr).to_string();
806
807        // Adding comments back to the formatted expression
808        let source_text = src_span.source_text().unwrap_or_default();
809        let mut source_lines = source_text.lines().peekable();
810        let mut output = String::from("");
811        let mut printed_empty_line = false;
812
813        if source_lines.peek().is_none() {
814            output = pretty_expr;
815        } else {
816            for line in pretty_expr.lines() {
817                let compacted_pretty_line = line.replace(" ", "").replace(",", "");
818                let trimmed_pretty_line = line.trim();
819
820                // Nested expressions might have comments already. We handle writing all of those
821                // at the outer level, so we skip them here
822                if trimmed_pretty_line.starts_with("//") {
823                    continue;
824                }
825
826                if !output.is_empty() {
827                    output.push('\n');
828                }
829
830                // pull down any source lines with whitespace until we hit a line that matches our current line.
831                while let Some(src) = source_lines.peek() {
832                    let trimmed_src = src.trim();
833
834                    // Write comments and empty lines as they are
835                    if trimmed_src.starts_with("//") || trimmed_src.is_empty() {
836                        if !trimmed_src.is_empty() {
837                            // Match the whitespace of the incoming source line
838                            for s in line.chars().take_while(|c| c.is_whitespace()) {
839                                output.push(s);
840                            }
841
842                            // Bump out the indent level if the line starts with a closing brace (ie we're at the end of a block)
843                            if matches!(trimmed_pretty_line.chars().next(), Some(')' | '}' | ']')) {
844                                output.push_str(self.out.indent.indent_str());
845                            }
846
847                            printed_empty_line = false;
848                            output.push_str(trimmed_src);
849                            output.push('\n');
850                        } else if !printed_empty_line {
851                            output.push('\n');
852                            printed_empty_line = true;
853                        }
854
855                        _ = source_lines.next();
856                        continue;
857                    }
858
859                    let compacted_src_line = src.replace(" ", "").replace(",", "");
860
861                    // If this source line matches our pretty line, we stop pulling down
862                    if compacted_src_line.contains(&compacted_pretty_line) {
863                        break;
864                    }
865
866                    // Otherwise, consume this source line and keep going
867                    _ = source_lines.next();
868                }
869
870                // Once all whitespace is written, write the pretty line
871                output.push_str(line);
872                printed_empty_line = false;
873
874                // And then pull the corresponding source line
875                let source_line = source_lines.next();
876
877                // And then write any inline comments
878                if let Some(source_line) = source_line {
879                    if let Some(captures) = COMMENT_REGEX.with(|f| f.captures(source_line)) {
880                        if let Some(comment) = captures.get(1) {
881                            output.push_str(" // ");
882                            output.push_str(comment.as_str().replace("//", "").trim());
883                        }
884                    }
885                }
886            }
887        }
888
889        self.write_mulitiline_tokens(output)?;
890
891        Ok(())
892    }
893
894    fn write_mulitiline_tokens(&mut self, out: String) -> Result {
895        let mut lines = out.split('\n').peekable();
896        let first = lines.next().unwrap();
897
898        // a one-liner for whatever reason
899        // Does not need a new line
900        if lines.peek().is_none() {
901            write!(self.out, "{first}")?;
902        } else {
903            writeln!(self.out, "{first}")?;
904
905            while let Some(line) = lines.next() {
906                if !line.trim().is_empty() {
907                    self.out.tab()?;
908                }
909
910                write!(self.out, "{line}")?;
911                if lines.peek().is_none() {
912                    write!(self.out, "")?;
913                } else {
914                    writeln!(self.out)?;
915                }
916            }
917        }
918
919        Ok(())
920    }
921
922    fn write_spread_attribute(&mut self, attr: &Expr) -> Result {
923        let formatted = self.unparse_expr(attr);
924
925        let mut lines = formatted.lines();
926
927        let first_line = lines.next().unwrap();
928
929        write!(self.out, "..{first_line}")?;
930        for line in lines {
931            self.out.indented_tabbed_line()?;
932            write!(self.out, "{line}")?;
933        }
934
935        Ok(())
936    }
937
938    // check if the children are short enough to be on the same line
939    // We don't have the notion of current line depth - each line tries to be < 80 total
940    // returns the total line length if it's short
941    // returns none if the length exceeds the limit
942    // I think this eventually becomes quadratic :(
943    fn is_short_children(&mut self, children: &[BodyNode]) -> syn::Result<Option<usize>> {
944        if children.is_empty() {
945            return Ok(Some(0));
946        }
947
948        // Any comments push us over the limit automatically
949        if self.children_have_comments(children) {
950            return Ok(None);
951        }
952
953        let res = match children {
954            [BodyNode::Text(ref text)] => Some(text.input.to_string_with_quotes().len()),
955
956            // TODO: let rawexprs to be inlined
957            [BodyNode::RawExpr(ref expr)] => {
958                let pretty = self.retrieve_formatted_expr(&expr.expr.as_expr()?);
959                if pretty.contains('\n') {
960                    None
961                } else {
962                    Some(pretty.len() + 2)
963                }
964            }
965
966            // TODO: let rawexprs to be inlined
967            [BodyNode::Component(ref comp)]
968            // basically if the component is completely empty, we can inline it
969                if comp.fields.is_empty()
970                    && comp.children.is_empty()
971                    && comp.spreads.is_empty() =>
972            {
973                Some(
974                    comp.name
975                        .segments
976                        .iter()
977                        .map(|s| s.ident.to_string().len() + 2)
978                        .sum::<usize>(),
979                )
980            }
981
982            // Feedback on discord indicates folks don't like combining multiple children on the same line
983            // We used to do a lot of math to figure out if we should expand out the line, but folks just
984            // don't like it.
985            _ => None,
986        };
987
988        Ok(res)
989    }
990
991    fn children_have_comments(&self, children: &[BodyNode]) -> bool {
992        for child in children {
993            if self.current_span_is_primary(child.span().start()) {
994                'line: for line in self.src[..child.span().start().line - 1].iter().rev() {
995                    match (line.trim().starts_with("//"), line.is_empty()) {
996                        (true, _) => return true,
997                        (_, true) => continue 'line,
998                        _ => break 'line,
999                    }
1000                }
1001            }
1002        }
1003
1004        false
1005    }
1006
1007    // make sure the comments are actually relevant to this element.
1008    // test by making sure this element is the primary element on this line (nothing else before it)
1009    fn current_span_is_primary(&self, location: LineColumn) -> bool {
1010        self.leading_row_is_empty(LineColumn {
1011            line: location.line,
1012            column: location.column + 1,
1013        })
1014    }
1015
1016    fn leading_row_is_empty(&self, location: LineColumn) -> bool {
1017        let Some(line) = self.src.get(location.line - 1) else {
1018            return false;
1019        };
1020
1021        let Some(sub) = line.get(..location.column - 1) else {
1022            return false;
1023        };
1024
1025        sub.trim().is_empty()
1026    }
1027
1028    #[allow(clippy::map_entry)]
1029    fn retrieve_formatted_expr(&mut self, expr: &Expr) -> Cow<'_, str> {
1030        let loc = expr.span().start();
1031
1032        // never cache expressions that are spanless
1033        if loc.line == 1 && loc.column == 0 {
1034            return self.unparse_expr(expr).into();
1035        }
1036
1037        if !self.cached_formats.contains_key(&loc) {
1038            let formatted = self.unparse_expr(expr);
1039            self.cached_formats.insert(loc, formatted);
1040        }
1041
1042        self.cached_formats
1043            .get(&loc)
1044            .expect("Just inserted the parsed expr, so it should be in the cache")
1045            .as_str()
1046            .into()
1047    }
1048
1049    fn final_span_of_node(node: &BodyNode) -> Span {
1050        // Get the ending span of the node
1051        match node {
1052            BodyNode::Element(el) => el
1053                .brace
1054                .as_ref()
1055                .map(|b| b.span.span())
1056                .unwrap_or_else(|| el.name.span()),
1057            BodyNode::Component(el) => el
1058                .brace
1059                .as_ref()
1060                .map(|b| b.span.span())
1061                .unwrap_or_else(|| el.name.span()),
1062            BodyNode::Text(txt) => txt.input.span(),
1063            BodyNode::RawExpr(exp) => exp.span(),
1064            BodyNode::ForLoop(f) => f.brace.span.span(),
1065            BodyNode::IfChain(i) => match i.else_brace {
1066                Some(b) => b.span.span(),
1067                None => i.then_brace.span.span(),
1068            },
1069        }
1070    }
1071
1072    fn final_span_of_attr(&self, attr: &Attribute) -> Span {
1073        match &attr.value {
1074            AttributeValue::Shorthand(s) => s.span(),
1075            AttributeValue::AttrLiteral(l) => l.span(),
1076            AttributeValue::EventTokens(closure) => closure.body.span(),
1077            AttributeValue::AttrExpr(exp) => exp.span(),
1078            AttributeValue::IfExpr(ex) => ex
1079                .else_value
1080                .as_ref()
1081                .map(|v| v.span())
1082                .unwrap_or_else(|| ex.then_value.span()),
1083        }
1084    }
1085
1086    fn has_trailing_comments(&self, children: &[BodyNode], brace: &Brace) -> bool {
1087        let brace_span = brace.span.span();
1088
1089        let Some(last_node) = children.last() else {
1090            return false;
1091        };
1092
1093        // Check for any comments after the last node between the last brace
1094        let final_span = Self::final_span_of_node(last_node);
1095        let final_span = final_span.end();
1096        let mut line = final_span.line;
1097        let mut column = final_span.column;
1098        loop {
1099            let Some(src_line) = self.src.get(line - 1) else {
1100                return false;
1101            };
1102
1103            // the line might contain emoji or other unicode characters - this will cause issues
1104            let Some(mut whitespace) = src_line.get(column..).map(|s| s.trim()) else {
1105                return false;
1106            };
1107
1108            let offset = 0;
1109            whitespace = whitespace[offset..].trim();
1110
1111            if whitespace.starts_with("//") {
1112                return true;
1113            }
1114
1115            if line == brace_span.end().line {
1116                // If we reached the end of the brace span, stop
1117                break;
1118            }
1119
1120            line += 1;
1121            column = 0; // reset column to the start of the next line
1122        }
1123
1124        false
1125    }
1126}