1use super::*;
2
3pub(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
28pub 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(0);
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(0);
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
151fn 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
183fn 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
202fn 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
220fn is_ident_char(character: char) -> bool {
230 character.is_alphanumeric() || character == CHAR_UNDERSCORE
231}
232
233fn 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
276fn 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
329fn 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
386fn 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
420fn 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
445fn 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
474pub 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
493fn 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
806fn 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
935 result
936}
937
938fn is_if_keyword(chars: &[char], pos: usize, len: usize) -> bool {
950 if pos + 2 > len {
951 return false;
952 }
953 chars[pos] == CHAR_LETTER_I
954 && chars[pos + 1] == CHAR_LETTER_F
955 && (pos + 2 >= len || !is_ident_char(chars[pos + 2]))
956 && (pos == 0 || !is_ident_char(chars[pos - 1]))
957 && !is_raw_prefix(chars, pos)
958}
959
960fn is_else_keyword(chars: &[char], pos: usize, len: usize) -> bool {
972 if pos + 4 > len {
973 return false;
974 }
975 chars[pos] == CHAR_LETTER_E
976 && chars[pos + 1] == CHAR_LETTER_L
977 && chars[pos + 2] == CHAR_LETTER_S
978 && chars[pos + 3] == CHAR_LETTER_E
979 && (pos + 4 >= len || !is_ident_char(chars[pos + 4]))
980 && (pos == 0 || !is_ident_char(chars[pos - 1]))
981 && !is_raw_prefix(chars, pos)
982}
983
984fn is_match_keyword(chars: &[char], pos: usize, len: usize) -> bool {
996 if pos + 5 > len {
997 return false;
998 }
999 chars[pos] == CHAR_LETTER_M
1000 && chars[pos + 1] == CHAR_LETTER_A
1001 && chars[pos + 2] == CHAR_LETTER_T
1002 && chars[pos + 3] == CHAR_LETTER_C
1003 && chars[pos + 4] == CHAR_LETTER_H
1004 && (pos + 5 >= len || !is_ident_char(chars[pos + 5]))
1005 && (pos == 0 || !is_ident_char(chars[pos - 1]))
1006 && !is_raw_prefix(chars, pos)
1007}
1008
1009fn is_for_keyword(chars: &[char], pos: usize, len: usize) -> bool {
1021 if pos + 3 > len {
1022 return false;
1023 }
1024 chars[pos] == CHAR_LETTER_F
1025 && chars[pos + 1] == CHAR_LETTER_O
1026 && chars[pos + 2] == CHAR_LETTER_R
1027 && (pos + 3 >= len || !is_ident_char(chars[pos + 3]))
1028 && (pos == 0 || !is_ident_char(chars[pos - 1]))
1029 && !is_raw_prefix(chars, pos)
1030}
1031
1032fn is_in_keyword(chars: &[char], pos: usize, len: usize) -> bool {
1044 if pos + 2 > len {
1045 return false;
1046 }
1047 chars[pos] == CHAR_LETTER_I
1048 && chars[pos + 1] == CHAR_LETTER_N
1049 && (pos + 2 >= len || !is_ident_char(chars[pos + 2]))
1050 && (pos == 0 || !is_ident_char(chars[pos - 1]))
1051 && !is_raw_prefix(chars, pos)
1052}
1053
1054fn format_brace_block(block: &str) -> String {
1068 let block_chars: Vec<char> = block.chars().collect();
1069 if block_chars.len() < 2
1070 || block_chars[0] != CHAR_BRACE_LEFT
1071 || *block_chars.last().unwrap() != CHAR_BRACE_RIGHT
1072 {
1073 return block.to_string();
1074 }
1075 let inner: String = block_chars[1..block_chars.len() - 1].iter().collect();
1076 if inner.contains(CHAR_NEWLINE) {
1077 return block.to_string();
1078 }
1079 let trimmed_inner: &str = inner.trim();
1080 if trimmed_inner.is_empty() {
1081 let mut empty_result: String = String::new();
1082 empty_result.push(CHAR_BRACE_LEFT);
1083 empty_result.push(CHAR_BRACE_RIGHT);
1084 return empty_result;
1085 }
1086 let mut formatted: String = String::new();
1087 formatted.push(CHAR_BRACE_LEFT);
1088 formatted.push(CHAR_SPACE);
1089 formatted.push_str(trimmed_inner);
1090 formatted.push(CHAR_SPACE);
1091 formatted.push(CHAR_BRACE_RIGHT);
1092 formatted
1093}
1094
1095fn skip_spaces_on_same_line(chars: &[char], mut pos: usize, len: usize) -> usize {
1107 while pos < len && (chars[pos] == CHAR_SPACE || chars[pos] == CHAR_TAB) {
1108 pos += 1;
1109 }
1110 pos
1111}
1112
1113fn is_pseudo_selector_after_colon(chars: &[char], mut pos: usize, len: usize) -> bool {
1138 if pos >= len || !is_ident_char(chars[pos]) {
1139 return false;
1140 }
1141 let ident_start: usize = pos;
1142 while pos < len && is_ident_char(chars[pos]) {
1143 pos += 1;
1144 }
1145 let first_ident: String = chars[ident_start..pos].iter().collect();
1146 if is_rust_keyword(&first_ident) {
1147 return false;
1148 }
1149 while pos < len && chars[pos] == CHAR_HYPHEN && pos + 1 < len && is_ident_char(chars[pos + 1]) {
1150 pos += 1;
1151 while pos < len && is_ident_char(chars[pos]) {
1152 pos += 1;
1153 }
1154 }
1155 let after_ident: usize = skip_spaces_on_same_line(chars, pos, len);
1156 if after_ident < len && chars[after_ident] == CHAR_BRACE_LEFT {
1157 return true;
1158 }
1159 if after_ident < len && chars[after_ident] == CHAR_LEFT_PAREN {
1160 let mut depth: i32 = 0;
1161 let mut paren_pos: usize = after_ident;
1162 while paren_pos < len {
1163 if chars[paren_pos] == CHAR_LEFT_PAREN {
1164 depth += 1;
1165 } else if chars[paren_pos] == CHAR_RIGHT_PAREN {
1166 depth -= 1;
1167 if depth == 0 {
1168 let after_paren: usize = skip_spaces_on_same_line(chars, paren_pos + 1, len);
1169 return after_paren < len && chars[after_paren] == CHAR_BRACE_LEFT;
1170 }
1171 }
1172 paren_pos += 1;
1173 }
1174 }
1175 false
1176}
1177
1178fn is_rust_keyword(ident: &str) -> bool {
1191 matches!(ident, KEYWORD_IF | KEYWORD_MATCH | KEYWORD_FOR)
1192}
1193
1194fn is_raw_ident_before(result: &str, ident: &str) -> bool {
1208 result
1209 .trim_end()
1210 .ends_with(&format!("{RAW_IDENT_PREFIX}{ident}"))
1211}
1212
1213fn find_ident_before(result: &str) -> String {
1223 let chars: Vec<char> = result.chars().collect();
1224 let mut end: usize = chars.len();
1225 while end > 0 && chars[end - 1] == CHAR_SPACE {
1226 end -= 1;
1227 }
1228 let mut start: usize = end;
1229 while start > 0 && is_ident_char(chars[start - 1]) {
1230 start -= 1;
1231 }
1232 if start < end {
1233 chars[start..end].iter().collect()
1234 } else {
1235 String::new()
1236 }
1237}
1238
1239fn remove_trailing_spaces(result: &str, prefix_len: usize) -> String {
1250 let chars: Vec<char> = result.chars().collect();
1251 let total_len: usize = chars.len();
1252 let mut end: usize = total_len;
1253 while end > 0 && chars[end - 1] == CHAR_SPACE {
1254 end -= 1;
1255 }
1256 if prefix_len > end {
1257 return result.to_string();
1258 }
1259 let new_end: usize = end - prefix_len;
1260 chars[..new_end].iter().collect()
1261}
1262
1263fn find_trailing_spaces(result: &str) -> String {
1273 let mut spaces: String = String::new();
1274 for ch in result.chars().rev() {
1275 if ch == CHAR_SPACE || ch == CHAR_TAB {
1276 spaces.push(ch);
1277 } else {
1278 break;
1279 }
1280 }
1281 spaces.chars().rev().collect()
1282}
1283
1284pub async fn format_dir(path: &Path, mode: FmtMode) -> Result<(), EuvError> {
1298 if path.is_file() {
1299 let changed: bool = format_file(path, &mode).await?;
1300 match mode {
1301 FmtMode::Check => {
1302 if changed {
1303 return Err(EuvError::Message(format!(
1304 "{} needs formatting.",
1305 path.display()
1306 )));
1307 }
1308 log::info!("{} is properly formatted.", path.display());
1309 }
1310 FmtMode::Write => {
1311 if changed {
1312 log::info!("Formatted: {}", path.display());
1313 } else {
1314 log::info!("Already formatted: {}", path.display());
1315 }
1316 }
1317 }
1318 return Ok(());
1319 }
1320 let mut entries: Vec<PathBuf> = collect_rs_files(path).await?;
1321 entries.sort();
1322 let mut changed_count: usize = 0;
1323 let mut unchanged_count: usize = 0;
1324 for entry in entries {
1325 match format_file(&entry, &mode).await {
1326 Ok(changed) => {
1327 if changed {
1328 changed_count += 1;
1329 } else {
1330 unchanged_count += 1;
1331 }
1332 }
1333 Err(error) => {
1334 log::warn!("Failed to format {}: {error}", entry.display());
1335 }
1336 }
1337 }
1338 match mode {
1339 FmtMode::Check => {
1340 if changed_count > 0 {
1341 return Err(EuvError::Message(format!(
1342 "{} file(s) need formatting. Run `euv fmt` to fix.",
1343 changed_count
1344 )));
1345 }
1346 log::info!("All {} file(s) are properly formatted.", unchanged_count);
1347 }
1348 FmtMode::Write => {
1349 log::info!(
1350 "Formatted {} file(s), {} unchanged.",
1351 changed_count,
1352 unchanged_count
1353 );
1354 }
1355 }
1356 Ok(())
1357}
1358
1359async fn collect_rs_files(path: &Path) -> Result<Vec<PathBuf>, EuvError> {
1369 let mut result: Vec<PathBuf> = Vec::new();
1370 let mut stack: Vec<PathBuf> = vec![path.to_path_buf()];
1371 while let Some(dir) = stack.pop() {
1372 let mut entries: ReadDir =
1373 read_dir(&dir)
1374 .await
1375 .map_err(|error: io::Error| EuvError::IoPath {
1376 message: String::from("Failed to read directory"),
1377 path: dir.clone(),
1378 error,
1379 })?;
1380 while let Some(entry) =
1381 entries
1382 .next_entry()
1383 .await
1384 .map_err(|error: io::Error| EuvError::IoPath {
1385 message: String::from("Failed to read entry in directory"),
1386 path: dir.clone(),
1387 error,
1388 })?
1389 {
1390 let entry_path: PathBuf = entry.path();
1391 if entry_path.is_dir() {
1392 let file_name: String = entry_path
1393 .file_name()
1394 .unwrap_or_default()
1395 .to_string_lossy()
1396 .to_string();
1397 if file_name != TARGET_DIR_NAME && file_name != NODE_MODULES_DIR_NAME {
1398 stack.push(entry_path);
1399 }
1400 } else if entry_path
1401 .extension()
1402 .is_some_and(|ext: &std::ffi::OsStr| ext == RS_EXTENSION)
1403 {
1404 result.push(entry_path);
1405 }
1406 }
1407 }
1408 Ok(result)
1409}
1410
1411async fn format_file(path: &Path, mode: &FmtMode) -> Result<bool, EuvError> {
1422 let content: String =
1423 read_to_string(path)
1424 .await
1425 .map_err(|error: io::Error| EuvError::IoPath {
1426 message: String::from("Failed to read"),
1427 path: path.to_path_buf(),
1428 error,
1429 })?;
1430 let fmt_result: FmtResult = format_source(&content);
1431 if fmt_result.get_changed() {
1432 match mode {
1433 FmtMode::Write => {
1434 write(path, fmt_result.get_output())
1435 .await
1436 .map_err(|error: io::Error| EuvError::IoPath {
1437 message: String::from("Failed to write"),
1438 path: path.to_path_buf(),
1439 error,
1440 })?;
1441 log::info!("Formatted: {}", path.display());
1442 }
1443 FmtMode::Check => {
1444 log::warn!("Needs formatting: {}", path.display());
1445 }
1446 }
1447 }
1448 Ok(fmt_result.get_changed())
1449}