Skip to main content

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, HashSet, 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_full_line_comments(span.span().end());
62            if !comments.is_empty() {
63                self.out.new_line()?;
64                self.apply_line_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(brace, attributes, spreads);
316        let has_postbrace_comments = self.brace_has_trailing_comments(brace);
317        let is_short_attr_list =
318            ((attr_len + self.out.indent_level * 4) < 80) && !has_postbrace_comments;
319        let children_len = self
320            .is_short_children(children)
321            .map_err(|_| std::fmt::Error)?;
322        let has_trailing_comments = self.has_trailing_comments(children, brace);
323        let is_small_children = children_len.is_some() && !has_trailing_comments;
324
325        // if we have one long attribute and a lot of children, place the attrs on top
326        if is_short_attr_list && !is_small_children {
327            opt_level = ShortOptimization::PropsOnTop;
328        }
329
330        // even if the attr is long, it should be put on one line
331        // However if we have childrne we need to just spread them out for readability
332        if !is_short_attr_list
333            && attributes.len() <= 1
334            && spreads.is_empty()
335            && !has_trailing_comments
336            && !has_postbrace_comments
337        {
338            if children.is_empty() {
339                opt_level = ShortOptimization::Oneliner;
340            } else {
341                opt_level = ShortOptimization::PropsOnTop;
342            }
343        }
344
345        // if we have few children and few attributes, make it a one-liner
346        if is_short_attr_list && is_small_children {
347            if children_len.unwrap() + attr_len + self.out.indent_level * 4 < 100 {
348                opt_level = ShortOptimization::Oneliner;
349            } else {
350                opt_level = ShortOptimization::PropsOnTop;
351            }
352        }
353
354        // If there's nothing at all, empty optimization
355        if attributes.is_empty()
356            && children.is_empty()
357            && spreads.is_empty()
358            && !has_trailing_comments
359        {
360            opt_level = ShortOptimization::Empty;
361
362            // Write comments if they exist
363            self.write_inline_comments(brace.span.span().start(), 1)?;
364            self.write_todo_body(brace)?;
365        }
366
367        // multiline handlers bump everything down
368        if attr_len > 1000 || self.out.indent.split_line_attributes() {
369            opt_level = ShortOptimization::NoOpt;
370        }
371
372        let has_children = !children.is_empty();
373
374        match opt_level {
375            ShortOptimization::Empty => {}
376            ShortOptimization::Oneliner => {
377                write!(self.out, " ")?;
378
379                self.write_attributes(attributes, spreads, true, brace, has_children)?;
380
381                if !children.is_empty() && !attributes.is_empty() {
382                    write!(self.out, " ")?;
383                }
384
385                let mut children_iter = children.iter().peekable();
386                while let Some(child) = children_iter.next() {
387                    self.write_ident(child)?;
388                    if children_iter.peek().is_some() {
389                        write!(self.out, " ")?;
390                    }
391                }
392
393                write!(self.out, " ")?;
394            }
395
396            ShortOptimization::PropsOnTop => {
397                if !attributes.is_empty() {
398                    write!(self.out, " ")?;
399                }
400
401                self.write_attributes(attributes, spreads, true, brace, has_children)?;
402
403                if !children.is_empty() {
404                    self.out.new_line()?;
405                    self.write_body_indented(children)?;
406                }
407
408                self.out.tabbed_line()?;
409            }
410
411            ShortOptimization::NoOpt => {
412                self.write_inline_comments(brace.span.span().start(), 1)?;
413                self.out.new_line()?;
414                self.write_attributes(attributes, spreads, false, brace, has_children)?;
415
416                if !children.is_empty() {
417                    self.out.new_line()?;
418                    self.write_body_indented(children)?;
419                }
420
421                self.out.tabbed_line()?;
422            }
423        }
424
425        // Write trailing comments
426        if matches!(
427            opt_level,
428            ShortOptimization::NoOpt | ShortOptimization::PropsOnTop
429        ) && self.leading_row_is_empty(brace.span.span().end())
430        {
431            let comments = self.accumulate_full_line_comments(brace.span.span().end());
432            if !comments.is_empty() {
433                self.apply_line_comments(comments)?;
434                self.out.tab()?;
435            }
436        }
437
438        write!(self.out, "}}")?;
439
440        Ok(())
441    }
442
443    fn write_attributes(
444        &mut self,
445        attributes: &[Attribute],
446        spreads: &[Spread],
447        props_same_line: bool,
448        brace: &Brace,
449        has_children: bool,
450    ) -> Result {
451        enum AttrType<'a> {
452            Attr(&'a Attribute),
453            Spread(&'a Spread),
454        }
455
456        let mut attr_iter = attributes
457            .iter()
458            .map(AttrType::Attr)
459            .chain(spreads.iter().map(AttrType::Spread))
460            .peekable();
461
462        let has_attributes = !attributes.is_empty() || !spreads.is_empty();
463
464        while let Some(attr) = attr_iter.next() {
465            self.out.indent_level += 1;
466
467            if !props_same_line {
468                self.write_attr_comments(
469                    brace,
470                    match attr {
471                        AttrType::Attr(attr) => attr.span(),
472                        AttrType::Spread(attr) => attr.expr.span(),
473                    },
474                )?;
475            }
476
477            self.out.indent_level -= 1;
478
479            if !props_same_line {
480                self.out.indented_tab()?;
481            }
482
483            match attr {
484                AttrType::Attr(attr) => self.write_attribute(attr)?,
485                AttrType::Spread(attr) => self.write_spread_attribute(&attr.expr)?,
486            }
487
488            let span = match attr {
489                AttrType::Attr(attr) => attr
490                    .comma
491                    .as_ref()
492                    .map(|c| c.span())
493                    .unwrap_or_else(|| self.total_span_of_attr(attr)),
494                AttrType::Spread(attr) => attr.span(),
495            };
496
497            let has_more = attr_iter.peek().is_some();
498            let should_finish_comma = has_attributes && has_children || !props_same_line;
499
500            if has_more || should_finish_comma {
501                write!(self.out, ",")?;
502            }
503
504            if !props_same_line {
505                self.write_inline_comments(span.end(), 0)?;
506            }
507
508            if props_same_line && !has_more {
509                self.write_inline_comments(span.end(), 0)?;
510            }
511
512            if props_same_line && has_more {
513                write!(self.out, " ")?;
514            }
515
516            if !props_same_line && has_more {
517                self.out.new_line()?;
518            }
519        }
520
521        Ok(())
522    }
523
524    fn write_attribute(&mut self, attr: &Attribute) -> Result {
525        self.write_attribute_name(&attr.name)?;
526
527        // if the attribute is a shorthand, we don't need to write the colon, just the name
528        if !attr.can_be_shorthand() {
529            write!(self.out, ": ")?;
530            self.write_attribute_value(&attr.value)?;
531        }
532
533        Ok(())
534    }
535
536    fn write_attribute_name(&mut self, attr: &AttributeName) -> Result {
537        match attr {
538            AttributeName::BuiltIn(name) => write!(self.out, "{}", name),
539            AttributeName::Custom(name) => write!(self.out, "{}", name.to_token_stream()),
540            AttributeName::Spread(_) => unreachable!(),
541        }
542    }
543
544    fn write_attribute_value(&mut self, value: &AttributeValue) -> Result {
545        match value {
546            AttributeValue::IfExpr(if_chain) => {
547                self.write_attribute_if_chain(if_chain)?;
548            }
549            AttributeValue::AttrLiteral(value) => {
550                write!(self.out, "{value}")?;
551            }
552            AttributeValue::Shorthand(value) => {
553                write!(self.out, "{value}")?;
554            }
555            AttributeValue::EventTokens(closure) => {
556                self.out.indent_level += 1;
557                self.write_partial_expr(closure.as_expr(), closure.span())?;
558                self.out.indent_level -= 1;
559            }
560            AttributeValue::AttrExpr(value) => {
561                self.out.indent_level += 1;
562                self.write_partial_expr(value.as_expr(), value.span())?;
563                self.out.indent_level -= 1;
564            }
565        }
566
567        Ok(())
568    }
569
570    fn write_attribute_if_chain(&mut self, if_chain: &IfAttributeValue) -> Result {
571        let cond = self.unparse_expr(&if_chain.if_expr.cond);
572        write!(self.out, "if {cond} {{ ")?;
573        self.write_attribute_value(&if_chain.then_value)?;
574        write!(self.out, " }}")?;
575        match if_chain.else_value.as_deref() {
576            Some(AttributeValue::IfExpr(else_if_chain)) => {
577                write!(self.out, " else ")?;
578                self.write_attribute_if_chain(else_if_chain)?;
579            }
580            Some(other) => {
581                write!(self.out, " else {{ ")?;
582                self.write_attribute_value(other)?;
583                write!(self.out, " }}")?;
584            }
585            None => {}
586        }
587
588        Ok(())
589    }
590
591    fn write_attr_comments(&mut self, brace: &Brace, attr_span: Span) -> Result {
592        // There's a chance this line actually shares the same line as the previous
593        // Only write comments if the comments actually belong to this line
594        //
595        // to do this, we check if the attr span starts on the same line as the brace
596        // if it doesn't, we write the comments
597        let brace_line = brace.span.span().start().line;
598        let attr_line = attr_span.start().line;
599
600        if brace_line != attr_line {
601            // Get the raw line of the attribute
602            let line = self.src.get(attr_line - 1).unwrap_or(&"");
603
604            // Only write comments if the line is empty before the attribute start
605            let row_start = line.get(..attr_span.start().column - 1).unwrap_or("");
606            if !row_start.trim().is_empty() {
607                return Ok(());
608            }
609
610            self.write_comments(attr_span.start())?;
611        }
612
613        Ok(())
614    }
615
616    fn write_inline_comments(&mut self, final_span: LineColumn, offset: usize) -> Result {
617        let line = final_span.line;
618        let column = final_span.column;
619        let Some(src_line) = self.src.get(line - 1) else {
620            return Ok(());
621        };
622
623        // the line might contain emoji or other unicode characters - this will cause issues
624        let Some(mut whitespace) = src_line.get(column..).map(|s| s.trim()) else {
625            return Ok(());
626        };
627
628        if whitespace.is_empty() {
629            return Ok(());
630        }
631
632        whitespace = whitespace[offset..].trim();
633
634        // don't emit whitespace if the span is messed up for some reason
635        if final_span.line == 1 && final_span.column == 0 {
636            return Ok(());
637        };
638
639        if whitespace.starts_with("//") {
640            write!(self.out, " {whitespace}")?;
641        }
642
643        Ok(())
644    }
645
646    fn accumulate_full_line_comments(&mut self, loc: LineColumn) -> VecDeque<usize> {
647        // collect all comments upwards
648        // make sure we don't collect the comments of the node that we're currently under.
649        let start = loc;
650        let line_start = start.line - 1;
651
652        let mut comments = VecDeque::new();
653
654        // don't emit whitespace if the span is messed up for some reason
655        if loc.line == 1 && loc.column == 0 {
656            return comments;
657        };
658
659        let Some(lines) = self.src.get(..line_start) else {
660            return comments;
661        };
662
663        // We go backwards to collect comments and empty lines. We only want to keep one empty line,
664        // the rest should be `//` comments
665        let mut last_line_was_empty = false;
666        for (id, line) in lines.iter().enumerate().rev() {
667            let trimmed = line.trim();
668            if trimmed.starts_with("//") {
669                comments.push_front(id);
670                last_line_was_empty = false;
671            } else if trimmed.is_empty() {
672                if !last_line_was_empty {
673                    comments.push_front(id);
674                    last_line_was_empty = true;
675                }
676
677                continue;
678            } else {
679                break;
680            }
681        }
682
683        // If there is more than 1 comment, make sure the first comment is not an empty line
684        if comments.len() > 1 {
685            if let Some(&first) = comments.back() {
686                if self.src[first].trim().is_empty() {
687                    comments.pop_back();
688                }
689            }
690        }
691
692        comments
693    }
694
695    fn apply_line_comments(&mut self, mut comments: VecDeque<usize>) -> Result {
696        while let Some(comment_line) = comments.pop_front() {
697            let Some(line) = self.src.get(comment_line) else {
698                continue;
699            };
700
701            let line = &line.trim();
702
703            if line.is_empty() {
704                self.out.new_line()?;
705            } else {
706                self.out.tab()?;
707                writeln!(self.out, "{}", line.trim())?;
708            }
709        }
710        Ok(())
711    }
712
713    fn write_comments(&mut self, loc: LineColumn) -> Result {
714        let comments = self.accumulate_full_line_comments(loc);
715        self.apply_line_comments(comments)?;
716        Ok(())
717    }
718
719    fn attr_value_len(&mut self, value: &AttributeValue) -> usize {
720        match value {
721            AttributeValue::IfExpr(if_chain) => {
722                let condition_len = self.retrieve_formatted_expr(&if_chain.if_expr.cond).len();
723                let value_len = self.attr_value_len(&if_chain.then_value);
724                let if_len = 2;
725                let brace_len = 2;
726                let space_len = 2;
727                let else_len = if_chain
728                    .else_value
729                    .as_ref()
730                    .map(|else_value| self.attr_value_len(else_value) + 1)
731                    .unwrap_or_default();
732                condition_len + value_len + if_len + brace_len + space_len + else_len
733            }
734            AttributeValue::AttrLiteral(lit) => lit.to_string().len(),
735            AttributeValue::Shorthand(expr) => {
736                let span = &expr.span();
737                span.end().line - span.start().line
738            }
739            AttributeValue::AttrExpr(expr) => expr
740                .as_expr()
741                .map(|expr| self.attr_expr_len(&expr))
742                .unwrap_or(100000),
743            AttributeValue::EventTokens(closure) => closure
744                .as_expr()
745                .map(|expr| self.attr_expr_len(&expr))
746                .unwrap_or(100000),
747        }
748    }
749
750    fn attr_expr_len(&mut self, expr: &Expr) -> usize {
751        let out = self.retrieve_formatted_expr(expr);
752        if out.contains('\n') {
753            100000
754        } else {
755            out.len()
756        }
757    }
758
759    fn is_short_attrs(
760        &mut self,
761        _brace: &Brace,
762        attributes: &[Attribute],
763        spreads: &[Spread],
764    ) -> usize {
765        let mut total = 0;
766
767        // No more than 3 attributes before breaking the line
768        if attributes.len() > 3 {
769            return 100000;
770        }
771
772        for attr in attributes {
773            if self.current_span_is_primary(attr.span().start()) {
774                if let Some(lines) = self.src.get(..attr.span().start().line - 1) {
775                    'line: for line in lines.iter().rev() {
776                        match (line.trim().starts_with("//"), line.is_empty()) {
777                            (true, _) => return 100000,
778                            (_, true) => continue 'line,
779                            _ => break 'line,
780                        }
781                    }
782                };
783            }
784
785            total += match &attr.name {
786                AttributeName::BuiltIn(name) => {
787                    let name = name.to_string();
788                    name.len()
789                }
790                AttributeName::Custom(name) => name.value().len() + 2,
791                AttributeName::Spread(_) => unreachable!(),
792            };
793
794            if attr.can_be_shorthand() {
795                total += 2;
796            } else {
797                total += self.attr_value_len(&attr.value);
798            }
799
800            total += 6;
801        }
802
803        for spread in spreads {
804            let expr_len = self.retrieve_formatted_expr(&spread.expr).len();
805            total += expr_len + 3;
806        }
807
808        total
809    }
810
811    fn write_todo_body(&mut self, brace: &Brace) -> std::fmt::Result {
812        let span = brace.span.span();
813        let start = span.start();
814        let end = span.end();
815
816        if start.line == end.line {
817            return Ok(());
818        }
819
820        writeln!(self.out)?;
821
822        for idx in start.line..end.line {
823            let Some(line) = self.src.get(idx) else {
824                continue;
825            };
826            if line.trim().starts_with("//") {
827                for _ in 0..self.out.indent_level + 1 {
828                    write!(self.out, "    ")?
829                }
830                writeln!(self.out, "{}", line.trim())?;
831            }
832        }
833
834        for _ in 0..self.out.indent_level {
835            write!(self.out, "    ")?
836        }
837
838        Ok(())
839    }
840
841    fn write_partial_expr(&mut self, expr: syn::Result<Expr>, src_span: Span) -> Result {
842        let Ok(expr) = expr else {
843            self.invalid_exprs.push(src_span);
844            return Err(std::fmt::Error);
845        };
846
847        thread_local! {
848            static COMMENT_REGEX: Regex = Regex::new("\"[^\"]*\"|(//.*)").unwrap();
849        }
850
851        let pretty = self.retrieve_formatted_expr(&expr).to_string();
852        let source = src_span.source_text().unwrap_or_default();
853        let mut src_lines = source.lines().peekable();
854
855        // Comments already in pretty output (from nested rsx!) - skip these from source
856        let pretty_comments: HashSet<_> = pretty
857            .lines()
858            .filter(|l| l.trim().starts_with("//"))
859            .map(|l| l.trim())
860            .collect();
861
862        let mut out = String::new();
863
864        if src_lines.peek().is_none() {
865            out = pretty;
866        } else {
867            for line in pretty.lines() {
868                let trimmed = line.trim();
869                let compacted = line.replace(" ", "").replace(",", "");
870
871                // Pretty comments: consume matching source lines, preserve preceding empty lines
872                if trimmed.starts_with("//") {
873                    if !out.is_empty() {
874                        out.push('\n');
875                    }
876                    let mut had_empty = false;
877                    while let Some(s) = src_lines.peek() {
878                        let t = s.trim();
879                        if t.is_empty() {
880                            had_empty = true;
881                            src_lines.next();
882                        } else if t == trimmed {
883                            src_lines.next();
884                            break;
885                        } else {
886                            break;
887                        }
888                    }
889                    if had_empty {
890                        out.push('\n');
891                    }
892                    out.push_str(line);
893                    continue;
894                }
895
896                // Pretty empty lines: preserve and sync with source
897                if trimmed.is_empty() {
898                    if !out.is_empty() {
899                        out.push('\n');
900                    }
901                    while src_lines
902                        .peek()
903                        .map(|s| s.trim().is_empty())
904                        .unwrap_or(false)
905                    {
906                        src_lines.next();
907                    }
908                    continue;
909                }
910
911                if !out.is_empty() {
912                    out.push('\n');
913                }
914
915                // Scan source for comments/empty lines before the matching line
916                let mut pending_comments = Vec::new();
917                let mut had_empty = false;
918                let mut multiline: Option<Vec<&str>> = None;
919
920                while let Some(src) = src_lines.peek() {
921                    let src_trimmed = src.trim();
922
923                    if src_trimmed.is_empty() || src_trimmed.starts_with("//") {
924                        if src_trimmed.is_empty() {
925                            if pending_comments.is_empty() {
926                                had_empty = true;
927                            }
928                        } else if !pretty_comments.contains(src_trimmed) {
929                            pending_comments.push(src_trimmed);
930                        }
931                        src_lines.next();
932                        continue;
933                    }
934
935                    let src_compacted = src.replace(" ", "").replace(",", "");
936
937                    // Exact match
938                    if src_compacted.contains(&compacted) {
939                        break;
940                    }
941
942                    // Multi-line method chain (e.g., foo\n  .bar()\n  .baz())
943                    if !src_compacted.is_empty() && compacted.starts_with(&src_compacted) {
944                        let is_call = src_trimmed.ends_with('(')
945                            || src_trimmed.ends_with(',')
946                            || src_trimmed.ends_with('{');
947                        if !is_call {
948                            multiline = Some(vec![*src]);
949                            break;
950                        }
951                    }
952
953                    // Non-matching line - clear pending and skip
954                    pending_comments.clear();
955                    had_empty = false;
956                    src_lines.next();
957                    break;
958                }
959
960                // Output empty line if needed
961                if had_empty {
962                    out.push('\n');
963                }
964
965                // Output pending comments
966                for comment in &pending_comments {
967                    for c in line.chars().take_while(|c| c.is_whitespace()) {
968                        out.push(c);
969                    }
970                    if matches!(trimmed.chars().next(), Some(')' | '}' | ']')) {
971                        out.push_str(self.out.indent.indent_str());
972                    }
973                    out.push_str(comment);
974                    out.push('\n');
975                }
976
977                // Handle multi-line method chains
978                if let Some(mut ml) = multiline {
979                    src_lines.next();
980                    let mut acc = ml[0].replace(" ", "").replace(",", "");
981
982                    while let Some(src) = src_lines.peek() {
983                        let t = src.trim();
984                        if t.starts_with("//") {
985                            ml.push(src);
986                            src_lines.next();
987                            continue;
988                        }
989                        if t.is_empty() {
990                            src_lines.next();
991                            continue;
992                        }
993
994                        acc.push_str(&src.replace(" ", "").replace(",", ""));
995                        ml.push(src);
996
997                        if acc.contains(&compacted) {
998                            src_lines.next();
999                            break;
1000                        }
1001
1002                        let cont = t.starts_with('.')
1003                            || t.starts_with("&&")
1004                            || t.starts_with("||")
1005                            || matches!(t.chars().next(), Some('+' | '-' | '*' | '/' | '?'));
1006
1007                        if cont || compacted.starts_with(&acc) {
1008                            src_lines.next();
1009                            continue;
1010                        }
1011                        break;
1012                    }
1013
1014                    // Write multi-line with adjusted indentation
1015                    let base_indent = ml[0].chars().take_while(|c| c.is_whitespace()).count();
1016                    let target: String = line.chars().take_while(|c| c.is_whitespace()).collect();
1017
1018                    for (i, src_line) in ml.iter().enumerate() {
1019                        let indent = src_line.chars().take_while(|c| c.is_whitespace()).count();
1020                        out.push_str(&target);
1021                        for _ in 0..indent.saturating_sub(base_indent) {
1022                            out.push(' ');
1023                        }
1024                        out.push_str(src_line.trim());
1025                        if i < ml.len() - 1 {
1026                            out.push('\n');
1027                        }
1028                    }
1029                } else {
1030                    // Single line - output pretty line and capture inline comments
1031                    out.push_str(line);
1032                    if let Some(src_line) = src_lines.next() {
1033                        if let Some(cap) = COMMENT_REGEX.with(|r| r.captures(src_line)) {
1034                            if let Some(c) = cap.get(1) {
1035                                out.push_str(" // ");
1036                                out.push_str(c.as_str().replace("//", "").trim());
1037                            }
1038                        }
1039                    }
1040                }
1041            }
1042        }
1043
1044        self.write_mulitiline_tokens(out)?;
1045        Ok(())
1046    }
1047
1048    fn write_mulitiline_tokens(&mut self, out: String) -> Result {
1049        let mut lines = out.split('\n').peekable();
1050        let first = lines.next().unwrap();
1051
1052        // a one-liner for whatever reason
1053        // Does not need a new line
1054        if lines.peek().is_none() {
1055            write!(self.out, "{first}")?;
1056        } else {
1057            writeln!(self.out, "{first}")?;
1058
1059            while let Some(line) = lines.next() {
1060                if !line.trim().is_empty() {
1061                    self.out.tab()?;
1062                }
1063
1064                write!(self.out, "{line}")?;
1065                if lines.peek().is_none() {
1066                    write!(self.out, "")?;
1067                } else {
1068                    writeln!(self.out)?;
1069                }
1070            }
1071        }
1072
1073        Ok(())
1074    }
1075
1076    fn write_spread_attribute(&mut self, attr: &Expr) -> Result {
1077        let formatted = self.unparse_expr(attr);
1078
1079        let mut lines = formatted.lines();
1080
1081        let first_line = lines.next().unwrap();
1082
1083        write!(self.out, "..{first_line}")?;
1084        for line in lines {
1085            self.out.indented_tabbed_line()?;
1086            write!(self.out, "{line}")?;
1087        }
1088
1089        Ok(())
1090    }
1091
1092    // check if the children are short enough to be on the same line
1093    // We don't have the notion of current line depth - each line tries to be < 80 total
1094    // returns the total line length if it's short
1095    // returns none if the length exceeds the limit
1096    // I think this eventually becomes quadratic :(
1097    fn is_short_children(&mut self, children: &[BodyNode]) -> syn::Result<Option<usize>> {
1098        if children.is_empty() {
1099            return Ok(Some(0));
1100        }
1101
1102        // Any comments push us over the limit automatically
1103        if self.children_have_comments(children) {
1104            return Ok(None);
1105        }
1106
1107        let res = match children {
1108            [BodyNode::Text(ref text)] => Some(text.input.to_string_with_quotes().len()),
1109
1110            // TODO: let rawexprs to be inlined
1111            [BodyNode::RawExpr(ref expr)] => {
1112                let pretty = self.retrieve_formatted_expr(&expr.expr.as_expr()?);
1113                if pretty.contains('\n') {
1114                    None
1115                } else {
1116                    Some(pretty.len() + 2)
1117                }
1118            }
1119
1120            // TODO: let rawexprs to be inlined
1121            [BodyNode::Component(ref comp)]
1122            // basically if the component is completely empty, we can inline it
1123                if comp.fields.is_empty()
1124                    && comp.children.is_empty()
1125                    && comp.spreads.is_empty() =>
1126            {
1127                Some(
1128                    comp.name
1129                        .segments
1130                        .iter()
1131                        .map(|s| s.ident.to_string().len() + 2)
1132                        .sum::<usize>(),
1133                )
1134            }
1135
1136            // Feedback on discord indicates folks don't like combining multiple children on the same line
1137            // We used to do a lot of math to figure out if we should expand out the line, but folks just
1138            // don't like it.
1139            _ => None,
1140        };
1141
1142        Ok(res)
1143    }
1144
1145    fn children_have_comments(&self, children: &[BodyNode]) -> bool {
1146        for child in children {
1147            if self.current_span_is_primary(child.span().start()) {
1148                'line: for line in self.src[..child.span().start().line - 1].iter().rev() {
1149                    match (line.trim().starts_with("//"), line.is_empty()) {
1150                        (true, _) => return true,
1151                        (_, true) => continue 'line,
1152                        _ => break 'line,
1153                    }
1154                }
1155            }
1156        }
1157
1158        false
1159    }
1160
1161    // make sure the comments are actually relevant to this element.
1162    // test by making sure this element is the primary element on this line (nothing else before it)
1163    fn current_span_is_primary(&self, location: LineColumn) -> bool {
1164        self.leading_row_is_empty(LineColumn {
1165            line: location.line,
1166            column: location.column + 1,
1167        })
1168    }
1169
1170    fn leading_row_is_empty(&self, location: LineColumn) -> bool {
1171        let Some(line) = self.src.get(location.line - 1) else {
1172            return false;
1173        };
1174
1175        let Some(sub) = line.get(..location.column - 1) else {
1176            return false;
1177        };
1178
1179        sub.trim().is_empty()
1180    }
1181
1182    #[allow(clippy::map_entry)]
1183    fn retrieve_formatted_expr(&mut self, expr: &Expr) -> Cow<'_, str> {
1184        let loc = expr.span().start();
1185
1186        // never cache expressions that are spanless
1187        if loc.line == 1 && loc.column == 0 {
1188            return self.unparse_expr(expr).into();
1189        }
1190
1191        if !self.cached_formats.contains_key(&loc) {
1192            let formatted = self.unparse_expr(expr);
1193            self.cached_formats.insert(loc, formatted);
1194        }
1195
1196        self.cached_formats
1197            .get(&loc)
1198            .expect("Just inserted the parsed expr, so it should be in the cache")
1199            .as_str()
1200            .into()
1201    }
1202
1203    fn final_span_of_node(node: &BodyNode) -> Span {
1204        // Get the ending span of the node
1205        match node {
1206            BodyNode::Element(el) => el
1207                .brace
1208                .as_ref()
1209                .map(|b| b.span.span())
1210                .unwrap_or_else(|| el.name.span()),
1211            BodyNode::Component(el) => el
1212                .brace
1213                .as_ref()
1214                .map(|b| b.span.span())
1215                .unwrap_or_else(|| el.name.span()),
1216            BodyNode::Text(txt) => txt.input.span(),
1217            BodyNode::RawExpr(exp) => exp.span(),
1218            BodyNode::ForLoop(f) => f.brace.span.span(),
1219            BodyNode::IfChain(i) => match i.else_brace {
1220                Some(b) => b.span.span(),
1221                None => i.then_brace.span.span(),
1222            },
1223        }
1224    }
1225
1226    fn total_span_of_attr(&self, attr: &Attribute) -> Span {
1227        match &attr.value {
1228            AttributeValue::Shorthand(s) => s.span(),
1229            AttributeValue::AttrLiteral(l) => l.span(),
1230            AttributeValue::EventTokens(closure) => closure.span(),
1231            AttributeValue::AttrExpr(exp) => exp.span(),
1232            AttributeValue::IfExpr(ex) => ex.span(),
1233        }
1234    }
1235
1236    fn brace_has_trailing_comments(&self, brace: &Brace) -> bool {
1237        let span = brace.span.span();
1238        let line = self.src.get(span.start().line - 1).unwrap_or(&"");
1239        let after_brace = line.get(span.start().column + 1..).unwrap_or("").trim();
1240        after_brace.starts_with("//")
1241    }
1242
1243    fn has_trailing_comments(&self, children: &[BodyNode], brace: &Brace) -> bool {
1244        let brace_span = brace.span.span();
1245
1246        let Some(last_node) = children.last() else {
1247            return false;
1248        };
1249
1250        // Check for any comments after the last node between the last brace
1251        let final_span = Self::final_span_of_node(last_node);
1252        let final_span = final_span.end();
1253        let mut line = final_span.line;
1254        let mut column = final_span.column;
1255        loop {
1256            let Some(src_line) = self.src.get(line - 1) else {
1257                return false;
1258            };
1259
1260            // the line might contain emoji or other unicode characters - this will cause issues
1261            let Some(mut whitespace) = src_line.get(column..).map(|s| s.trim()) else {
1262                return false;
1263            };
1264
1265            let offset = 0;
1266            whitespace = whitespace[offset..].trim();
1267
1268            if whitespace.starts_with("//") {
1269                return true;
1270            }
1271
1272            if line == brace_span.end().line {
1273                // If we reached the end of the brace span, stop
1274                break;
1275            }
1276
1277            line += 1;
1278            column = 0; // reset column to the start of the next line
1279        }
1280
1281        false
1282    }
1283}