1use crate::*;
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 =
73 result.rfind(CHAR_NEWLINE).map(|i| i + 1).unwrap_or(0);
74 let macro_indent: usize = result[last_newline_pos..]
75 .chars()
76 .take_while(|&c| c == CHAR_SPACE)
77 .count();
78 let min_indent: usize = body_content
79 .lines()
80 .filter(|line| !line.trim().is_empty())
81 .map(|line| line.chars().take_while(|c| c.is_whitespace()).count())
82 .min()
83 .unwrap_or(0);
84 let base_indent: usize = if min_indent > 0 {
85 min_indent
86 } else {
87 macro_indent + 4
88 };
89 let indent_str: String = " ".repeat(base_indent);
90 let indented_body: String = trimmed_body
91 .lines()
92 .map(|line| {
93 if line.trim().is_empty() {
94 line.to_string()
95 } else {
96 format!("{}{}", indent_str, line)
97 }
98 })
99 .collect::<Vec<String>>()
100 .join("\n");
101 let outer_indent_str: String = " ".repeat(macro_indent);
102 result.push(CHAR_BRACE_LEFT);
103 result.push(CHAR_NEWLINE);
104 result.push_str(&indented_body);
105 result.push(CHAR_NEWLINE);
106 result.push_str(&outer_indent_str);
107 result.push(CHAR_BRACE_RIGHT);
108 }
109 position = end_pos;
110 continue;
111 }
112 }
113 continue;
114 }
115 if chars[position] == CHAR_DOUBLE_QUOTE || chars[position] == CHAR_SINGLE_QUOTE {
116 let (literal, end_pos) = extract_string_literal(&chars, position, len);
117 result.push_str(&literal);
118 position = end_pos;
119 continue;
120 }
121 if position + 1 < len
122 && chars[position] == CHAR_SLASH_FORWARD
123 && chars[position + 1] == CHAR_SLASH_FORWARD
124 {
125 let (comment, end_pos) = extract_line_comment(&chars, position, len);
126 result.push_str(&comment);
127 position = end_pos;
128 continue;
129 }
130 if position + 1 < len
131 && chars[position] == CHAR_SLASH_FORWARD
132 && chars[position + 1] == CHAR_ASTERISK
133 {
134 let (comment, end_pos) = extract_block_comment(&chars, position, len);
135 result.push_str(&comment);
136 position = end_pos;
137 continue;
138 }
139 result.push(chars[position]);
140 position += 1;
141 }
142 result
143}
144
145fn is_euv_macro_start(chars: &[char], pos: usize, len: usize) -> bool {
157 for name in EUV_MACRO_NAMES {
158 let name_len: usize = name.len();
159 if pos + name_len > len {
160 continue;
161 }
162 let candidate: String = chars[pos..pos + name_len].iter().collect::<String>();
163 if candidate != *name {
164 continue;
165 }
166 if pos > 0 && is_ident_char(chars[pos - 1]) {
167 continue;
168 }
169 if pos + name_len < len && is_ident_char(chars[pos + name_len]) {
170 continue;
171 }
172 return true;
173 }
174 false
175}
176
177fn find_macro_name_end(chars: &[char], pos: usize, _len: usize) -> usize {
189 let mut end: usize = pos;
190 while end < chars.len() && is_ident_char(chars[end]) {
191 end += 1;
192 }
193 end
194}
195
196fn is_raw_prefix(chars: &[char], pos: usize) -> bool {
208 if pos < 2 {
209 return false;
210 }
211 chars[pos - 2] == CHAR_LETTER_R && chars[pos - 1] == CHAR_HASH
212}
213
214fn is_ident_char(character: char) -> bool {
224 character.is_alphanumeric() || character == CHAR_UNDERSCORE
225}
226
227fn skip_whitespace_and_comments(
240 chars: &[char],
241 mut pos: usize,
242 len: usize,
243 result: &mut String,
244) -> usize {
245 while pos < len {
246 if chars[pos].is_whitespace() {
247 result.push(chars[pos]);
248 pos += 1;
249 } else if pos + 1 < len
250 && chars[pos] == CHAR_SLASH_FORWARD
251 && chars[pos + 1] == CHAR_SLASH_FORWARD
252 {
253 let (comment, end_pos) = extract_line_comment(chars, pos, len);
254 result.push_str(&comment);
255 pos = end_pos;
256 } else if pos + 1 < len
257 && chars[pos] == CHAR_SLASH_FORWARD
258 && chars[pos + 1] == CHAR_ASTERISK
259 {
260 let (comment, end_pos) = extract_block_comment(chars, pos, len);
261 result.push_str(&comment);
262 pos = end_pos;
263 } else {
264 break;
265 }
266 }
267 pos
268}
269
270fn extract_brace_block(chars: &[char], start: usize) -> (String, usize) {
284 let mut depth: i32 = 0;
285 let mut position: usize = start;
286 while position < chars.len() {
287 if chars[position] == CHAR_DOUBLE_QUOTE || chars[position] == CHAR_SINGLE_QUOTE {
288 let (_, end) = extract_string_literal(chars, position, chars.len());
289 position = end;
290 continue;
291 }
292 if position + 1 < chars.len()
293 && chars[position] == CHAR_SLASH_FORWARD
294 && chars[position + 1] == CHAR_SLASH_FORWARD
295 {
296 let (_, end) = extract_line_comment(chars, position, chars.len());
297 position = end;
298 continue;
299 }
300 if position + 1 < chars.len()
301 && chars[position] == CHAR_SLASH_FORWARD
302 && chars[position + 1] == CHAR_ASTERISK
303 {
304 let (_, end) = extract_block_comment(chars, position, chars.len());
305 position = end;
306 continue;
307 }
308 if chars[position] == CHAR_BRACE_LEFT {
309 depth += 1;
310 } else if chars[position] == CHAR_BRACE_RIGHT {
311 depth -= 1;
312 if depth == 0 {
313 let content: String = chars[start..=position].iter().collect();
314 return (content, position + 1);
315 }
316 }
317 position += 1;
318 }
319 let content: String = chars[start..].iter().collect();
320 (content, chars.len())
321}
322
323fn extract_brace_content(chars: &[char], start: usize) -> (String, usize) {
337 let mut depth: i32 = 0;
338 let mut position: usize = start;
339 let mut content_start: usize = start + 1;
340 while position < chars.len() {
341 if chars[position] == CHAR_DOUBLE_QUOTE || chars[position] == CHAR_SINGLE_QUOTE {
342 let (_, end) = extract_string_literal(chars, position, chars.len());
343 position = end;
344 continue;
345 }
346 if position + 1 < chars.len()
347 && chars[position] == CHAR_SLASH_FORWARD
348 && chars[position + 1] == CHAR_SLASH_FORWARD
349 {
350 let (_, end) = extract_line_comment(chars, position, chars.len());
351 position = end;
352 continue;
353 }
354 if position + 1 < chars.len()
355 && chars[position] == CHAR_SLASH_FORWARD
356 && chars[position + 1] == CHAR_ASTERISK
357 {
358 let (_, end) = extract_block_comment(chars, position, chars.len());
359 position = end;
360 continue;
361 }
362 if chars[position] == CHAR_BRACE_LEFT {
363 if depth == 0 {
364 content_start = position + 1;
365 }
366 depth += 1;
367 } else if chars[position] == CHAR_BRACE_RIGHT {
368 depth -= 1;
369 if depth == 0 {
370 let content: String = chars[content_start..position].iter().collect();
371 return (content, position + 1);
372 }
373 }
374 position += 1;
375 }
376 let content: String = chars[content_start..].iter().collect();
377 (content, chars.len())
378}
379
380fn extract_string_literal(chars: &[char], start: usize, len: usize) -> (String, usize) {
394 let quote: char = chars[start];
395 let mut position: usize = start + 1;
396 let mut result: String = String::new();
397 result.push(quote);
398 while position < len {
399 if chars[position] == CHAR_SLASH_BACK && position + 1 < len {
400 result.push(chars[position]);
401 result.push(chars[position + 1]);
402 position += 2;
403 continue;
404 }
405 result.push(chars[position]);
406 if chars[position] == quote {
407 return (result, position + 1);
408 }
409 position += 1;
410 }
411 (result, position)
412}
413
414fn extract_line_comment(chars: &[char], start: usize, len: usize) -> (String, usize) {
426 let mut position: usize = start;
427 let mut result: String = String::new();
428 while position < len && chars[position] != CHAR_NEWLINE {
429 result.push(chars[position]);
430 position += 1;
431 }
432 if position < len {
433 result.push(CHAR_NEWLINE);
434 position += 1;
435 }
436 (result, position)
437}
438
439fn extract_block_comment(chars: &[char], start: usize, len: usize) -> (String, usize) {
451 let mut position: usize = start + 2;
452 let mut result: String = String::from(BLOCK_COMMENT_START);
453 while position + 1 < len {
454 result.push(chars[position]);
455 if chars[position] == CHAR_ASTERISK && chars[position + 1] == CHAR_SLASH_FORWARD {
456 result.push(CHAR_SLASH_FORWARD);
457 return (result, position + 2);
458 }
459 position += 1;
460 }
461 while position < len {
462 result.push(chars[position]);
463 position += 1;
464 }
465 (result, position)
466}
467
468pub fn format_macro_body<B>(body: B) -> String
480where
481 B: AsRef<str>,
482{
483 let raw: String = format_macro_body_raw(body);
484 add_indentation(&raw)
485}
486
487fn format_macro_body_raw<B>(body: B) -> String
507where
508 B: AsRef<str>,
509{
510 let body: &str = body.as_ref();
511 let chars: Vec<char> = body.chars().collect();
512 let len: usize = chars.len();
513 let mut result: String = String::new();
514 let mut position: usize = 0;
515 while position < len {
516 if chars[position] == CHAR_DOUBLE_QUOTE || chars[position] == CHAR_SINGLE_QUOTE {
517 let (literal, end) = extract_string_literal(&chars, position, len);
518 result.push_str(&literal);
519 position = end;
520 continue;
521 }
522 if position + 1 < len
523 && chars[position] == CHAR_SLASH_FORWARD
524 && chars[position + 1] == CHAR_SLASH_FORWARD
525 {
526 let (comment, end) = extract_line_comment(&chars, position, len);
527 result.push_str(&comment);
528 position = end;
529 continue;
530 }
531 if position + 1 < len
532 && chars[position] == CHAR_SLASH_FORWARD
533 && chars[position + 1] == CHAR_ASTERISK
534 {
535 let (comment, end) = extract_block_comment(&chars, position, len);
536 result.push_str(&comment);
537 position = end;
538 continue;
539 }
540 if is_if_keyword(&chars, position, len) {
541 let after_if_colon: usize = skip_spaces_on_same_line(&chars, position + 2, len);
542 if after_if_colon < len
543 && chars[after_if_colon] == CHAR_COLON
544 && (after_if_colon + 1 >= len || chars[after_if_colon + 1] != CHAR_COLON)
545 {
546 result.push_str(KEYWORD_IF);
547 position += 2;
548 if position < len && chars[position] == CHAR_SPACE {
549 result.push(CHAR_SPACE);
550 position += 1;
551 }
552 continue;
553 }
554 result.push_str(KEYWORD_IF);
555 position += 2;
556 let after_if: usize = skip_spaces_on_same_line(&chars, position, len);
557 if position < len && chars[after_if] == CHAR_BRACE_LEFT {
558 result.push(CHAR_SPACE);
559 let (block, end) = extract_brace_block(&chars, after_if);
560 result.push_str(&format_brace_block(&block));
561 position = end;
562 position = skip_spaces_on_same_line(&chars, position, len);
563 if position < len && chars[position] == CHAR_BRACE_LEFT {
564 result.push(CHAR_SPACE);
565 }
566 } else {
567 result.push(CHAR_SPACE);
568 position = after_if;
569 }
570 continue;
571 }
572 if is_else_keyword(&chars, position, len) {
573 if !result.ends_with(CHAR_SPACE)
574 && !result.ends_with(CHAR_NEWLINE)
575 && !result.ends_with(CHAR_TAB)
576 {
577 result.push(CHAR_SPACE);
578 }
579 result.push_str(KEYWORD_ELSE);
580 position += 4;
581 position = skip_spaces_on_same_line(&chars, position, len);
582 if is_if_keyword(&chars, position, len) {
583 result.push(CHAR_SPACE);
584 continue;
585 }
586 if position < len && chars[position] == CHAR_BRACE_LEFT {
587 result.push(CHAR_SPACE);
588 }
589 continue;
590 }
591 if is_match_keyword(&chars, position, len) {
592 let after_match_colon: usize = skip_spaces_on_same_line(&chars, position + 5, len);
593 if after_match_colon < len
594 && chars[after_match_colon] == CHAR_COLON
595 && (after_match_colon + 1 >= len || chars[after_match_colon + 1] != CHAR_COLON)
596 {
597 result.push_str(KEYWORD_MATCH);
598 position += 5;
599 if position < len && chars[position] == CHAR_SPACE {
600 result.push(CHAR_SPACE);
601 position += 1;
602 }
603 continue;
604 }
605 result.push_str(KEYWORD_MATCH);
606 position += 5;
607 let after_match: usize = skip_spaces_on_same_line(&chars, position, len);
608 if after_match < len && chars[after_match] == CHAR_BRACE_LEFT {
609 result.push(CHAR_SPACE);
610 let (block, end) = extract_brace_block(&chars, after_match);
611 result.push_str(&format_brace_block(&block));
612 position = end;
613 position = skip_spaces_on_same_line(&chars, position, len);
614 if position < len && chars[position] == CHAR_BRACE_LEFT {
615 result.push(CHAR_SPACE);
616 }
617 } else {
618 result.push(CHAR_SPACE);
619 position = after_match;
620 }
621 continue;
622 }
623 if is_for_keyword(&chars, position, len) {
624 let after_for: usize = skip_spaces_on_same_line(&chars, position + 3, len);
625 if after_for < len
626 && chars[after_for] == CHAR_COLON
627 && (after_for + 1 >= len || chars[after_for + 1] != CHAR_COLON)
628 {
629 result.push_str(KEYWORD_FOR);
630 position += 3;
631 if position < len && chars[position] == CHAR_SPACE {
632 result.push(CHAR_SPACE);
633 position += 1;
634 }
635 continue;
636 }
637 result.push_str(KEYWORD_FOR);
638 position += 3;
639 position = skip_spaces_on_same_line(&chars, position, len);
640 if position < len && !is_in_keyword(&chars, position, len) {
641 result.push(CHAR_SPACE);
642 }
643 while position < len && !is_in_keyword(&chars, position, len) {
644 if chars[position] == CHAR_BRACE_LEFT {
645 let (block, end) = extract_brace_block(&chars, position);
646 result.push_str(&format_brace_block(&block));
647 position = end;
648 continue;
649 }
650 if chars[position] == CHAR_DOUBLE_QUOTE || chars[position] == CHAR_SINGLE_QUOTE {
651 let (literal, end) = extract_string_literal(&chars, position, len);
652 result.push_str(&literal);
653 position = end;
654 continue;
655 }
656 result.push(chars[position]);
657 position += 1;
658 }
659 if result.ends_with(CHAR_SPACE) {
660 result.truncate(result.len() - 1);
661 }
662 position = skip_spaces_on_same_line(&chars, position, len);
663 if is_in_keyword(&chars, position, len) {
664 result.push(CHAR_SPACE);
665 result.push_str(KEYWORD_IN);
666 position += 2;
667 position = skip_spaces_on_same_line(&chars, position, len);
668 if position < len && chars[position] == CHAR_BRACE_LEFT {
669 result.push(CHAR_SPACE);
670 let (block, end) = extract_brace_block(&chars, position);
671 result.push_str(&format_brace_block(&block));
672 position = end;
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 }
677 } else {
678 result.push(CHAR_SPACE);
679 while position < len && chars[position] != CHAR_BRACE_LEFT {
680 result.push(chars[position]);
681 position += 1;
682 }
683 if result.ends_with(CHAR_SPACE) {
684 result.truncate(result.len() - 1);
685 }
686 if position < len && chars[position] == CHAR_BRACE_LEFT {
687 result.push(CHAR_SPACE);
688 }
689 }
690 }
691 continue;
692 }
693 if chars[position] == CHAR_COLON && position + 1 < len && chars[position + 1] != CHAR_COLON
694 {
695 if result.ends_with(CHAR_COLON) {
696 result.push(CHAR_COLON);
697 position += 1;
698 continue;
699 }
700 let colon_prefix: String = find_ident_before(&result);
701 if is_raw_ident_before(&result, &colon_prefix) {
702 result.push(CHAR_COLON);
703 position += 1;
704 continue;
705 }
706 if !colon_prefix.is_empty() {
707 let before_colon: String = remove_trailing_spaces(&result, colon_prefix.len());
708 result = before_colon;
709 result.push_str(&colon_prefix);
710 }
711 result.push(CHAR_COLON);
712 position += 1;
713 while position < len && (chars[position] == CHAR_SPACE || chars[position] == CHAR_TAB) {
714 position += 1;
715 }
716 if position < len
717 && chars[position] != CHAR_NEWLINE
718 && chars[position] != CHAR_CARRIAGE_RETURN
719 && !is_pseudo_selector_after_colon(&chars, position, len)
720 {
721 result.push(CHAR_SPACE);
722 }
723 continue;
724 }
725 if position + 1 < len
726 && chars[position] == CHAR_EQUALS
727 && chars[position + 1] == CHAR_GREATER_THAN
728 {
729 let trailing: String = find_trailing_spaces(&result);
730 if !trailing.is_empty() {
731 result.truncate(result.len() - trailing.len());
732 }
733 result.push(CHAR_SPACE);
734 result.push_str(ARROW_FAT);
735 position += 2;
736 while position < len && (chars[position] == CHAR_SPACE || chars[position] == CHAR_TAB) {
737 position += 1;
738 }
739 result.push(CHAR_SPACE);
740 continue;
741 }
742 if chars[position] == CHAR_BRACE_LEFT {
743 let (inner, end) = extract_brace_content(&chars, position);
744 let trimmed_inner: &str = inner.trim();
745 if trimmed_inner.is_empty() {
746 result.push(CHAR_BRACE_LEFT);
747 result.push(CHAR_BRACE_RIGHT);
748 } else {
749 let formatted_inner: String = format_macro_body_raw(trimmed_inner);
750 result.push(CHAR_BRACE_LEFT);
751 result.push(CHAR_NEWLINE);
752 result.push_str(&formatted_inner);
753 result.push(CHAR_NEWLINE);
754 result.push(CHAR_BRACE_RIGHT);
755 }
756 position = end;
757 continue;
758 }
759 if is_ident_char(chars[position]) {
760 let start: usize = position;
761 while position < len && is_ident_char(chars[position]) {
762 position += 1;
763 }
764 let ident: String = chars[start..position].iter().collect();
765 result.push_str(&ident);
766 let ws_start: usize = position;
767 while position < len && (chars[position] == CHAR_SPACE || chars[position] == CHAR_TAB) {
768 position += 1;
769 }
770 let had_whitespace: bool = position > ws_start;
771 if position < len && chars[position] == CHAR_BRACE_LEFT {
772 result.push(CHAR_SPACE);
773 } else if had_whitespace {
774 let next_pos: usize = position;
775 if next_pos < len && is_ident_char(chars[next_pos]) {
776 let mut ident_end: usize = next_pos;
777 while ident_end < len && is_ident_char(chars[ident_end]) {
778 ident_end += 1;
779 }
780 let after_ident: usize = skip_spaces_on_same_line(&chars, ident_end, len);
781 if after_ident < len
782 && chars[after_ident] == CHAR_COLON
783 && (after_ident + 1 >= len || chars[after_ident + 1] != CHAR_COLON)
784 {
785 result.push(CHAR_NEWLINE);
786 continue;
787 }
788 }
789 let ws: String = chars[ws_start..position].iter().collect();
790 result.push_str(&ws);
791 }
792 continue;
793 }
794 result.push(chars[position]);
795 position += 1;
796 }
797 result
798}
799
800fn add_indentation(body: &str) -> String {
813 let chars: Vec<char> = body.chars().collect();
814 let len: usize = chars.len();
815 let mut result: String = String::new();
816 let mut depth: i32 = 0;
817 let mut index: usize = 0;
818 while index < len {
819 if chars[index] == CHAR_DOUBLE_QUOTE || chars[index] == CHAR_SINGLE_QUOTE {
820 let quote: char = chars[index];
821 result.push(chars[index]);
822 index += 1;
823 while index < len {
824 if chars[index] == CHAR_SLASH_BACK && index + 1 < len {
825 result.push(chars[index]);
826 result.push(chars[index + 1]);
827 index += 2;
828 continue;
829 }
830 result.push(chars[index]);
831 if chars[index] == quote {
832 index += 1;
833 break;
834 }
835 index += 1;
836 }
837 continue;
838 }
839 if index + 1 < len
840 && chars[index] == CHAR_SLASH_FORWARD
841 && chars[index + 1] == CHAR_SLASH_FORWARD
842 {
843 while index < len && chars[index] != CHAR_NEWLINE {
844 result.push(chars[index]);
845 index += 1;
846 }
847 continue;
848 }
849 if index + 1 < len
850 && chars[index] == CHAR_SLASH_FORWARD
851 && chars[index + 1] == CHAR_ASTERISK
852 {
853 result.push(chars[index]);
854 result.push(chars[index + 1]);
855 index += 2;
856 while index + 1 < len {
857 if chars[index] == CHAR_ASTERISK && chars[index + 1] == CHAR_SLASH_FORWARD {
858 result.push(chars[index]);
859 result.push(chars[index + 1]);
860 index += 2;
861 break;
862 }
863 result.push(chars[index]);
864 index += 1;
865 }
866 continue;
867 }
868 if chars[index] == CHAR_NEWLINE {
869 result.push(CHAR_NEWLINE);
870 index += 1;
871 while index < len && (chars[index] == CHAR_SPACE || chars[index] == CHAR_TAB) {
872 index += 1;
873 }
874 if index >= len || chars[index] == CHAR_NEWLINE || chars[index] == CHAR_CARRIAGE_RETURN
875 {
876 continue;
877 }
878 let mut closing_count: i32 = 0;
879 let mut peek: usize = index;
880 while peek < len && chars[peek] == CHAR_BRACE_RIGHT {
881 closing_count += 1;
882 peek += 1;
883 }
884 let indent_depth: i32 = (depth - closing_count).max(0);
885 for _ in 0..indent_depth * 4 {
886 result.push(CHAR_SPACE);
887 }
888 continue;
889 }
890 if chars[index] == CHAR_BRACE_LEFT {
891 depth += 1;
892 result.push(CHAR_BRACE_LEFT);
893 index += 1;
894 continue;
895 }
896 if chars[index] == CHAR_BRACE_RIGHT {
897 depth -= 1;
898 result.push(CHAR_BRACE_RIGHT);
899 index += 1;
900 let mut peek: usize = index;
901 while peek < len && (chars[peek] == CHAR_SPACE || chars[peek] == CHAR_TAB) {
902 peek += 1;
903 }
904 if peek < len
905 && chars[peek] != CHAR_BRACE_RIGHT
906 && chars[peek] != CHAR_BRACE_LEFT
907 && chars[peek] != CHAR_NEWLINE
908 && chars[peek] != CHAR_CARRIAGE_RETURN
909 && chars[peek] != CHAR_RIGHT_PAREN
910 && chars[peek] != CHAR_COMMA
911 && chars[peek] != CHAR_SEMICOLON
912 {
913 let next_chars: String = chars[peek..(peek + 4).min(len)].iter().collect();
914 if next_chars != "else" {
915 result.push(CHAR_NEWLINE);
916 let indent_depth: i32 = depth.max(0);
917 for _ in 0..indent_depth * 4 {
918 result.push(CHAR_SPACE);
919 }
920 index = peek;
921 }
922 }
923 continue;
924 }
925 result.push(chars[index]);
926 index += 1;
927 }
928
929 result
930}
931
932fn is_if_keyword(chars: &[char], pos: usize, len: usize) -> bool {
944 if pos + 2 > len {
945 return false;
946 }
947 chars[pos] == CHAR_LETTER_I
948 && chars[pos + 1] == CHAR_LETTER_F
949 && (pos + 2 >= len || !is_ident_char(chars[pos + 2]))
950 && (pos == 0 || !is_ident_char(chars[pos - 1]))
951 && !is_raw_prefix(chars, pos)
952}
953
954fn is_else_keyword(chars: &[char], pos: usize, len: usize) -> bool {
966 if pos + 4 > len {
967 return false;
968 }
969 chars[pos] == CHAR_LETTER_E
970 && chars[pos + 1] == CHAR_LETTER_L
971 && chars[pos + 2] == CHAR_LETTER_S
972 && chars[pos + 3] == CHAR_LETTER_E
973 && (pos + 4 >= len || !is_ident_char(chars[pos + 4]))
974 && (pos == 0 || !is_ident_char(chars[pos - 1]))
975 && !is_raw_prefix(chars, pos)
976}
977
978fn is_match_keyword(chars: &[char], pos: usize, len: usize) -> bool {
990 if pos + 5 > len {
991 return false;
992 }
993 chars[pos] == CHAR_LETTER_M
994 && chars[pos + 1] == CHAR_LETTER_A
995 && chars[pos + 2] == CHAR_LETTER_T
996 && chars[pos + 3] == CHAR_LETTER_C
997 && chars[pos + 4] == CHAR_LETTER_H
998 && (pos + 5 >= len || !is_ident_char(chars[pos + 5]))
999 && (pos == 0 || !is_ident_char(chars[pos - 1]))
1000 && !is_raw_prefix(chars, pos)
1001}
1002
1003fn is_for_keyword(chars: &[char], pos: usize, len: usize) -> bool {
1015 if pos + 3 > len {
1016 return false;
1017 }
1018 chars[pos] == CHAR_LETTER_F
1019 && chars[pos + 1] == CHAR_LETTER_O
1020 && chars[pos + 2] == CHAR_LETTER_R
1021 && (pos + 3 >= len || !is_ident_char(chars[pos + 3]))
1022 && (pos == 0 || !is_ident_char(chars[pos - 1]))
1023 && !is_raw_prefix(chars, pos)
1024}
1025
1026fn is_in_keyword(chars: &[char], pos: usize, len: usize) -> bool {
1038 if pos + 2 > len {
1039 return false;
1040 }
1041 chars[pos] == CHAR_LETTER_I
1042 && chars[pos + 1] == CHAR_LETTER_N
1043 && (pos + 2 >= len || !is_ident_char(chars[pos + 2]))
1044 && (pos == 0 || !is_ident_char(chars[pos - 1]))
1045 && !is_raw_prefix(chars, pos)
1046}
1047
1048fn format_brace_block(block: &str) -> String {
1062 let block_chars: Vec<char> = block.chars().collect();
1063 if block_chars.len() < 2
1064 || block_chars[0] != CHAR_BRACE_LEFT
1065 || *block_chars.last().unwrap() != CHAR_BRACE_RIGHT
1066 {
1067 return block.to_string();
1068 }
1069 let inner: String = block_chars[1..block_chars.len() - 1].iter().collect();
1070 if inner.contains(CHAR_NEWLINE) {
1071 return block.to_string();
1072 }
1073 let trimmed_inner: &str = inner.trim();
1074 if trimmed_inner.is_empty() {
1075 let mut empty_result: String = String::new();
1076 empty_result.push(CHAR_BRACE_LEFT);
1077 empty_result.push(CHAR_BRACE_RIGHT);
1078 return empty_result;
1079 }
1080 let mut formatted: String = String::new();
1081 formatted.push(CHAR_BRACE_LEFT);
1082 formatted.push(CHAR_SPACE);
1083 formatted.push_str(trimmed_inner);
1084 formatted.push(CHAR_SPACE);
1085 formatted.push(CHAR_BRACE_RIGHT);
1086 formatted
1087}
1088
1089fn skip_spaces_on_same_line(chars: &[char], mut pos: usize, len: usize) -> usize {
1101 while pos < len && (chars[pos] == CHAR_SPACE || chars[pos] == CHAR_TAB) {
1102 pos += 1;
1103 }
1104 pos
1105}
1106
1107fn is_pseudo_selector_after_colon(chars: &[char], mut pos: usize, len: usize) -> bool {
1132 if pos >= len || !is_ident_char(chars[pos]) {
1133 return false;
1134 }
1135 let ident_start: usize = pos;
1136 while pos < len && is_ident_char(chars[pos]) {
1137 pos += 1;
1138 }
1139 let first_ident: String = chars[ident_start..pos].iter().collect();
1140 if is_rust_keyword(&first_ident) {
1141 return false;
1142 }
1143 while pos < len && chars[pos] == CHAR_HYPHEN && pos + 1 < len && is_ident_char(chars[pos + 1]) {
1144 pos += 1;
1145 while pos < len && is_ident_char(chars[pos]) {
1146 pos += 1;
1147 }
1148 }
1149 let after_ident: usize = skip_spaces_on_same_line(chars, pos, len);
1150 if after_ident < len && chars[after_ident] == CHAR_BRACE_LEFT {
1151 return true;
1152 }
1153 if after_ident < len && chars[after_ident] == CHAR_LEFT_PAREN {
1154 let mut depth: i32 = 0;
1155 let mut paren_pos: usize = after_ident;
1156 while paren_pos < len {
1157 if chars[paren_pos] == CHAR_LEFT_PAREN {
1158 depth += 1;
1159 } else if chars[paren_pos] == CHAR_RIGHT_PAREN {
1160 depth -= 1;
1161 if depth == 0 {
1162 let after_paren: usize = skip_spaces_on_same_line(chars, paren_pos + 1, len);
1163 return after_paren < len && chars[after_paren] == CHAR_BRACE_LEFT;
1164 }
1165 }
1166 paren_pos += 1;
1167 }
1168 }
1169 false
1170}
1171
1172fn is_rust_keyword(ident: &str) -> bool {
1185 matches!(ident, KEYWORD_IF | KEYWORD_MATCH | KEYWORD_FOR)
1186}
1187
1188fn is_raw_ident_before(result: &str, ident: &str) -> bool {
1202 result
1203 .trim_end()
1204 .ends_with(&format!("{RAW_IDENT_PREFIX}{ident}"))
1205}
1206
1207fn find_ident_before(result: &str) -> String {
1217 let chars: Vec<char> = result.chars().collect();
1218 let mut end: usize = chars.len();
1219 while end > 0 && chars[end - 1] == CHAR_SPACE {
1220 end -= 1;
1221 }
1222 let mut start: usize = end;
1223 while start > 0 && is_ident_char(chars[start - 1]) {
1224 start -= 1;
1225 }
1226 if start < end {
1227 chars[start..end].iter().collect()
1228 } else {
1229 String::new()
1230 }
1231}
1232
1233fn remove_trailing_spaces(result: &str, prefix_len: usize) -> String {
1244 let chars: Vec<char> = result.chars().collect();
1245 let total_len: usize = chars.len();
1246 let mut end: usize = total_len;
1247 while end > 0 && chars[end - 1] == CHAR_SPACE {
1248 end -= 1;
1249 }
1250 if prefix_len > end {
1251 return result.to_string();
1252 }
1253 let new_end: usize = end - prefix_len;
1254 chars[..new_end].iter().collect()
1255}
1256
1257fn find_trailing_spaces(result: &str) -> String {
1267 let mut spaces: String = String::new();
1268 for ch in result.chars().rev() {
1269 if ch == CHAR_SPACE || ch == CHAR_TAB {
1270 spaces.push(ch);
1271 } else {
1272 break;
1273 }
1274 }
1275 spaces.chars().rev().collect()
1276}
1277
1278pub async fn format_dir(path: &Path, mode: FmtMode) -> Result<(), EuvError> {
1292 if path.is_file() {
1293 let changed: bool = format_file(path, &mode).await?;
1294 match mode {
1295 FmtMode::Check => {
1296 if changed {
1297 return Err(EuvError::Message(format!(
1298 "{} needs formatting.",
1299 path.display()
1300 )));
1301 }
1302 log::info!("{} is properly formatted.", path.display());
1303 }
1304 FmtMode::Write => {
1305 if changed {
1306 log::info!("Formatted: {}", path.display());
1307 } else {
1308 log::info!("Already formatted: {}", path.display());
1309 }
1310 }
1311 }
1312 return Ok(());
1313 }
1314 let mut entries: Vec<PathBuf> = collect_rs_files(path).await?;
1315 entries.sort();
1316 let mut changed_count: usize = 0;
1317 let mut unchanged_count: usize = 0;
1318 for entry in entries {
1319 match format_file(&entry, &mode).await {
1320 Ok(changed) => {
1321 if changed {
1322 changed_count += 1;
1323 } else {
1324 unchanged_count += 1;
1325 }
1326 }
1327 Err(error) => {
1328 log::warn!("Failed to format {}: {error}", entry.display());
1329 }
1330 }
1331 }
1332 match mode {
1333 FmtMode::Check => {
1334 if changed_count > 0 {
1335 return Err(EuvError::Message(format!(
1336 "{} file(s) need formatting. Run `euv fmt` to fix.",
1337 changed_count
1338 )));
1339 }
1340 log::info!("All {} file(s) are properly formatted.", unchanged_count);
1341 }
1342 FmtMode::Write => {
1343 log::info!(
1344 "Formatted {} file(s), {} unchanged.",
1345 changed_count,
1346 unchanged_count
1347 );
1348 }
1349 }
1350 Ok(())
1351}
1352
1353async fn collect_rs_files(path: &Path) -> Result<Vec<PathBuf>, EuvError> {
1363 let mut result: Vec<PathBuf> = Vec::new();
1364 let mut stack: Vec<PathBuf> = vec![path.to_path_buf()];
1365 while let Some(dir) = stack.pop() {
1366 let mut entries: ReadDir =
1367 read_dir(&dir)
1368 .await
1369 .map_err(|error: io::Error| EuvError::IoPath {
1370 message: String::from("Failed to read directory"),
1371 path: dir.clone(),
1372 error,
1373 })?;
1374 while let Some(entry) =
1375 entries
1376 .next_entry()
1377 .await
1378 .map_err(|error: io::Error| EuvError::IoPath {
1379 message: String::from("Failed to read entry in directory"),
1380 path: dir.clone(),
1381 error,
1382 })?
1383 {
1384 let entry_path: PathBuf = entry.path();
1385 if entry_path.is_dir() {
1386 let file_name: String = entry_path
1387 .file_name()
1388 .unwrap_or_default()
1389 .to_string_lossy()
1390 .to_string();
1391 if file_name != TARGET_DIR_NAME && file_name != NODE_MODULES_DIR_NAME {
1392 stack.push(entry_path);
1393 }
1394 } else if entry_path
1395 .extension()
1396 .is_some_and(|ext: &std::ffi::OsStr| ext == RS_EXTENSION)
1397 {
1398 result.push(entry_path);
1399 }
1400 }
1401 }
1402 Ok(result)
1403}
1404
1405async fn format_file(path: &Path, mode: &FmtMode) -> Result<bool, EuvError> {
1416 let content: String =
1417 read_to_string(path)
1418 .await
1419 .map_err(|error: io::Error| EuvError::IoPath {
1420 message: String::from("Failed to read"),
1421 path: path.to_path_buf(),
1422 error,
1423 })?;
1424 let fmt_result: FmtResult = format_source(&content);
1425 if fmt_result.get_changed() {
1426 match mode {
1427 FmtMode::Write => {
1428 write(path, fmt_result.get_output())
1429 .await
1430 .map_err(|error: io::Error| EuvError::IoPath {
1431 message: String::from("Failed to write"),
1432 path: path.to_path_buf(),
1433 error,
1434 })?;
1435 log::info!("Formatted: {}", path.display());
1436 }
1437 FmtMode::Check => {
1438 log::warn!("Needs formatting: {}", path.display());
1439 }
1440 }
1441 }
1442 Ok(fmt_result.get_changed())
1443}