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_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 =
97 indented_body_skipping_block_comments(trimmed_body, &indent_str);
98 let outer_indent_str: String = " ".repeat(macro_indent);
99 result.push(CHAR_BRACE_LEFT);
100 result.push(CHAR_NEWLINE);
101 result.push_str(&indented_body);
102 result.push(CHAR_NEWLINE);
103 result.push_str(&outer_indent_str);
104 result.push(CHAR_BRACE_RIGHT);
105 }
106 position = end_pos;
107 continue;
108 }
109 }
110 continue;
111 }
112 if chars[position] == CHAR_DOUBLE_QUOTE || chars[position] == CHAR_SINGLE_QUOTE {
113 let (literal, end_pos) = extract_string_literal(&chars, position, len);
114 result.push_str(&literal);
115 position = end_pos;
116 continue;
117 }
118 if position + 1 < len
119 && chars[position] == CHAR_SLASH_FORWARD
120 && chars[position + 1] == CHAR_SLASH_FORWARD
121 {
122 let (comment, end_pos) = extract_line_comment(&chars, position, len);
123 result.push_str(&comment);
124 position = end_pos;
125 continue;
126 }
127 if position + 1 < len
128 && chars[position] == CHAR_SLASH_FORWARD
129 && chars[position + 1] == CHAR_ASTERISK
130 {
131 let (comment, end_pos) = extract_block_comment(&chars, position, len);
132 result.push_str(&comment);
133 position = end_pos;
134 continue;
135 }
136 result.push(chars[position]);
137 position += 1;
138 }
139 result
140}
141
142fn is_euv_macro_start(chars: &[char], pos: usize, len: usize) -> bool {
154 for name in EUV_MACRO_NAMES {
155 let name_len: usize = name.len();
156 if pos + name_len > len {
157 continue;
158 }
159 let candidate: String = chars[pos..pos + name_len].iter().collect::<String>();
160 if candidate != *name {
161 continue;
162 }
163 if pos > 0 && is_ident_char(chars[pos - 1]) {
164 continue;
165 }
166 if pos + name_len < len && is_ident_char(chars[pos + name_len]) {
167 continue;
168 }
169 return true;
170 }
171 false
172}
173
174fn find_macro_name_end(chars: &[char], pos: usize, _len: usize) -> usize {
186 let mut end: usize = pos;
187 while end < chars.len() && is_ident_char(chars[end]) {
188 end += 1;
189 }
190 end
191}
192
193fn is_raw_prefix(chars: &[char], pos: usize) -> bool {
205 if pos < 2 {
206 return false;
207 }
208 chars[pos - 2] == CHAR_LETTER_R && chars[pos - 1] == CHAR_HASH
209}
210
211fn is_ident_char(character: char) -> bool {
221 character.is_alphanumeric() || character == CHAR_UNDERSCORE
222}
223
224fn skip_whitespace_and_comments(
237 chars: &[char],
238 mut pos: usize,
239 len: usize,
240 result: &mut String,
241) -> usize {
242 while pos < len {
243 if chars[pos].is_whitespace() {
244 result.push(chars[pos]);
245 pos += 1;
246 } else if pos + 1 < len
247 && chars[pos] == CHAR_SLASH_FORWARD
248 && chars[pos + 1] == CHAR_SLASH_FORWARD
249 {
250 let (comment, end_pos) = extract_line_comment(chars, pos, len);
251 result.push_str(&comment);
252 pos = end_pos;
253 } else if pos + 1 < len
254 && chars[pos] == CHAR_SLASH_FORWARD
255 && chars[pos + 1] == CHAR_ASTERISK
256 {
257 let (comment, end_pos) = extract_block_comment(chars, pos, len);
258 result.push_str(&comment);
259 pos = end_pos;
260 } else {
261 break;
262 }
263 }
264 pos
265}
266
267fn extract_brace_block(chars: &[char], start: usize) -> (String, usize) {
281 let mut depth: i32 = 0;
282 let mut position: usize = start;
283 while position < chars.len() {
284 if chars[position] == CHAR_DOUBLE_QUOTE || chars[position] == CHAR_SINGLE_QUOTE {
285 let (_, end) = extract_string_literal(chars, position, chars.len());
286 position = end;
287 continue;
288 }
289 if position + 1 < chars.len()
290 && chars[position] == CHAR_SLASH_FORWARD
291 && chars[position + 1] == CHAR_SLASH_FORWARD
292 {
293 let (_, end) = extract_line_comment(chars, position, chars.len());
294 position = end;
295 continue;
296 }
297 if position + 1 < chars.len()
298 && chars[position] == CHAR_SLASH_FORWARD
299 && chars[position + 1] == CHAR_ASTERISK
300 {
301 let (_, end) = extract_block_comment(chars, position, chars.len());
302 position = end;
303 continue;
304 }
305 if chars[position] == CHAR_BRACE_LEFT {
306 depth += 1;
307 } else if chars[position] == CHAR_BRACE_RIGHT {
308 depth -= 1;
309 if depth == 0 {
310 let content: String = chars[start..=position].iter().collect();
311 return (content, position + 1);
312 }
313 }
314 position += 1;
315 }
316 let content: String = chars[start..].iter().collect();
317 (content, chars.len())
318}
319
320fn extract_brace_content(chars: &[char], start: usize) -> (String, usize) {
334 let mut depth: i32 = 0;
335 let mut position: usize = start;
336 let mut content_start: usize = start + 1;
337 while position < chars.len() {
338 if chars[position] == CHAR_DOUBLE_QUOTE || chars[position] == CHAR_SINGLE_QUOTE {
339 let (_, end) = extract_string_literal(chars, position, chars.len());
340 position = end;
341 continue;
342 }
343 if position + 1 < chars.len()
344 && chars[position] == CHAR_SLASH_FORWARD
345 && chars[position + 1] == CHAR_SLASH_FORWARD
346 {
347 let (_, end) = extract_line_comment(chars, position, chars.len());
348 position = end;
349 continue;
350 }
351 if position + 1 < chars.len()
352 && chars[position] == CHAR_SLASH_FORWARD
353 && chars[position + 1] == CHAR_ASTERISK
354 {
355 let (_, end) = extract_block_comment(chars, position, chars.len());
356 position = end;
357 continue;
358 }
359 if chars[position] == CHAR_BRACE_LEFT {
360 if depth == 0 {
361 content_start = position + 1;
362 }
363 depth += 1;
364 } else if chars[position] == CHAR_BRACE_RIGHT {
365 depth -= 1;
366 if depth == 0 {
367 let content: String = chars[content_start..position].iter().collect();
368 return (content, position + 1);
369 }
370 }
371 position += 1;
372 }
373 let content: String = chars[content_start..].iter().collect();
374 (content, chars.len())
375}
376
377fn extract_string_literal(chars: &[char], start: usize, len: usize) -> (String, usize) {
391 let quote: char = chars[start];
392 let mut position: usize = start + 1;
393 let mut result: String = String::new();
394 result.push(quote);
395 while position < len {
396 if chars[position] == CHAR_SLASH_BACK && position + 1 < len {
397 result.push(chars[position]);
398 result.push(chars[position + 1]);
399 position += 2;
400 continue;
401 }
402 result.push(chars[position]);
403 if chars[position] == quote {
404 return (result, position + 1);
405 }
406 position += 1;
407 }
408 (result, position)
409}
410
411fn extract_line_comment(chars: &[char], start: usize, len: usize) -> (String, usize) {
423 let mut position: usize = start;
424 let mut result: String = String::new();
425 while position < len && chars[position] != CHAR_NEWLINE {
426 result.push(chars[position]);
427 position += 1;
428 }
429 if position < len {
430 result.push(CHAR_NEWLINE);
431 position += 1;
432 }
433 (result, position)
434}
435
436fn extract_block_comment(chars: &[char], start: usize, len: usize) -> (String, usize) {
448 let mut position: usize = start + 2;
449 let mut result: String = String::from(BLOCK_COMMENT_START);
450 while position + 1 < len {
451 result.push(chars[position]);
452 if chars[position] == CHAR_ASTERISK && chars[position + 1] == CHAR_SLASH_FORWARD {
453 result.push(CHAR_SLASH_FORWARD);
454 return (result, position + 2);
455 }
456 position += 1;
457 }
458 while position < len {
459 result.push(chars[position]);
460 position += 1;
461 }
462 (result, position)
463}
464
465pub fn format_macro_body<B>(body: B) -> String
477where
478 B: AsRef<str>,
479{
480 let raw: String = format_macro_body_raw(body);
481 add_indentation(&raw)
482}
483
484fn format_macro_body_raw<B>(body: B) -> String
504where
505 B: AsRef<str>,
506{
507 let body: &str = body.as_ref();
508 let chars: Vec<char> = body.chars().collect();
509 let len: usize = chars.len();
510 let mut result: String = String::new();
511 let mut position: usize = 0;
512 while position < len {
513 if chars[position] == CHAR_DOUBLE_QUOTE || chars[position] == CHAR_SINGLE_QUOTE {
514 let (literal, end) = extract_string_literal(&chars, position, len);
515 result.push_str(&literal);
516 position = end;
517 continue;
518 }
519 if position + 1 < len
520 && chars[position] == CHAR_SLASH_FORWARD
521 && chars[position + 1] == CHAR_SLASH_FORWARD
522 {
523 let (comment, end) = extract_line_comment(&chars, position, len);
524 result.push_str(&comment);
525 position = end;
526 continue;
527 }
528 if position + 1 < len
529 && chars[position] == CHAR_SLASH_FORWARD
530 && chars[position + 1] == CHAR_ASTERISK
531 {
532 let (comment, end) = extract_block_comment(&chars, position, len);
533 result.push_str(&comment);
534 position = end;
535 continue;
536 }
537 if is_if_keyword(&chars, position, len) {
538 let after_if_colon: usize = skip_spaces_on_same_line(&chars, position + 2, len);
539 if after_if_colon < len
540 && chars[after_if_colon] == CHAR_COLON
541 && (after_if_colon + 1 >= len || chars[after_if_colon + 1] != CHAR_COLON)
542 {
543 result.push_str(KEYWORD_IF);
544 position += 2;
545 if position < len && chars[position] == CHAR_SPACE {
546 result.push(CHAR_SPACE);
547 position += 1;
548 }
549 continue;
550 }
551 result.push_str(KEYWORD_IF);
552 position += 2;
553 let after_if: usize = skip_spaces_on_same_line(&chars, position, len);
554 if position < len && chars[after_if] == CHAR_BRACE_LEFT {
555 result.push(CHAR_SPACE);
556 let (block, end) = extract_brace_block(&chars, after_if);
557 result.push_str(&format_brace_block(&block));
558 position = end;
559 position = skip_spaces_on_same_line(&chars, position, len);
560 if position < len && chars[position] == CHAR_BRACE_LEFT {
561 result.push(CHAR_SPACE);
562 }
563 } else {
564 result.push(CHAR_SPACE);
565 position = after_if;
566 }
567 continue;
568 }
569 if is_else_keyword(&chars, position, len) {
570 if !result.ends_with(CHAR_SPACE)
571 && !result.ends_with(CHAR_NEWLINE)
572 && !result.ends_with(CHAR_TAB)
573 {
574 result.push(CHAR_SPACE);
575 }
576 result.push_str(KEYWORD_ELSE);
577 position += 4;
578 position = skip_spaces_on_same_line(&chars, position, len);
579 if is_if_keyword(&chars, position, len) {
580 result.push(CHAR_SPACE);
581 continue;
582 }
583 if position < len && chars[position] == CHAR_BRACE_LEFT {
584 result.push(CHAR_SPACE);
585 }
586 continue;
587 }
588 if is_match_keyword(&chars, position, len) {
589 let after_match_colon: usize = skip_spaces_on_same_line(&chars, position + 5, len);
590 if after_match_colon < len
591 && chars[after_match_colon] == CHAR_COLON
592 && (after_match_colon + 1 >= len || chars[after_match_colon + 1] != CHAR_COLON)
593 {
594 result.push_str(KEYWORD_MATCH);
595 position += 5;
596 if position < len && chars[position] == CHAR_SPACE {
597 result.push(CHAR_SPACE);
598 position += 1;
599 }
600 continue;
601 }
602 result.push_str(KEYWORD_MATCH);
603 position += 5;
604 let after_match: usize = skip_spaces_on_same_line(&chars, position, len);
605 if after_match < len && chars[after_match] == CHAR_BRACE_LEFT {
606 result.push(CHAR_SPACE);
607 let (block, end) = extract_brace_block(&chars, after_match);
608 result.push_str(&format_brace_block(&block));
609 position = end;
610 position = skip_spaces_on_same_line(&chars, position, len);
611 if position < len && chars[position] == CHAR_BRACE_LEFT {
612 result.push(CHAR_SPACE);
613 }
614 } else {
615 result.push(CHAR_SPACE);
616 position = after_match;
617 }
618 continue;
619 }
620 if is_for_keyword(&chars, position, len) {
621 let after_for: usize = skip_spaces_on_same_line(&chars, position + 3, len);
622 if after_for < len
623 && chars[after_for] == CHAR_COLON
624 && (after_for + 1 >= len || chars[after_for + 1] != CHAR_COLON)
625 {
626 result.push_str(KEYWORD_FOR);
627 position += 3;
628 if position < len && chars[position] == CHAR_SPACE {
629 result.push(CHAR_SPACE);
630 position += 1;
631 }
632 continue;
633 }
634 result.push_str(KEYWORD_FOR);
635 position += 3;
636 position = skip_spaces_on_same_line(&chars, position, len);
637 if position < len && !is_in_keyword(&chars, position, len) {
638 result.push(CHAR_SPACE);
639 }
640 while position < len && !is_in_keyword(&chars, position, len) {
641 if chars[position] == CHAR_BRACE_LEFT {
642 let (block, end) = extract_brace_block(&chars, position);
643 result.push_str(&format_brace_block(&block));
644 position = end;
645 continue;
646 }
647 if chars[position] == CHAR_DOUBLE_QUOTE || chars[position] == CHAR_SINGLE_QUOTE {
648 let (literal, end) = extract_string_literal(&chars, position, len);
649 result.push_str(&literal);
650 position = end;
651 continue;
652 }
653 result.push(chars[position]);
654 position += 1;
655 }
656 if result.ends_with(CHAR_SPACE) {
657 result.truncate(result.len() - 1);
658 }
659 position = skip_spaces_on_same_line(&chars, position, len);
660 if is_in_keyword(&chars, position, len) {
661 result.push(CHAR_SPACE);
662 result.push_str(KEYWORD_IN);
663 position += 2;
664 position = skip_spaces_on_same_line(&chars, position, len);
665 if position < len && chars[position] == CHAR_BRACE_LEFT {
666 result.push(CHAR_SPACE);
667 let (block, end) = extract_brace_block(&chars, position);
668 result.push_str(&format_brace_block(&block));
669 position = end;
670 position = skip_spaces_on_same_line(&chars, position, len);
671 if position < len && chars[position] == CHAR_BRACE_LEFT {
672 result.push(CHAR_SPACE);
673 }
674 } else {
675 result.push(CHAR_SPACE);
676 while position < len && chars[position] != CHAR_BRACE_LEFT {
677 result.push(chars[position]);
678 position += 1;
679 }
680 if result.ends_with(CHAR_SPACE) {
681 result.truncate(result.len() - 1);
682 }
683 if position < len && chars[position] == CHAR_BRACE_LEFT {
684 result.push(CHAR_SPACE);
685 }
686 }
687 }
688 continue;
689 }
690 if chars[position] == CHAR_COLON && position + 1 < len && chars[position + 1] != CHAR_COLON
691 {
692 if result.ends_with(CHAR_COLON) {
693 result.push(CHAR_COLON);
694 position += 1;
695 continue;
696 }
697 let colon_prefix: String = find_ident_before(&result);
698 if is_raw_ident_before(&result, &colon_prefix) {
699 result.push(CHAR_COLON);
700 position += 1;
701 continue;
702 }
703 if !colon_prefix.is_empty() {
704 let before_colon: String = remove_trailing_spaces(&result, colon_prefix.len());
705 result = before_colon;
706 result.push_str(&colon_prefix);
707 }
708 result.push(CHAR_COLON);
709 position += 1;
710 while position < len && (chars[position] == CHAR_SPACE || chars[position] == CHAR_TAB) {
711 position += 1;
712 }
713 if position < len
714 && chars[position] != CHAR_NEWLINE
715 && chars[position] != CHAR_CARRIAGE_RETURN
716 && !is_pseudo_selector_after_colon(&chars, position, len)
717 {
718 result.push(CHAR_SPACE);
719 }
720 continue;
721 }
722 if position + 1 < len
723 && chars[position] == CHAR_EQUALS
724 && chars[position + 1] == CHAR_GREATER_THAN
725 {
726 let trailing: String = find_trailing_spaces(&result);
727 if !trailing.is_empty() {
728 result.truncate(result.len() - trailing.len());
729 }
730 result.push(CHAR_SPACE);
731 result.push_str(ARROW_FAT);
732 position += 2;
733 while position < len && (chars[position] == CHAR_SPACE || chars[position] == CHAR_TAB) {
734 position += 1;
735 }
736 result.push(CHAR_SPACE);
737 continue;
738 }
739 if chars[position] == CHAR_BRACE_LEFT {
740 let (inner, end) = extract_brace_content(&chars, position);
741 let trimmed_inner: &str = inner.trim();
742 if trimmed_inner.is_empty() {
743 result.push(CHAR_BRACE_LEFT);
744 result.push(CHAR_BRACE_RIGHT);
745 } else {
746 let formatted_inner: String = format_macro_body_raw(trimmed_inner);
747 result.push(CHAR_BRACE_LEFT);
748 result.push(CHAR_NEWLINE);
749 result.push_str(&formatted_inner);
750 result.push(CHAR_NEWLINE);
751 result.push(CHAR_BRACE_RIGHT);
752 }
753 position = end;
754 continue;
755 }
756 if is_ident_char(chars[position]) {
757 let start: usize = position;
758 while position < len && is_ident_char(chars[position]) {
759 position += 1;
760 }
761 let ident: String = chars[start..position].iter().collect();
762 result.push_str(&ident);
763 let ws_start: usize = position;
764 while position < len && (chars[position] == CHAR_SPACE || chars[position] == CHAR_TAB) {
765 position += 1;
766 }
767 let had_whitespace: bool = position > ws_start;
768 if position < len && chars[position] == CHAR_BRACE_LEFT {
769 result.push(CHAR_SPACE);
770 } else if had_whitespace {
771 let next_pos: usize = position;
772 if next_pos < len && is_ident_char(chars[next_pos]) {
773 let mut ident_end: usize = next_pos;
774 while ident_end < len && is_ident_char(chars[ident_end]) {
775 ident_end += 1;
776 }
777 let after_ident: usize = skip_spaces_on_same_line(&chars, ident_end, len);
778 if after_ident < len
779 && chars[after_ident] == CHAR_COLON
780 && (after_ident + 1 >= len || chars[after_ident + 1] != CHAR_COLON)
781 {
782 result.push(CHAR_NEWLINE);
783 continue;
784 }
785 }
786 let ws: String = chars[ws_start..position].iter().collect();
787 result.push_str(&ws);
788 }
789 continue;
790 }
791 result.push(chars[position]);
792 position += 1;
793 }
794 result
795}
796
797fn add_indentation(body: &str) -> String {
810 let chars: Vec<char> = body.chars().collect();
811 let len: usize = chars.len();
812 let mut result: String = String::new();
813 let mut depth: i32 = 0;
814 let mut index: usize = 0;
815 while index < len {
816 if chars[index] == CHAR_DOUBLE_QUOTE || chars[index] == CHAR_SINGLE_QUOTE {
817 let quote: char = chars[index];
818 result.push(chars[index]);
819 index += 1;
820 while index < len {
821 if chars[index] == CHAR_SLASH_BACK && index + 1 < len {
822 result.push(chars[index]);
823 result.push(chars[index + 1]);
824 index += 2;
825 continue;
826 }
827 result.push(chars[index]);
828 if chars[index] == quote {
829 index += 1;
830 break;
831 }
832 index += 1;
833 }
834 continue;
835 }
836 if index + 1 < len
837 && chars[index] == CHAR_SLASH_FORWARD
838 && chars[index + 1] == CHAR_SLASH_FORWARD
839 {
840 while index < len && chars[index] != CHAR_NEWLINE {
841 result.push(chars[index]);
842 index += 1;
843 }
844 continue;
845 }
846 if index + 1 < len
847 && chars[index] == CHAR_SLASH_FORWARD
848 && chars[index + 1] == CHAR_ASTERISK
849 {
850 result.push(chars[index]);
851 result.push(chars[index + 1]);
852 index += 2;
853 while index + 1 < len {
854 if chars[index] == CHAR_ASTERISK && chars[index + 1] == CHAR_SLASH_FORWARD {
855 result.push(chars[index]);
856 result.push(chars[index + 1]);
857 index += 2;
858 break;
859 }
860 result.push(chars[index]);
861 index += 1;
862 }
863 continue;
864 }
865 if chars[index] == CHAR_NEWLINE {
866 result.push(CHAR_NEWLINE);
867 index += 1;
868 while index < len && (chars[index] == CHAR_SPACE || chars[index] == CHAR_TAB) {
869 index += 1;
870 }
871 if index >= len || chars[index] == CHAR_NEWLINE || chars[index] == CHAR_CARRIAGE_RETURN
872 {
873 continue;
874 }
875 let mut closing_count: i32 = 0;
876 let mut peek: usize = index;
877 while peek < len && chars[peek] == CHAR_BRACE_RIGHT {
878 closing_count += 1;
879 peek += 1;
880 }
881 let indent_depth: i32 = (depth - closing_count).max(0);
882 for _ in 0..indent_depth * 4 {
883 result.push(CHAR_SPACE);
884 }
885 continue;
886 }
887 if chars[index] == CHAR_BRACE_LEFT {
888 depth += 1;
889 result.push(CHAR_BRACE_LEFT);
890 index += 1;
891 continue;
892 }
893 if chars[index] == CHAR_BRACE_RIGHT {
894 depth -= 1;
895 result.push(CHAR_BRACE_RIGHT);
896 index += 1;
897 let mut peek: usize = index;
898 while peek < len && (chars[peek] == CHAR_SPACE || chars[peek] == CHAR_TAB) {
899 peek += 1;
900 }
901 if peek < len
902 && chars[peek] != CHAR_BRACE_RIGHT
903 && chars[peek] != CHAR_BRACE_LEFT
904 && chars[peek] != CHAR_NEWLINE
905 && chars[peek] != CHAR_CARRIAGE_RETURN
906 && chars[peek] != CHAR_RIGHT_PAREN
907 && chars[peek] != CHAR_COMMA
908 && chars[peek] != CHAR_SEMICOLON
909 {
910 let next_chars: String = chars[peek..(peek + 4).min(len)].iter().collect();
911 if next_chars != "else" {
912 result.push(CHAR_NEWLINE);
913 let indent_depth: i32 = depth.max(0);
914 for _ in 0..indent_depth * 4 {
915 result.push(CHAR_SPACE);
916 }
917 index = peek;
918 }
919 }
920 continue;
921 }
922 result.push(chars[index]);
923 index += 1;
924 }
925 result
926}
927
928fn indented_body_skipping_block_comments(body: &str, indent_str: &str) -> String {
941 let chars: Vec<char> = body.chars().collect();
942 let len: usize = chars.len();
943 let mut in_block_comment: bool = false;
944 let mut line_starts_in_comment: bool = false;
945 let mut result_lines: Vec<String> = Vec::new();
946 let mut current_line: String = String::new();
947 let mut index: usize = 0;
948 while index < len {
949 if !in_block_comment
950 && index + 1 < len
951 && chars[index] == CHAR_SLASH_FORWARD
952 && chars[index + 1] == CHAR_ASTERISK
953 {
954 in_block_comment = true;
955 line_starts_in_comment = current_line.is_empty()
956 || current_line
957 .chars()
958 .all(|c: char| c == CHAR_SPACE || c == CHAR_TAB);
959 current_line.push(chars[index]);
960 current_line.push(chars[index + 1]);
961 index += 2;
962 continue;
963 }
964 if in_block_comment {
965 current_line.push(chars[index]);
966 if chars[index] == CHAR_ASTERISK
967 && index + 1 < len
968 && chars[index + 1] == CHAR_SLASH_FORWARD
969 {
970 current_line.push(chars[index + 1]);
971 index += 2;
972 in_block_comment = false;
973 continue;
974 }
975 index += 1;
976 continue;
977 }
978 if chars[index] == CHAR_NEWLINE {
979 current_line.push(CHAR_NEWLINE);
980 let line_owned: String = std::mem::take(&mut current_line);
981 let line_ref: &str = line_owned.as_str();
982 let indented: String = if line_starts_in_comment || line_ref.trim().is_empty() {
983 line_owned
984 } else {
985 let mut s: String = String::with_capacity(indent_str.len() + line_owned.len());
986 s.push_str(indent_str);
987 s.push_str(line_owned.as_str());
988 s
989 };
990 result_lines.push(indented);
991 line_starts_in_comment = in_block_comment;
997 index += 1;
998 continue;
999 }
1000 current_line.push(chars[index]);
1001 index += 1;
1002 }
1003 let line_owned: String = current_line;
1004 let indented: String = if line_starts_in_comment || line_owned.trim().is_empty() {
1005 line_owned
1006 } else {
1007 let mut s: String = String::with_capacity(indent_str.len() + line_owned.len());
1008 s.push_str(indent_str);
1009 s.push_str(line_owned.as_str());
1010 s
1011 };
1012 result_lines.push(indented);
1013 result_lines.join("")
1014}
1015
1016fn is_if_keyword(chars: &[char], pos: usize, len: usize) -> bool {
1028 if pos + 2 > len {
1029 return false;
1030 }
1031 chars[pos] == CHAR_LETTER_I
1032 && chars[pos + 1] == CHAR_LETTER_F
1033 && (pos + 2 >= len || !is_ident_char(chars[pos + 2]))
1034 && (pos == 0 || !is_ident_char(chars[pos - 1]))
1035 && !is_raw_prefix(chars, pos)
1036}
1037
1038fn is_else_keyword(chars: &[char], pos: usize, len: usize) -> bool {
1050 if pos + 4 > len {
1051 return false;
1052 }
1053 chars[pos] == CHAR_LETTER_E
1054 && chars[pos + 1] == CHAR_LETTER_L
1055 && chars[pos + 2] == CHAR_LETTER_S
1056 && chars[pos + 3] == CHAR_LETTER_E
1057 && (pos + 4 >= len || !is_ident_char(chars[pos + 4]))
1058 && (pos == 0 || !is_ident_char(chars[pos - 1]))
1059 && !is_raw_prefix(chars, pos)
1060}
1061
1062fn is_match_keyword(chars: &[char], pos: usize, len: usize) -> bool {
1074 if pos + 5 > len {
1075 return false;
1076 }
1077 chars[pos] == CHAR_LETTER_M
1078 && chars[pos + 1] == CHAR_LETTER_A
1079 && chars[pos + 2] == CHAR_LETTER_T
1080 && chars[pos + 3] == CHAR_LETTER_C
1081 && chars[pos + 4] == CHAR_LETTER_H
1082 && (pos + 5 >= len || !is_ident_char(chars[pos + 5]))
1083 && (pos == 0 || !is_ident_char(chars[pos - 1]))
1084 && !is_raw_prefix(chars, pos)
1085}
1086
1087fn is_for_keyword(chars: &[char], pos: usize, len: usize) -> bool {
1099 if pos + 3 > len {
1100 return false;
1101 }
1102 chars[pos] == CHAR_LETTER_F
1103 && chars[pos + 1] == CHAR_LETTER_O
1104 && chars[pos + 2] == CHAR_LETTER_R
1105 && (pos + 3 >= len || !is_ident_char(chars[pos + 3]))
1106 && (pos == 0 || !is_ident_char(chars[pos - 1]))
1107 && !is_raw_prefix(chars, pos)
1108}
1109
1110fn is_in_keyword(chars: &[char], pos: usize, len: usize) -> bool {
1122 if pos + 2 > len {
1123 return false;
1124 }
1125 chars[pos] == CHAR_LETTER_I
1126 && chars[pos + 1] == CHAR_LETTER_N
1127 && (pos + 2 >= len || !is_ident_char(chars[pos + 2]))
1128 && (pos == 0 || !is_ident_char(chars[pos - 1]))
1129 && !is_raw_prefix(chars, pos)
1130}
1131
1132fn format_brace_block(block: &str) -> String {
1146 let block_chars: Vec<char> = block.chars().collect();
1147 if block_chars.len() < 2
1148 || block_chars[0] != CHAR_BRACE_LEFT
1149 || block_chars.last() != Some(&CHAR_BRACE_RIGHT)
1150 {
1151 return block.to_string();
1152 }
1153 let inner: String = block_chars[1..block_chars.len() - 1].iter().collect();
1154 if inner.contains(CHAR_NEWLINE) {
1155 return block.to_string();
1156 }
1157 let trimmed_inner: &str = inner.trim();
1158 if trimmed_inner.is_empty() {
1159 let mut empty_result: String = String::new();
1160 empty_result.push(CHAR_BRACE_LEFT);
1161 empty_result.push(CHAR_BRACE_RIGHT);
1162 return empty_result;
1163 }
1164 let mut formatted: String = String::new();
1165 formatted.push(CHAR_BRACE_LEFT);
1166 formatted.push(CHAR_SPACE);
1167 formatted.push_str(trimmed_inner);
1168 formatted.push(CHAR_SPACE);
1169 formatted.push(CHAR_BRACE_RIGHT);
1170 formatted
1171}
1172
1173fn skip_spaces_on_same_line(chars: &[char], mut pos: usize, len: usize) -> usize {
1185 while pos < len && (chars[pos] == CHAR_SPACE || chars[pos] == CHAR_TAB) {
1186 pos += 1;
1187 }
1188 pos
1189}
1190
1191fn is_pseudo_selector_after_colon(chars: &[char], mut pos: usize, len: usize) -> bool {
1216 if pos >= len || !is_ident_char(chars[pos]) {
1217 return false;
1218 }
1219 let ident_start: usize = pos;
1220 while pos < len && is_ident_char(chars[pos]) {
1221 pos += 1;
1222 }
1223 let first_ident: String = chars[ident_start..pos].iter().collect();
1224 if is_rust_keyword(&first_ident) {
1225 return false;
1226 }
1227 while pos < len && chars[pos] == CHAR_HYPHEN && pos + 1 < len && is_ident_char(chars[pos + 1]) {
1228 pos += 1;
1229 while pos < len && is_ident_char(chars[pos]) {
1230 pos += 1;
1231 }
1232 }
1233 let after_ident: usize = skip_spaces_on_same_line(chars, pos, len);
1234 if after_ident < len && chars[after_ident] == CHAR_BRACE_LEFT {
1235 return true;
1236 }
1237 if after_ident < len && chars[after_ident] == CHAR_LEFT_PAREN {
1238 let mut depth: i32 = 0;
1239 let mut paren_pos: usize = after_ident;
1240 while paren_pos < len {
1241 if chars[paren_pos] == CHAR_LEFT_PAREN {
1242 depth += 1;
1243 } else if chars[paren_pos] == CHAR_RIGHT_PAREN {
1244 depth -= 1;
1245 if depth == 0 {
1246 let after_paren: usize = skip_spaces_on_same_line(chars, paren_pos + 1, len);
1247 return after_paren < len && chars[after_paren] == CHAR_BRACE_LEFT;
1248 }
1249 }
1250 paren_pos += 1;
1251 }
1252 }
1253 false
1254}
1255
1256fn is_rust_keyword(ident: &str) -> bool {
1269 matches!(ident, KEYWORD_IF | KEYWORD_MATCH | KEYWORD_FOR)
1270}
1271
1272fn is_raw_ident_before(result: &str, ident: &str) -> bool {
1286 result
1287 .trim_end()
1288 .ends_with(&format!("{RAW_IDENT_PREFIX}{ident}"))
1289}
1290
1291fn find_ident_before(result: &str) -> String {
1301 let chars: Vec<char> = result.chars().collect();
1302 let mut end: usize = chars.len();
1303 while end > 0 && chars[end - 1] == CHAR_SPACE {
1304 end -= 1;
1305 }
1306 let mut start: usize = end;
1307 while start > 0 && is_ident_char(chars[start - 1]) {
1308 start -= 1;
1309 }
1310 if start < end {
1311 chars[start..end].iter().collect()
1312 } else {
1313 String::new()
1314 }
1315}
1316
1317fn remove_trailing_spaces(result: &str, prefix_len: usize) -> String {
1328 let chars: Vec<char> = result.chars().collect();
1329 let total_len: usize = chars.len();
1330 let mut end: usize = total_len;
1331 while end > 0 && chars[end - 1] == CHAR_SPACE {
1332 end -= 1;
1333 }
1334 if prefix_len > end {
1335 return result.to_string();
1336 }
1337 let new_end: usize = end - prefix_len;
1338 chars[..new_end].iter().collect()
1339}
1340
1341fn find_trailing_spaces(result: &str) -> String {
1351 let mut spaces: String = String::new();
1352 for ch in result.chars().rev() {
1353 if ch == CHAR_SPACE || ch == CHAR_TAB {
1354 spaces.push(ch);
1355 } else {
1356 break;
1357 }
1358 }
1359 spaces.chars().rev().collect()
1360}
1361
1362pub async fn format_dir(path: &Path, mode: FmtMode) -> Result<(), EuvError> {
1376 if path.is_file() {
1377 let changed: bool = format_file(path, &mode).await?;
1378 match mode {
1379 FmtMode::Check => {
1380 if changed {
1381 return Err(EuvError::Message(format!(
1382 "{} needs formatting.",
1383 path.display()
1384 )));
1385 }
1386 log::info!("{} is properly formatted.", path.display());
1387 }
1388 FmtMode::Write => {
1389 if changed {
1390 log::info!("Formatted: {}", path.display());
1391 } else {
1392 log::info!("Already formatted: {}", path.display());
1393 }
1394 }
1395 }
1396 return Ok(());
1397 }
1398 let mut entries: Vec<PathBuf> = collect_rs_files(path).await?;
1399 entries.sort();
1400 let mut changed_count: usize = 0;
1401 let mut unchanged_count: usize = 0;
1402 for entry in entries {
1403 match format_file(&entry, &mode).await {
1404 Ok(changed) => {
1405 if changed {
1406 changed_count += 1;
1407 } else {
1408 unchanged_count += 1;
1409 }
1410 }
1411 Err(error) => {
1412 log::warn!("Failed to format {}: {error}", entry.display());
1413 }
1414 }
1415 }
1416 match mode {
1417 FmtMode::Check => {
1418 if changed_count > 0 {
1419 return Err(EuvError::Message(format!(
1420 "{} file(s) need formatting. Run `euv fmt` to fix.",
1421 changed_count
1422 )));
1423 }
1424 log::info!("All {} file(s) are properly formatted.", unchanged_count);
1425 }
1426 FmtMode::Write => {
1427 log::info!(
1428 "Formatted {} file(s), {} unchanged.",
1429 changed_count,
1430 unchanged_count
1431 );
1432 }
1433 }
1434 Ok(())
1435}
1436
1437async fn collect_rs_files(path: &Path) -> Result<Vec<PathBuf>, EuvError> {
1447 let mut result: Vec<PathBuf> = Vec::new();
1448 let mut stack: Vec<PathBuf> = vec![path.to_path_buf()];
1449 while let Some(dir) = stack.pop() {
1450 let mut entries: ReadDir =
1451 read_dir(&dir)
1452 .await
1453 .map_err(|error: io::Error| EuvError::IoPath {
1454 message: String::from("Failed to read directory"),
1455 path: dir.clone(),
1456 error,
1457 })?;
1458 while let Some(entry) =
1459 entries
1460 .next_entry()
1461 .await
1462 .map_err(|error: io::Error| EuvError::IoPath {
1463 message: String::from("Failed to read entry in directory"),
1464 path: dir.clone(),
1465 error,
1466 })?
1467 {
1468 let entry_path: PathBuf = entry.path();
1469 if entry_path.is_dir() {
1470 let file_name: String = entry_path
1471 .file_name()
1472 .unwrap_or_default()
1473 .to_string_lossy()
1474 .to_string();
1475 if file_name != TARGET_DIR_NAME && file_name != NODE_MODULES_DIR_NAME {
1476 stack.push(entry_path);
1477 }
1478 } else if entry_path
1479 .extension()
1480 .is_some_and(|ext: &OsStr| ext == RS_EXTENSION)
1481 {
1482 result.push(entry_path);
1483 }
1484 }
1485 }
1486 Ok(result)
1487}
1488
1489async fn format_file(path: &Path, mode: &FmtMode) -> Result<bool, EuvError> {
1500 let content: String =
1501 read_to_string(path)
1502 .await
1503 .map_err(|error: io::Error| EuvError::IoPath {
1504 message: String::from("Failed to read"),
1505 path: path.to_path_buf(),
1506 error,
1507 })?;
1508 let fmt_result: FmtResult = format_source(&content);
1509 if fmt_result.get_changed() {
1510 match mode {
1511 FmtMode::Write => {
1512 write(path, fmt_result.get_output())
1513 .await
1514 .map_err(|error: io::Error| EuvError::IoPath {
1515 message: String::from("Failed to write"),
1516 path: path.to_path_buf(),
1517 error,
1518 })?;
1519 log::info!("Formatted: {}", path.display());
1520 }
1521 FmtMode::Check => {
1522 log::warn!("Needs formatting: {}", path.display());
1523 }
1524 }
1525 }
1526 Ok(fmt_result.get_changed())
1527}