Skip to main content

euv_cli/fmt/
fn.rs

1use super::*;
2
3/// Formats a single Rust source file by reformatting all euv macro invocations.
4///
5/// Scans the source code for `html!`, `class!`, `vars!`, and `watch!`
6/// macro calls, then applies formatting rules to their token bodies:
7///
8/// - Tag name and `{` separated by exactly one space
9/// - Attribute name immediately followed by `:`, then one space before the value
10/// - `if { expr }` with single spaces around each token
11/// - `match { expr }` with proper alignment
12/// - `for pattern in { expr }` with proper alignment
13/// - `=>` in watch! macros with proper spacing
14///
15/// # Arguments
16///
17/// - `&str` - The Rust source code content.
18///
19/// # Returns
20///
21/// - `FmtResult` - The formatting result indicating whether changes were made.
22pub(crate) fn format_source(source: &str) -> FmtResult {
23    let formatted: String = format_euv_macros(source);
24    let changed: bool = formatted != source;
25    FmtResult::new(changed, formatted)
26}
27
28/// Finds and reformats all euv macro invocations in the source text.
29///
30/// Uses a character-level scanner to locate macro calls (e.g., `html! { ... }`),
31/// tracks brace depth to find the complete macro body, then formats the body
32/// using `format_macro_body`.
33///
34/// # Arguments
35///
36/// - `S: AsRef<str>` - The Rust source code content.
37///
38/// # Returns
39///
40/// - `String` - The source with all euv macros reformatted.
41pub fn format_euv_macros<S>(source: S) -> String
42where
43    S: AsRef<str>,
44{
45    let source: &str = source.as_ref();
46    let mut result: String = String::new();
47    let chars: Vec<char> = source.chars().collect();
48    let len: usize = chars.len();
49    let mut position: usize = 0;
50    while position < len {
51        if is_euv_macro_start(&chars, position, len) {
52            let macro_name_end: usize = find_macro_name_end(&chars, position, len);
53            let name: String = chars[position..macro_name_end].iter().collect::<String>();
54            result.push_str(&name);
55            position = macro_name_end;
56            position = skip_whitespace_and_comments(&chars, position, len, &mut result);
57            if position < len && chars[position] == CHAR_MACRO_BANG {
58                result.push(CHAR_MACRO_BANG);
59                position += 1;
60                position = skip_whitespace_and_comments(&chars, position, len, &mut result);
61                if position < len && chars[position] == CHAR_BRACE_LEFT {
62                    if !result.ends_with(CHAR_SPACE) {
63                        result.push(CHAR_SPACE);
64                    }
65                    let (body_content, end_pos) = extract_brace_content(&chars, position);
66                    let formatted_body: String = format_macro_body(&body_content);
67                    if formatted_body.trim().is_empty() {
68                        result.push(CHAR_BRACE_LEFT);
69                        result.push(CHAR_BRACE_RIGHT);
70                    } else {
71                        let trimmed_body: &str = formatted_body.trim();
72                        let last_newline_pos: usize = result
73                            .rfind(CHAR_NEWLINE)
74                            .map(|position: usize| position + 1)
75                            .unwrap_or_default();
76                        let macro_indent: usize = result[last_newline_pos..]
77                            .chars()
78                            .take_while(|character: &char| *character == CHAR_SPACE)
79                            .count();
80                        let min_indent: usize = body_content
81                            .lines()
82                            .filter(|line: &&str| !line.trim().is_empty())
83                            .map(|line: &str| {
84                                line.chars()
85                                    .take_while(|character: &char| character.is_whitespace())
86                                    .count()
87                            })
88                            .min()
89                            .unwrap_or_default();
90                        let base_indent: usize = if min_indent > 0 {
91                            min_indent
92                        } else {
93                            macro_indent + 4
94                        };
95                        let indent_str: String = " ".repeat(base_indent);
96                        let indented_body: String = trimmed_body
97                            .lines()
98                            .map(|line: &str| {
99                                if line.trim().is_empty() {
100                                    line.to_string()
101                                } else {
102                                    format!("{indent_str}{line}")
103                                }
104                            })
105                            .collect::<Vec<String>>()
106                            .join("\n");
107                        let outer_indent_str: String = " ".repeat(macro_indent);
108                        result.push(CHAR_BRACE_LEFT);
109                        result.push(CHAR_NEWLINE);
110                        result.push_str(&indented_body);
111                        result.push(CHAR_NEWLINE);
112                        result.push_str(&outer_indent_str);
113                        result.push(CHAR_BRACE_RIGHT);
114                    }
115                    position = end_pos;
116                    continue;
117                }
118            }
119            continue;
120        }
121        if chars[position] == CHAR_DOUBLE_QUOTE || chars[position] == CHAR_SINGLE_QUOTE {
122            let (literal, end_pos) = extract_string_literal(&chars, position, len);
123            result.push_str(&literal);
124            position = end_pos;
125            continue;
126        }
127        if position + 1 < len
128            && chars[position] == CHAR_SLASH_FORWARD
129            && chars[position + 1] == CHAR_SLASH_FORWARD
130        {
131            let (comment, end_pos) = extract_line_comment(&chars, position, len);
132            result.push_str(&comment);
133            position = end_pos;
134            continue;
135        }
136        if position + 1 < len
137            && chars[position] == CHAR_SLASH_FORWARD
138            && chars[position + 1] == CHAR_ASTERISK
139        {
140            let (comment, end_pos) = extract_block_comment(&chars, position, len);
141            result.push_str(&comment);
142            position = end_pos;
143            continue;
144        }
145        result.push(chars[position]);
146        position += 1;
147    }
148    result
149}
150
151/// Checks whether the current position starts a known euv macro name.
152///
153/// # Arguments
154///
155/// - `&[char]` - The source character slice.
156/// - `usize` - The current position.
157/// - `usize` - The total length of the source.
158///
159/// # Returns
160///
161/// - `bool` - Whether a euv macro name starts at the given position.
162fn is_euv_macro_start(chars: &[char], pos: usize, len: usize) -> bool {
163    for name in EUV_MACRO_NAMES {
164        let name_len: usize = name.len();
165        if pos + name_len > len {
166            continue;
167        }
168        let candidate: String = chars[pos..pos + name_len].iter().collect::<String>();
169        if candidate != *name {
170            continue;
171        }
172        if pos > 0 && is_ident_char(chars[pos - 1]) {
173            continue;
174        }
175        if pos + name_len < len && is_ident_char(chars[pos + name_len]) {
176            continue;
177        }
178        return true;
179    }
180    false
181}
182
183/// Finds the end position of a macro name starting at the given position.
184///
185/// # Arguments
186///
187/// - `&[char]` - The source character slice.
188/// - `usize` - The start position of the macro name.
189/// - `usize` - The total length of the source.
190///
191/// # Returns
192///
193/// - `usize` - The end position (exclusive) of the macro name.
194fn find_macro_name_end(chars: &[char], pos: usize, _len: usize) -> usize {
195    let mut end: usize = pos;
196    while end < chars.len() && is_ident_char(chars[end]) {
197        end += 1;
198    }
199    end
200}
201
202/// Checks whether the keyword at the given position is preceded by `r#`,
203/// indicating a Rust raw identifier rather than a keyword.
204///
205/// # Arguments
206///
207/// - `&[char]` - The source character slice.
208/// - `usize` - The position of the keyword.
209///
210/// # Returns
211///
212/// - `bool` - Whether the keyword is preceded by `r#`.
213fn is_raw_prefix(chars: &[char], pos: usize) -> bool {
214    if pos < 2 {
215        return false;
216    }
217    chars[pos - 2] == CHAR_LETTER_R && chars[pos - 1] == CHAR_HASH
218}
219
220/// Checks if a character can be part of a Rust identifier.
221///
222/// # Arguments
223///
224/// - `char` - The character to check.
225///
226/// # Returns
227///
228/// - `bool` - `true` if the character is alphanumeric or underscore.
229fn is_ident_char(character: char) -> bool {
230    character.is_alphanumeric() || character == CHAR_UNDERSCORE
231}
232
233/// Skips whitespace and comments, preserving them in the output.
234///
235/// # Arguments
236///
237/// - `&[char]` - The source character slice.
238/// - `usize` - The current position.
239/// - `usize` - The total length.
240/// - `&mut String` - The output string to append preserved whitespace/comments to.
241///
242/// # Returns
243///
244/// - `usize` - The new position after skipping whitespace and comments.
245fn skip_whitespace_and_comments(
246    chars: &[char],
247    mut pos: usize,
248    len: usize,
249    result: &mut String,
250) -> usize {
251    while pos < len {
252        if chars[pos].is_whitespace() {
253            result.push(chars[pos]);
254            pos += 1;
255        } else if pos + 1 < len
256            && chars[pos] == CHAR_SLASH_FORWARD
257            && chars[pos + 1] == CHAR_SLASH_FORWARD
258        {
259            let (comment, end_pos) = extract_line_comment(chars, pos, len);
260            result.push_str(&comment);
261            pos = end_pos;
262        } else if pos + 1 < len
263            && chars[pos] == CHAR_SLASH_FORWARD
264            && chars[pos + 1] == CHAR_ASTERISK
265        {
266            let (comment, end_pos) = extract_block_comment(chars, pos, len);
267            result.push_str(&comment);
268            pos = end_pos;
269        } else {
270            break;
271        }
272    }
273    pos
274}
275
276/// Extracts a brace-enclosed block verbatim (including the braces).
277///
278/// Assumes `chars[start]` is `{`. Returns the full content (including the
279/// outer `{` and `}`) and the position after the closing `}`.
280///
281/// # Arguments
282///
283/// - `&[char]` - The source character slice.
284/// - `usize` - The position of the opening `{`.
285///
286/// # Returns
287///
288/// - `(String, usize)` - The content including braces and the position after the closing `}`.
289fn extract_brace_block(chars: &[char], start: usize) -> (String, usize) {
290    let mut depth: i32 = 0;
291    let mut position: usize = start;
292    while position < chars.len() {
293        if chars[position] == CHAR_DOUBLE_QUOTE || chars[position] == CHAR_SINGLE_QUOTE {
294            let (_, end) = extract_string_literal(chars, position, chars.len());
295            position = end;
296            continue;
297        }
298        if position + 1 < chars.len()
299            && chars[position] == CHAR_SLASH_FORWARD
300            && chars[position + 1] == CHAR_SLASH_FORWARD
301        {
302            let (_, end) = extract_line_comment(chars, position, chars.len());
303            position = end;
304            continue;
305        }
306        if position + 1 < chars.len()
307            && chars[position] == CHAR_SLASH_FORWARD
308            && chars[position + 1] == CHAR_ASTERISK
309        {
310            let (_, end) = extract_block_comment(chars, position, chars.len());
311            position = end;
312            continue;
313        }
314        if chars[position] == CHAR_BRACE_LEFT {
315            depth += 1;
316        } else if chars[position] == CHAR_BRACE_RIGHT {
317            depth -= 1;
318            if depth == 0 {
319                let content: String = chars[start..=position].iter().collect();
320                return (content, position + 1);
321            }
322        }
323        position += 1;
324    }
325    let content: String = chars[start..].iter().collect();
326    (content, chars.len())
327}
328
329/// Extracts the content inside a brace-delimited block (without the outer braces).
330///
331/// Assumes `chars[start]` is `{`. Returns the inner content (without the
332/// outer braces) and the position after the closing `}`.
333///
334/// # Arguments
335///
336/// - `&[char]` - The source character slice.
337/// - `usize` - The position of the opening `{`.
338///
339/// # Returns
340///
341/// - `(String, usize)` - The content inside the braces and the position after the closing `}`.
342fn extract_brace_content(chars: &[char], start: usize) -> (String, usize) {
343    let mut depth: i32 = 0;
344    let mut position: usize = start;
345    let mut content_start: usize = start + 1;
346    while position < chars.len() {
347        if chars[position] == CHAR_DOUBLE_QUOTE || chars[position] == CHAR_SINGLE_QUOTE {
348            let (_, end) = extract_string_literal(chars, position, chars.len());
349            position = end;
350            continue;
351        }
352        if position + 1 < chars.len()
353            && chars[position] == CHAR_SLASH_FORWARD
354            && chars[position + 1] == CHAR_SLASH_FORWARD
355        {
356            let (_, end) = extract_line_comment(chars, position, chars.len());
357            position = end;
358            continue;
359        }
360        if position + 1 < chars.len()
361            && chars[position] == CHAR_SLASH_FORWARD
362            && chars[position + 1] == CHAR_ASTERISK
363        {
364            let (_, end) = extract_block_comment(chars, position, chars.len());
365            position = end;
366            continue;
367        }
368        if chars[position] == CHAR_BRACE_LEFT {
369            if depth == 0 {
370                content_start = position + 1;
371            }
372            depth += 1;
373        } else if chars[position] == CHAR_BRACE_RIGHT {
374            depth -= 1;
375            if depth == 0 {
376                let content: String = chars[content_start..position].iter().collect();
377                return (content, position + 1);
378            }
379        }
380        position += 1;
381    }
382    let content: String = chars[content_start..].iter().collect();
383    (content, chars.len())
384}
385
386/// Extracts a string literal (single-quoted or double-quoted) from the source.
387///
388/// Handles escape sequences (`\"`, `\'`, `\\`).
389///
390/// # Arguments
391///
392/// - `&[char]` - The source character slice.
393/// - `usize` - The start position (at the opening quote).
394/// - `usize` - The total length.
395///
396/// # Returns
397///
398/// - `(String, usize)` - The literal text and the position after the closing quote.
399fn extract_string_literal(chars: &[char], start: usize, len: usize) -> (String, usize) {
400    let quote: char = chars[start];
401    let mut position: usize = start + 1;
402    let mut result: String = String::new();
403    result.push(quote);
404    while position < len {
405        if chars[position] == CHAR_SLASH_BACK && position + 1 < len {
406            result.push(chars[position]);
407            result.push(chars[position + 1]);
408            position += 2;
409            continue;
410        }
411        result.push(chars[position]);
412        if chars[position] == quote {
413            return (result, position + 1);
414        }
415        position += 1;
416    }
417    (result, position)
418}
419
420/// Extracts a line comment (`// ...`) from the source.
421///
422/// # Arguments
423///
424/// - `&[char]` - The source character slice.
425/// - `usize` - The start position (at the first `/`).
426/// - `usize` - The total length.
427///
428/// # Returns
429///
430/// - `(String, usize)` - The comment text and the position after the newline.
431fn extract_line_comment(chars: &[char], start: usize, len: usize) -> (String, usize) {
432    let mut position: usize = start;
433    let mut result: String = String::new();
434    while position < len && chars[position] != CHAR_NEWLINE {
435        result.push(chars[position]);
436        position += 1;
437    }
438    if position < len {
439        result.push(CHAR_NEWLINE);
440        position += 1;
441    }
442    (result, position)
443}
444
445/// Extracts a block comment (`/* ... */`) from the source.
446///
447/// # Arguments
448///
449/// - `&[char]` - The source character slice.
450/// - `usize` - The start position (at the first `/`).
451/// - `usize` - The total length.
452///
453/// # Returns
454///
455/// - `(String, usize)` - The comment text and the position after the closing `*/`.
456fn extract_block_comment(chars: &[char], start: usize, len: usize) -> (String, usize) {
457    let mut position: usize = start + 2;
458    let mut result: String = String::from(BLOCK_COMMENT_START);
459    while position + 1 < len {
460        result.push(chars[position]);
461        if chars[position] == CHAR_ASTERISK && chars[position + 1] == CHAR_SLASH_FORWARD {
462            result.push(CHAR_SLASH_FORWARD);
463            return (result, position + 2);
464        }
465        position += 1;
466    }
467    while position < len {
468        result.push(chars[position]);
469        position += 1;
470    }
471    (result, position)
472}
473
474/// Formats the body of a euv macro invocation.
475///
476/// First applies raw formatting rules, then adds proper indentation.
477///
478/// # Arguments
479///
480/// - `B: AsRef<str>` - The raw macro body text (without outer braces).
481///
482/// # Returns
483///
484/// - `String` - The formatted macro body text.
485pub fn format_macro_body<B>(body: B) -> String
486where
487    B: AsRef<str>,
488{
489    let raw: String = format_macro_body_raw(body);
490    add_indentation(&raw)
491}
492
493/// Formats the body of a euv macro invocation without adding indentation.
494///
495/// Uses a single-pass scanner that tracks context to apply formatting rules:
496///
497/// - Tag name and `{` separated by exactly one space
498/// - Attribute name immediately followed by `:`, then one space before the value
499/// - `if { expr }` / `match { expr }` / `for pattern in { expr }` with proper spacing
500/// - `=>` in watch macros with proper spacing
501///
502/// Content inside expression braces `{ expr }` is preserved verbatim.
503/// Only template-level whitespace is normalized.
504///
505/// # Arguments
506///
507/// - `B: AsRef<str>` - The raw macro body text (without outer braces).
508///
509/// # Returns
510///
511/// - `String` - The formatted macro body text.
512fn format_macro_body_raw<B>(body: B) -> String
513where
514    B: AsRef<str>,
515{
516    let body: &str = body.as_ref();
517    let chars: Vec<char> = body.chars().collect();
518    let len: usize = chars.len();
519    let mut result: String = String::new();
520    let mut position: usize = 0;
521    while position < len {
522        if chars[position] == CHAR_DOUBLE_QUOTE || chars[position] == CHAR_SINGLE_QUOTE {
523            let (literal, end) = extract_string_literal(&chars, position, len);
524            result.push_str(&literal);
525            position = end;
526            continue;
527        }
528        if position + 1 < len
529            && chars[position] == CHAR_SLASH_FORWARD
530            && chars[position + 1] == CHAR_SLASH_FORWARD
531        {
532            let (comment, end) = extract_line_comment(&chars, position, len);
533            result.push_str(&comment);
534            position = end;
535            continue;
536        }
537        if position + 1 < len
538            && chars[position] == CHAR_SLASH_FORWARD
539            && chars[position + 1] == CHAR_ASTERISK
540        {
541            let (comment, end) = extract_block_comment(&chars, position, len);
542            result.push_str(&comment);
543            position = end;
544            continue;
545        }
546        if is_if_keyword(&chars, position, len) {
547            let after_if_colon: usize = skip_spaces_on_same_line(&chars, position + 2, len);
548            if after_if_colon < len
549                && chars[after_if_colon] == CHAR_COLON
550                && (after_if_colon + 1 >= len || chars[after_if_colon + 1] != CHAR_COLON)
551            {
552                result.push_str(KEYWORD_IF);
553                position += 2;
554                if position < len && chars[position] == CHAR_SPACE {
555                    result.push(CHAR_SPACE);
556                    position += 1;
557                }
558                continue;
559            }
560            result.push_str(KEYWORD_IF);
561            position += 2;
562            let after_if: usize = skip_spaces_on_same_line(&chars, position, len);
563            if position < len && chars[after_if] == CHAR_BRACE_LEFT {
564                result.push(CHAR_SPACE);
565                let (block, end) = extract_brace_block(&chars, after_if);
566                result.push_str(&format_brace_block(&block));
567                position = end;
568                position = skip_spaces_on_same_line(&chars, position, len);
569                if position < len && chars[position] == CHAR_BRACE_LEFT {
570                    result.push(CHAR_SPACE);
571                }
572            } else {
573                result.push(CHAR_SPACE);
574                position = after_if;
575            }
576            continue;
577        }
578        if is_else_keyword(&chars, position, len) {
579            if !result.ends_with(CHAR_SPACE)
580                && !result.ends_with(CHAR_NEWLINE)
581                && !result.ends_with(CHAR_TAB)
582            {
583                result.push(CHAR_SPACE);
584            }
585            result.push_str(KEYWORD_ELSE);
586            position += 4;
587            position = skip_spaces_on_same_line(&chars, position, len);
588            if is_if_keyword(&chars, position, len) {
589                result.push(CHAR_SPACE);
590                continue;
591            }
592            if position < len && chars[position] == CHAR_BRACE_LEFT {
593                result.push(CHAR_SPACE);
594            }
595            continue;
596        }
597        if is_match_keyword(&chars, position, len) {
598            let after_match_colon: usize = skip_spaces_on_same_line(&chars, position + 5, len);
599            if after_match_colon < len
600                && chars[after_match_colon] == CHAR_COLON
601                && (after_match_colon + 1 >= len || chars[after_match_colon + 1] != CHAR_COLON)
602            {
603                result.push_str(KEYWORD_MATCH);
604                position += 5;
605                if position < len && chars[position] == CHAR_SPACE {
606                    result.push(CHAR_SPACE);
607                    position += 1;
608                }
609                continue;
610            }
611            result.push_str(KEYWORD_MATCH);
612            position += 5;
613            let after_match: usize = skip_spaces_on_same_line(&chars, position, len);
614            if after_match < len && chars[after_match] == CHAR_BRACE_LEFT {
615                result.push(CHAR_SPACE);
616                let (block, end) = extract_brace_block(&chars, after_match);
617                result.push_str(&format_brace_block(&block));
618                position = end;
619                position = skip_spaces_on_same_line(&chars, position, len);
620                if position < len && chars[position] == CHAR_BRACE_LEFT {
621                    result.push(CHAR_SPACE);
622                }
623            } else {
624                result.push(CHAR_SPACE);
625                position = after_match;
626            }
627            continue;
628        }
629        if is_for_keyword(&chars, position, len) {
630            let after_for: usize = skip_spaces_on_same_line(&chars, position + 3, len);
631            if after_for < len
632                && chars[after_for] == CHAR_COLON
633                && (after_for + 1 >= len || chars[after_for + 1] != CHAR_COLON)
634            {
635                result.push_str(KEYWORD_FOR);
636                position += 3;
637                if position < len && chars[position] == CHAR_SPACE {
638                    result.push(CHAR_SPACE);
639                    position += 1;
640                }
641                continue;
642            }
643            result.push_str(KEYWORD_FOR);
644            position += 3;
645            position = skip_spaces_on_same_line(&chars, position, len);
646            if position < len && !is_in_keyword(&chars, position, len) {
647                result.push(CHAR_SPACE);
648            }
649            while position < len && !is_in_keyword(&chars, position, len) {
650                if chars[position] == CHAR_BRACE_LEFT {
651                    let (block, end) = extract_brace_block(&chars, position);
652                    result.push_str(&format_brace_block(&block));
653                    position = end;
654                    continue;
655                }
656                if chars[position] == CHAR_DOUBLE_QUOTE || chars[position] == CHAR_SINGLE_QUOTE {
657                    let (literal, end) = extract_string_literal(&chars, position, len);
658                    result.push_str(&literal);
659                    position = end;
660                    continue;
661                }
662                result.push(chars[position]);
663                position += 1;
664            }
665            if result.ends_with(CHAR_SPACE) {
666                result.truncate(result.len() - 1);
667            }
668            position = skip_spaces_on_same_line(&chars, position, len);
669            if is_in_keyword(&chars, position, len) {
670                result.push(CHAR_SPACE);
671                result.push_str(KEYWORD_IN);
672                position += 2;
673                position = skip_spaces_on_same_line(&chars, position, len);
674                if position < len && chars[position] == CHAR_BRACE_LEFT {
675                    result.push(CHAR_SPACE);
676                    let (block, end) = extract_brace_block(&chars, position);
677                    result.push_str(&format_brace_block(&block));
678                    position = end;
679                    position = skip_spaces_on_same_line(&chars, position, len);
680                    if position < len && chars[position] == CHAR_BRACE_LEFT {
681                        result.push(CHAR_SPACE);
682                    }
683                } else {
684                    result.push(CHAR_SPACE);
685                    while position < len && chars[position] != CHAR_BRACE_LEFT {
686                        result.push(chars[position]);
687                        position += 1;
688                    }
689                    if result.ends_with(CHAR_SPACE) {
690                        result.truncate(result.len() - 1);
691                    }
692                    if position < len && chars[position] == CHAR_BRACE_LEFT {
693                        result.push(CHAR_SPACE);
694                    }
695                }
696            }
697            continue;
698        }
699        if chars[position] == CHAR_COLON && position + 1 < len && chars[position + 1] != CHAR_COLON
700        {
701            if result.ends_with(CHAR_COLON) {
702                result.push(CHAR_COLON);
703                position += 1;
704                continue;
705            }
706            let colon_prefix: String = find_ident_before(&result);
707            if is_raw_ident_before(&result, &colon_prefix) {
708                result.push(CHAR_COLON);
709                position += 1;
710                continue;
711            }
712            if !colon_prefix.is_empty() {
713                let before_colon: String = remove_trailing_spaces(&result, colon_prefix.len());
714                result = before_colon;
715                result.push_str(&colon_prefix);
716            }
717            result.push(CHAR_COLON);
718            position += 1;
719            while position < len && (chars[position] == CHAR_SPACE || chars[position] == CHAR_TAB) {
720                position += 1;
721            }
722            if position < len
723                && chars[position] != CHAR_NEWLINE
724                && chars[position] != CHAR_CARRIAGE_RETURN
725                && !is_pseudo_selector_after_colon(&chars, position, len)
726            {
727                result.push(CHAR_SPACE);
728            }
729            continue;
730        }
731        if position + 1 < len
732            && chars[position] == CHAR_EQUALS
733            && chars[position + 1] == CHAR_GREATER_THAN
734        {
735            let trailing: String = find_trailing_spaces(&result);
736            if !trailing.is_empty() {
737                result.truncate(result.len() - trailing.len());
738            }
739            result.push(CHAR_SPACE);
740            result.push_str(ARROW_FAT);
741            position += 2;
742            while position < len && (chars[position] == CHAR_SPACE || chars[position] == CHAR_TAB) {
743                position += 1;
744            }
745            result.push(CHAR_SPACE);
746            continue;
747        }
748        if chars[position] == CHAR_BRACE_LEFT {
749            let (inner, end) = extract_brace_content(&chars, position);
750            let trimmed_inner: &str = inner.trim();
751            if trimmed_inner.is_empty() {
752                result.push(CHAR_BRACE_LEFT);
753                result.push(CHAR_BRACE_RIGHT);
754            } else {
755                let formatted_inner: String = format_macro_body_raw(trimmed_inner);
756                result.push(CHAR_BRACE_LEFT);
757                result.push(CHAR_NEWLINE);
758                result.push_str(&formatted_inner);
759                result.push(CHAR_NEWLINE);
760                result.push(CHAR_BRACE_RIGHT);
761            }
762            position = end;
763            continue;
764        }
765        if is_ident_char(chars[position]) {
766            let start: usize = position;
767            while position < len && is_ident_char(chars[position]) {
768                position += 1;
769            }
770            let ident: String = chars[start..position].iter().collect();
771            result.push_str(&ident);
772            let ws_start: usize = position;
773            while position < len && (chars[position] == CHAR_SPACE || chars[position] == CHAR_TAB) {
774                position += 1;
775            }
776            let had_whitespace: bool = position > ws_start;
777            if position < len && chars[position] == CHAR_BRACE_LEFT {
778                result.push(CHAR_SPACE);
779            } else if had_whitespace {
780                let next_pos: usize = position;
781                if next_pos < len && is_ident_char(chars[next_pos]) {
782                    let mut ident_end: usize = next_pos;
783                    while ident_end < len && is_ident_char(chars[ident_end]) {
784                        ident_end += 1;
785                    }
786                    let after_ident: usize = skip_spaces_on_same_line(&chars, ident_end, len);
787                    if after_ident < len
788                        && chars[after_ident] == CHAR_COLON
789                        && (after_ident + 1 >= len || chars[after_ident + 1] != CHAR_COLON)
790                    {
791                        result.push(CHAR_NEWLINE);
792                        continue;
793                    }
794                }
795                let ws: String = chars[ws_start..position].iter().collect();
796                result.push_str(&ws);
797            }
798            continue;
799        }
800        result.push(chars[position]);
801        position += 1;
802    }
803    result
804}
805
806/// Adds 4-space indentation to each line based on brace depth.
807///
808/// String literals and comments are skipped so braces inside them do not affect depth.
809/// The first non-empty line receives a base indent of 4 spaces (depth 1).
810///
811/// # Arguments
812///
813/// - `&str` - The raw formatted macro body text.
814///
815/// # Returns
816///
817/// - `String` - The indented macro body text.
818fn add_indentation(body: &str) -> String {
819    let chars: Vec<char> = body.chars().collect();
820    let len: usize = chars.len();
821    let mut result: String = String::new();
822    let mut depth: i32 = 0;
823    let mut index: usize = 0;
824    while index < len {
825        if chars[index] == CHAR_DOUBLE_QUOTE || chars[index] == CHAR_SINGLE_QUOTE {
826            let quote: char = chars[index];
827            result.push(chars[index]);
828            index += 1;
829            while index < len {
830                if chars[index] == CHAR_SLASH_BACK && index + 1 < len {
831                    result.push(chars[index]);
832                    result.push(chars[index + 1]);
833                    index += 2;
834                    continue;
835                }
836                result.push(chars[index]);
837                if chars[index] == quote {
838                    index += 1;
839                    break;
840                }
841                index += 1;
842            }
843            continue;
844        }
845        if index + 1 < len
846            && chars[index] == CHAR_SLASH_FORWARD
847            && chars[index + 1] == CHAR_SLASH_FORWARD
848        {
849            while index < len && chars[index] != CHAR_NEWLINE {
850                result.push(chars[index]);
851                index += 1;
852            }
853            continue;
854        }
855        if index + 1 < len
856            && chars[index] == CHAR_SLASH_FORWARD
857            && chars[index + 1] == CHAR_ASTERISK
858        {
859            result.push(chars[index]);
860            result.push(chars[index + 1]);
861            index += 2;
862            while index + 1 < len {
863                if chars[index] == CHAR_ASTERISK && chars[index + 1] == CHAR_SLASH_FORWARD {
864                    result.push(chars[index]);
865                    result.push(chars[index + 1]);
866                    index += 2;
867                    break;
868                }
869                result.push(chars[index]);
870                index += 1;
871            }
872            continue;
873        }
874        if chars[index] == CHAR_NEWLINE {
875            result.push(CHAR_NEWLINE);
876            index += 1;
877            while index < len && (chars[index] == CHAR_SPACE || chars[index] == CHAR_TAB) {
878                index += 1;
879            }
880            if index >= len || chars[index] == CHAR_NEWLINE || chars[index] == CHAR_CARRIAGE_RETURN
881            {
882                continue;
883            }
884            let mut closing_count: i32 = 0;
885            let mut peek: usize = index;
886            while peek < len && chars[peek] == CHAR_BRACE_RIGHT {
887                closing_count += 1;
888                peek += 1;
889            }
890            let indent_depth: i32 = (depth - closing_count).max(0);
891            for _ in 0..indent_depth * 4 {
892                result.push(CHAR_SPACE);
893            }
894            continue;
895        }
896        if chars[index] == CHAR_BRACE_LEFT {
897            depth += 1;
898            result.push(CHAR_BRACE_LEFT);
899            index += 1;
900            continue;
901        }
902        if chars[index] == CHAR_BRACE_RIGHT {
903            depth -= 1;
904            result.push(CHAR_BRACE_RIGHT);
905            index += 1;
906            let mut peek: usize = index;
907            while peek < len && (chars[peek] == CHAR_SPACE || chars[peek] == CHAR_TAB) {
908                peek += 1;
909            }
910            if peek < len
911                && chars[peek] != CHAR_BRACE_RIGHT
912                && chars[peek] != CHAR_BRACE_LEFT
913                && chars[peek] != CHAR_NEWLINE
914                && chars[peek] != CHAR_CARRIAGE_RETURN
915                && chars[peek] != CHAR_RIGHT_PAREN
916                && chars[peek] != CHAR_COMMA
917                && chars[peek] != CHAR_SEMICOLON
918            {
919                let next_chars: String = chars[peek..(peek + 4).min(len)].iter().collect();
920                if next_chars != "else" {
921                    result.push(CHAR_NEWLINE);
922                    let indent_depth: i32 = depth.max(0);
923                    for _ in 0..indent_depth * 4 {
924                        result.push(CHAR_SPACE);
925                    }
926                    index = peek;
927                }
928            }
929            continue;
930        }
931        result.push(chars[index]);
932        index += 1;
933    }
934    result
935}
936
937/// Checks if the current position is the start of the `if` keyword.
938///
939/// # Arguments
940///
941/// - `&[char]` - The source character slice.
942/// - `usize` - The current position.
943/// - `usize` - The total length.
944///
945/// # Returns
946///
947/// - `bool` - Whether `if` starts at the given position.
948fn is_if_keyword(chars: &[char], pos: usize, len: usize) -> bool {
949    if pos + 2 > len {
950        return false;
951    }
952    chars[pos] == CHAR_LETTER_I
953        && chars[pos + 1] == CHAR_LETTER_F
954        && (pos + 2 >= len || !is_ident_char(chars[pos + 2]))
955        && (pos == 0 || !is_ident_char(chars[pos - 1]))
956        && !is_raw_prefix(chars, pos)
957}
958
959/// Checks if the current position is the start of the `else` keyword.
960///
961/// # Arguments
962///
963/// - `&[char]` - The source character slice.
964/// - `usize` - The current position.
965/// - `usize` - The total length.
966///
967/// # Returns
968///
969/// - `bool` - Whether `else` starts at the given position.
970fn is_else_keyword(chars: &[char], pos: usize, len: usize) -> bool {
971    if pos + 4 > len {
972        return false;
973    }
974    chars[pos] == CHAR_LETTER_E
975        && chars[pos + 1] == CHAR_LETTER_L
976        && chars[pos + 2] == CHAR_LETTER_S
977        && chars[pos + 3] == CHAR_LETTER_E
978        && (pos + 4 >= len || !is_ident_char(chars[pos + 4]))
979        && (pos == 0 || !is_ident_char(chars[pos - 1]))
980        && !is_raw_prefix(chars, pos)
981}
982
983/// Checks if the current position is the start of the `match` keyword.
984///
985/// # Arguments
986///
987/// - `&[char]` - The source character slice.
988/// - `usize` - The current position.
989/// - `usize` - The total length.
990///
991/// # Returns
992///
993/// - `bool` - Whether `match` starts at the given position.
994fn is_match_keyword(chars: &[char], pos: usize, len: usize) -> bool {
995    if pos + 5 > len {
996        return false;
997    }
998    chars[pos] == CHAR_LETTER_M
999        && chars[pos + 1] == CHAR_LETTER_A
1000        && chars[pos + 2] == CHAR_LETTER_T
1001        && chars[pos + 3] == CHAR_LETTER_C
1002        && chars[pos + 4] == CHAR_LETTER_H
1003        && (pos + 5 >= len || !is_ident_char(chars[pos + 5]))
1004        && (pos == 0 || !is_ident_char(chars[pos - 1]))
1005        && !is_raw_prefix(chars, pos)
1006}
1007
1008/// Checks if the current position is the start of the `for` keyword.
1009///
1010/// # Arguments
1011///
1012/// - `&[char]` - The source character slice.
1013/// - `usize` - The current position.
1014/// - `usize` - The total length.
1015///
1016/// # Returns
1017///
1018/// - `bool` - Whether `for` starts at the given position.
1019fn is_for_keyword(chars: &[char], pos: usize, len: usize) -> bool {
1020    if pos + 3 > len {
1021        return false;
1022    }
1023    chars[pos] == CHAR_LETTER_F
1024        && chars[pos + 1] == CHAR_LETTER_O
1025        && chars[pos + 2] == CHAR_LETTER_R
1026        && (pos + 3 >= len || !is_ident_char(chars[pos + 3]))
1027        && (pos == 0 || !is_ident_char(chars[pos - 1]))
1028        && !is_raw_prefix(chars, pos)
1029}
1030
1031/// Checks if the current position is the start of the `in` keyword.
1032///
1033/// # Arguments
1034///
1035/// - `&[char]` - The source character slice.
1036/// - `usize` - The current position.
1037/// - `usize` - The total length.
1038///
1039/// # Returns
1040///
1041/// - `bool` - Whether `in` starts at the given position.
1042fn is_in_keyword(chars: &[char], pos: usize, len: usize) -> bool {
1043    if pos + 2 > len {
1044        return false;
1045    }
1046    chars[pos] == CHAR_LETTER_I
1047        && chars[pos + 1] == CHAR_LETTER_N
1048        && (pos + 2 >= len || !is_ident_char(chars[pos + 2]))
1049        && (pos == 0 || !is_ident_char(chars[pos - 1]))
1050        && !is_raw_prefix(chars, pos)
1051}
1052
1053/// Formats a brace block by adding spaces inside single-line braces.
1054///
1055/// For blocks that do not contain newlines, trims inner content and adds
1056/// single spaces around it: `{ content }`. Empty blocks remain `{}`.
1057/// For blocks containing newlines, returns the block unchanged.
1058///
1059/// # Arguments
1060///
1061/// - `&str` - The brace block text (including outer `{` and `}`).
1062///
1063/// # Returns
1064///
1065/// - `String` - The formatted brace block text.
1066fn format_brace_block(block: &str) -> String {
1067    let block_chars: Vec<char> = block.chars().collect();
1068    if block_chars.len() < 2
1069        || block_chars[0] != CHAR_BRACE_LEFT
1070        || block_chars.last() != Some(&CHAR_BRACE_RIGHT)
1071    {
1072        return block.to_string();
1073    }
1074    let inner: String = block_chars[1..block_chars.len() - 1].iter().collect();
1075    if inner.contains(CHAR_NEWLINE) {
1076        return block.to_string();
1077    }
1078    let trimmed_inner: &str = inner.trim();
1079    if trimmed_inner.is_empty() {
1080        let mut empty_result: String = String::new();
1081        empty_result.push(CHAR_BRACE_LEFT);
1082        empty_result.push(CHAR_BRACE_RIGHT);
1083        return empty_result;
1084    }
1085    let mut formatted: String = String::new();
1086    formatted.push(CHAR_BRACE_LEFT);
1087    formatted.push(CHAR_SPACE);
1088    formatted.push_str(trimmed_inner);
1089    formatted.push(CHAR_SPACE);
1090    formatted.push(CHAR_BRACE_RIGHT);
1091    formatted
1092}
1093
1094/// Skips spaces (but not newlines) on the same line.
1095///
1096/// # Arguments
1097///
1098/// - `&[char]` - The source character slice.
1099/// - `usize` - The current position.
1100/// - `usize` - The total length.
1101///
1102/// # Returns
1103///
1104/// - `usize` - The position after skipping same-line spaces.
1105fn skip_spaces_on_same_line(chars: &[char], mut pos: usize, len: usize) -> usize {
1106    while pos < len && (chars[pos] == CHAR_SPACE || chars[pos] == CHAR_TAB) {
1107        pos += 1;
1108    }
1109    pos
1110}
1111
1112/// Checks whether the content after a colon is a CSS pseudo-class or pseudo-element selector.
1113///
1114/// A pseudo-class/pseudo-element selector is identified by looking ahead:
1115/// after the colon, if an identifier is found, and that identifier
1116/// (possibly followed by more hyphen-separated identifiers like `focus-visible`)
1117/// is immediately followed by `{` (not `!` which indicates a macro call like `format!`),
1118/// then this is a selector colon and should not have a space after it.
1119///
1120/// Also handles functional pseudo-classes like `:nth-child(2n+1)`, `:not(.class)`,
1121/// where the identifier is followed by a parenthesized argument list before `{`.
1122///
1123/// Returns `false` if the first identifier after the colon is a Rust keyword
1124/// (e.g., `if`, `match`, `for`) since those are attribute value expressions,
1125/// not CSS selectors.
1126///
1127/// # Arguments
1128///
1129/// - `&[char]` - The source character slice.
1130/// - `usize` - The current position (right after the colon, spaces already skipped).
1131/// - `usize` - The total length of the source.
1132///
1133/// # Returns
1134///
1135/// - `bool` - Whether the content after the colon is a CSS selector.
1136fn is_pseudo_selector_after_colon(chars: &[char], mut pos: usize, len: usize) -> bool {
1137    if pos >= len || !is_ident_char(chars[pos]) {
1138        return false;
1139    }
1140    let ident_start: usize = pos;
1141    while pos < len && is_ident_char(chars[pos]) {
1142        pos += 1;
1143    }
1144    let first_ident: String = chars[ident_start..pos].iter().collect();
1145    if is_rust_keyword(&first_ident) {
1146        return false;
1147    }
1148    while pos < len && chars[pos] == CHAR_HYPHEN && pos + 1 < len && is_ident_char(chars[pos + 1]) {
1149        pos += 1;
1150        while pos < len && is_ident_char(chars[pos]) {
1151            pos += 1;
1152        }
1153    }
1154    let after_ident: usize = skip_spaces_on_same_line(chars, pos, len);
1155    if after_ident < len && chars[after_ident] == CHAR_BRACE_LEFT {
1156        return true;
1157    }
1158    if after_ident < len && chars[after_ident] == CHAR_LEFT_PAREN {
1159        let mut depth: i32 = 0;
1160        let mut paren_pos: usize = after_ident;
1161        while paren_pos < len {
1162            if chars[paren_pos] == CHAR_LEFT_PAREN {
1163                depth += 1;
1164            } else if chars[paren_pos] == CHAR_RIGHT_PAREN {
1165                depth -= 1;
1166                if depth == 0 {
1167                    let after_paren: usize = skip_spaces_on_same_line(chars, paren_pos + 1, len);
1168                    return after_paren < len && chars[after_paren] == CHAR_BRACE_LEFT;
1169                }
1170            }
1171            paren_pos += 1;
1172        }
1173    }
1174    false
1175}
1176
1177/// Checks whether a string is a Rust keyword that can appear after a colon
1178/// in euv macro attribute syntax (e.g., `class: if { ... }`).
1179///
1180/// These keywords indicate attribute value expressions, not CSS selectors.
1181///
1182/// # Arguments
1183///
1184/// - `&str` - The identifier string to check.
1185///
1186/// # Returns
1187///
1188/// - `bool` - Whether the string is a Rust keyword.
1189fn is_rust_keyword(ident: &str) -> bool {
1190    matches!(ident, KEYWORD_IF | KEYWORD_MATCH | KEYWORD_FOR)
1191}
1192
1193/// Checks whether the identifier found before the colon is a Rust raw identifier (r#prefix).
1194///
1195/// Raw identifiers use the `r#` prefix (e.g., `r#for`), and their colons
1196/// should not be formatted as attribute separators.
1197///
1198/// # Arguments
1199///
1200/// - `&str` - The result string built so far.
1201/// - `&str` - The identifier found before the colon.
1202///
1203/// # Returns
1204///
1205/// - `bool` - Whether the identifier is preceded by `r#`.
1206fn is_raw_ident_before(result: &str, ident: &str) -> bool {
1207    result
1208        .trim_end()
1209        .ends_with(&format!("{RAW_IDENT_PREFIX}{ident}"))
1210}
1211
1212/// Finds the identifier immediately before the current position in the result string.
1213///
1214/// # Arguments
1215///
1216/// - `&str` - The result string built so far.
1217///
1218/// # Returns
1219///
1220/// - `String` - The identifier found before the current position, or empty if none.
1221fn find_ident_before(result: &str) -> String {
1222    let chars: Vec<char> = result.chars().collect();
1223    let mut end: usize = chars.len();
1224    while end > 0 && chars[end - 1] == CHAR_SPACE {
1225        end -= 1;
1226    }
1227    let mut start: usize = end;
1228    while start > 0 && is_ident_char(chars[start - 1]) {
1229        start -= 1;
1230    }
1231    if start < end {
1232        chars[start..end].iter().collect()
1233    } else {
1234        String::new()
1235    }
1236}
1237
1238/// Removes trailing spaces and the identified prefix from the result string.
1239///
1240/// # Arguments
1241///
1242/// - `&str` - The result string built so far.
1243/// - `usize` - The length of the prefix to remove.
1244///
1245/// # Returns
1246///
1247/// - `String` - The string with trailing spaces and prefix removed.
1248fn remove_trailing_spaces(result: &str, prefix_len: usize) -> String {
1249    let chars: Vec<char> = result.chars().collect();
1250    let total_len: usize = chars.len();
1251    let mut end: usize = total_len;
1252    while end > 0 && chars[end - 1] == CHAR_SPACE {
1253        end -= 1;
1254    }
1255    if prefix_len > end {
1256        return result.to_string();
1257    }
1258    let new_end: usize = end - prefix_len;
1259    chars[..new_end].iter().collect()
1260}
1261
1262/// Finds trailing spaces in the result string.
1263///
1264/// # Arguments
1265///
1266/// - `&str` - The result string.
1267///
1268/// # Returns
1269///
1270/// - `String` - The trailing spaces.
1271fn find_trailing_spaces(result: &str) -> String {
1272    let mut spaces: String = String::new();
1273    for ch in result.chars().rev() {
1274        if ch == CHAR_SPACE || ch == CHAR_TAB {
1275            spaces.push(ch);
1276        } else {
1277            break;
1278        }
1279    }
1280    spaces.chars().rev().collect()
1281}
1282
1283/// Formats all Rust source files in the given directory that contain euv macros.
1284///
1285/// Recursively walks the directory tree, finds `.rs` files, and formats
1286/// any euv macro invocations found within them.
1287///
1288/// # Arguments
1289///
1290/// - `&Path` - The root directory to search.
1291/// - `FmtMode` - Whether to check or write formatting.
1292///
1293/// # Returns
1294///
1295/// - `Result<(), EuvError>` - Indicates success or failure.
1296pub async fn format_dir(path: &Path, mode: FmtMode) -> Result<(), EuvError> {
1297    if path.is_file() {
1298        let changed: bool = format_file(path, &mode).await?;
1299        match mode {
1300            FmtMode::Check => {
1301                if changed {
1302                    return Err(EuvError::Message(format!(
1303                        "{} needs formatting.",
1304                        path.display()
1305                    )));
1306                }
1307                log::info!("{} is properly formatted.", path.display());
1308            }
1309            FmtMode::Write => {
1310                if changed {
1311                    log::info!("Formatted: {}", path.display());
1312                } else {
1313                    log::info!("Already formatted: {}", path.display());
1314                }
1315            }
1316        }
1317        return Ok(());
1318    }
1319    let mut entries: Vec<PathBuf> = collect_rs_files(path).await?;
1320    entries.sort();
1321    let mut changed_count: usize = 0;
1322    let mut unchanged_count: usize = 0;
1323    for entry in entries {
1324        match format_file(&entry, &mode).await {
1325            Ok(changed) => {
1326                if changed {
1327                    changed_count += 1;
1328                } else {
1329                    unchanged_count += 1;
1330                }
1331            }
1332            Err(error) => {
1333                log::warn!("Failed to format {}: {error}", entry.display());
1334            }
1335        }
1336    }
1337    match mode {
1338        FmtMode::Check => {
1339            if changed_count > 0 {
1340                return Err(EuvError::Message(format!(
1341                    "{} file(s) need formatting. Run `euv fmt` to fix.",
1342                    changed_count
1343                )));
1344            }
1345            log::info!("All {} file(s) are properly formatted.", unchanged_count);
1346        }
1347        FmtMode::Write => {
1348            log::info!(
1349                "Formatted {} file(s), {} unchanged.",
1350                changed_count,
1351                unchanged_count
1352            );
1353        }
1354    }
1355    Ok(())
1356}
1357
1358/// Collects all `.rs` files in a directory recursively.
1359///
1360/// # Arguments
1361///
1362/// - `&Path` - The directory to search.
1363///
1364/// # Returns
1365///
1366/// - `Result<Vec<PathBuf>, EuvError>` - The list of `.rs` file paths.
1367async fn collect_rs_files(path: &Path) -> Result<Vec<PathBuf>, EuvError> {
1368    let mut result: Vec<PathBuf> = Vec::new();
1369    let mut stack: Vec<PathBuf> = vec![path.to_path_buf()];
1370    while let Some(dir) = stack.pop() {
1371        let mut entries: ReadDir =
1372            read_dir(&dir)
1373                .await
1374                .map_err(|error: io::Error| EuvError::IoPath {
1375                    message: String::from("Failed to read directory"),
1376                    path: dir.clone(),
1377                    error,
1378                })?;
1379        while let Some(entry) =
1380            entries
1381                .next_entry()
1382                .await
1383                .map_err(|error: io::Error| EuvError::IoPath {
1384                    message: String::from("Failed to read entry in directory"),
1385                    path: dir.clone(),
1386                    error,
1387                })?
1388        {
1389            let entry_path: PathBuf = entry.path();
1390            if entry_path.is_dir() {
1391                let file_name: String = entry_path
1392                    .file_name()
1393                    .unwrap_or_default()
1394                    .to_string_lossy()
1395                    .to_string();
1396                if file_name != TARGET_DIR_NAME && file_name != NODE_MODULES_DIR_NAME {
1397                    stack.push(entry_path);
1398                }
1399            } else if entry_path
1400                .extension()
1401                .is_some_and(|ext: &OsStr| ext == RS_EXTENSION)
1402            {
1403                result.push(entry_path);
1404            }
1405        }
1406    }
1407    Ok(result)
1408}
1409
1410/// Formats a single Rust source file.
1411///
1412/// # Arguments
1413///
1414/// - `&Path` - The file path.
1415/// - `&FmtMode` - Whether to check or write formatting.
1416///
1417/// # Returns
1418///
1419/// - `Result<bool, EuvError>` - Whether the file was changed.
1420async fn format_file(path: &Path, mode: &FmtMode) -> Result<bool, EuvError> {
1421    let content: String =
1422        read_to_string(path)
1423            .await
1424            .map_err(|error: io::Error| EuvError::IoPath {
1425                message: String::from("Failed to read"),
1426                path: path.to_path_buf(),
1427                error,
1428            })?;
1429    let fmt_result: FmtResult = format_source(&content);
1430    if fmt_result.get_changed() {
1431        match mode {
1432            FmtMode::Write => {
1433                write(path, fmt_result.get_output())
1434                    .await
1435                    .map_err(|error: io::Error| EuvError::IoPath {
1436                        message: String::from("Failed to write"),
1437                        path: path.to_path_buf(),
1438                        error,
1439                    })?;
1440                log::info!("Formatted: {}", path.display());
1441            }
1442            FmtMode::Check => {
1443                log::warn!("Needs formatting: {}", path.display());
1444            }
1445        }
1446    }
1447    Ok(fmt_result.get_changed())
1448}