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(source: &str) -> String {
42 let mut result: String = String::new();
43 let chars: Vec<char> = source.chars().collect();
44 let len: usize = chars.len();
45 let mut position: usize = 0;
46 while position < len {
47 if is_euv_macro_start(&chars, position, len) {
48 let macro_name_end: usize = find_macro_name_end(&chars, position, len);
49 let name: String = chars[position..macro_name_end].iter().collect::<String>();
50 result.push_str(&name);
51 position = macro_name_end;
52 position = skip_whitespace_and_comments(&chars, position, len, &mut result);
53 if position < len && chars[position] == CHAR_MACRO_BANG {
54 result.push(CHAR_MACRO_BANG);
55 position += 1;
56 position = skip_whitespace_and_comments(&chars, position, len, &mut result);
57 if position < len && chars[position] == CHAR_BRACE_LEFT {
58 if !result.ends_with(CHAR_SPACE) {
59 result.push(CHAR_SPACE);
60 }
61 let (body_content, end_pos) = extract_brace_content(&chars, position);
62 let formatted_body: String = format_macro_body(&body_content);
63 if formatted_body.trim().is_empty() {
64 result.push(CHAR_BRACE_LEFT);
65 result.push(CHAR_BRACE_RIGHT);
66 } else {
67 let trimmed_body: &str = formatted_body.trim();
68 let last_newline_pos: usize =
69 result.rfind(CHAR_NEWLINE).map(|i| i + 1).unwrap_or(0);
70 let macro_indent: usize = result[last_newline_pos..]
71 .chars()
72 .take_while(|&c| c == CHAR_SPACE)
73 .count();
74 let min_indent: usize = body_content
75 .lines()
76 .filter(|line| !line.trim().is_empty())
77 .map(|line| line.chars().take_while(|c| c.is_whitespace()).count())
78 .min()
79 .unwrap_or(0);
80 let base_indent: usize = if min_indent > 0 {
81 min_indent
82 } else {
83 macro_indent + 4
84 };
85 let indent_str: String = " ".repeat(base_indent);
86 let indented_body: String = trimmed_body
87 .lines()
88 .map(|line| {
89 if line.trim().is_empty() {
90 line.to_string()
91 } else {
92 format!("{}{}", indent_str, line)
93 }
94 })
95 .collect::<Vec<String>>()
96 .join("\n");
97 let outer_indent_str: String = " ".repeat(macro_indent);
98 result.push(CHAR_BRACE_LEFT);
99 result.push(CHAR_NEWLINE);
100 result.push_str(&indented_body);
101 result.push(CHAR_NEWLINE);
102 result.push_str(&outer_indent_str);
103 result.push(CHAR_BRACE_RIGHT);
104 }
105 position = end_pos;
106 continue;
107 }
108 }
109 continue;
110 }
111 if chars[position] == CHAR_DOUBLE_QUOTE || chars[position] == CHAR_SINGLE_QUOTE {
112 let (literal, end_pos) = extract_string_literal(&chars, position, len);
113 result.push_str(&literal);
114 position = end_pos;
115 continue;
116 }
117 if position + 1 < len
118 && chars[position] == CHAR_SLASH_FORWARD
119 && chars[position + 1] == CHAR_SLASH_FORWARD
120 {
121 let (comment, end_pos) = extract_line_comment(&chars, position, len);
122 result.push_str(&comment);
123 position = end_pos;
124 continue;
125 }
126 if position + 1 < len
127 && chars[position] == CHAR_SLASH_FORWARD
128 && chars[position + 1] == CHAR_ASTERISK
129 {
130 let (comment, end_pos) = extract_block_comment(&chars, position, len);
131 result.push_str(&comment);
132 position = end_pos;
133 continue;
134 }
135 result.push(chars[position]);
136 position += 1;
137 }
138 result
139}
140
141fn is_euv_macro_start(chars: &[char], pos: usize, len: usize) -> bool {
153 for name in EUV_MACRO_NAMES {
154 let name_len: usize = name.len();
155 if pos + name_len > len {
156 continue;
157 }
158 let candidate: String = chars[pos..pos + name_len].iter().collect::<String>();
159 if candidate != *name {
160 continue;
161 }
162 if pos > 0 && is_ident_char(chars[pos - 1]) {
163 continue;
164 }
165 if pos + name_len < len && is_ident_char(chars[pos + name_len]) {
166 continue;
167 }
168 return true;
169 }
170 false
171}
172
173fn find_macro_name_end(chars: &[char], pos: usize, _len: usize) -> usize {
185 let mut end: usize = pos;
186 while end < chars.len() && is_ident_char(chars[end]) {
187 end += 1;
188 }
189 end
190}
191
192fn is_raw_prefix(chars: &[char], pos: usize) -> bool {
204 if pos < 2 {
205 return false;
206 }
207 chars[pos - 2] == CHAR_LETTER_R && chars[pos - 1] == CHAR_HASH
208}
209
210fn is_ident_char(character: char) -> bool {
220 character.is_alphanumeric() || character == CHAR_UNDERSCORE
221}
222
223fn skip_whitespace_and_comments(
236 chars: &[char],
237 mut pos: usize,
238 len: usize,
239 result: &mut String,
240) -> usize {
241 while pos < len {
242 if chars[pos].is_whitespace() {
243 result.push(chars[pos]);
244 pos += 1;
245 } else if pos + 1 < len
246 && chars[pos] == CHAR_SLASH_FORWARD
247 && chars[pos + 1] == CHAR_SLASH_FORWARD
248 {
249 let (comment, end_pos) = extract_line_comment(chars, pos, len);
250 result.push_str(&comment);
251 pos = end_pos;
252 } else if pos + 1 < len
253 && chars[pos] == CHAR_SLASH_FORWARD
254 && chars[pos + 1] == CHAR_ASTERISK
255 {
256 let (comment, end_pos) = extract_block_comment(chars, pos, len);
257 result.push_str(&comment);
258 pos = end_pos;
259 } else {
260 break;
261 }
262 }
263 pos
264}
265
266fn extract_brace_block(chars: &[char], start: usize) -> (String, usize) {
280 let mut depth: i32 = 0;
281 let mut position: usize = start;
282 while position < chars.len() {
283 if chars[position] == CHAR_DOUBLE_QUOTE || chars[position] == CHAR_SINGLE_QUOTE {
284 let (_, end) = extract_string_literal(chars, position, chars.len());
285 position = end;
286 continue;
287 }
288 if position + 1 < chars.len()
289 && chars[position] == CHAR_SLASH_FORWARD
290 && chars[position + 1] == CHAR_SLASH_FORWARD
291 {
292 let (_, end) = extract_line_comment(chars, position, chars.len());
293 position = end;
294 continue;
295 }
296 if position + 1 < chars.len()
297 && chars[position] == CHAR_SLASH_FORWARD
298 && chars[position + 1] == CHAR_ASTERISK
299 {
300 let (_, end) = extract_block_comment(chars, position, chars.len());
301 position = end;
302 continue;
303 }
304 if chars[position] == CHAR_BRACE_LEFT {
305 depth += 1;
306 } else if chars[position] == CHAR_BRACE_RIGHT {
307 depth -= 1;
308 if depth == 0 {
309 let content: String = chars[start..=position].iter().collect();
310 return (content, position + 1);
311 }
312 }
313 position += 1;
314 }
315 let content: String = chars[start..].iter().collect();
316 (content, chars.len())
317}
318
319fn extract_brace_content(chars: &[char], start: usize) -> (String, usize) {
333 let mut depth: i32 = 0;
334 let mut position: usize = start;
335 let mut content_start: usize = start + 1;
336 while position < chars.len() {
337 if chars[position] == CHAR_DOUBLE_QUOTE || chars[position] == CHAR_SINGLE_QUOTE {
338 let (_, end) = extract_string_literal(chars, position, chars.len());
339 position = end;
340 continue;
341 }
342 if position + 1 < chars.len()
343 && chars[position] == CHAR_SLASH_FORWARD
344 && chars[position + 1] == CHAR_SLASH_FORWARD
345 {
346 let (_, end) = extract_line_comment(chars, position, chars.len());
347 position = end;
348 continue;
349 }
350 if position + 1 < chars.len()
351 && chars[position] == CHAR_SLASH_FORWARD
352 && chars[position + 1] == CHAR_ASTERISK
353 {
354 let (_, end) = extract_block_comment(chars, position, chars.len());
355 position = end;
356 continue;
357 }
358 if chars[position] == CHAR_BRACE_LEFT {
359 if depth == 0 {
360 content_start = position + 1;
361 }
362 depth += 1;
363 } else if chars[position] == CHAR_BRACE_RIGHT {
364 depth -= 1;
365 if depth == 0 {
366 let content: String = chars[content_start..position].iter().collect();
367 return (content, position + 1);
368 }
369 }
370 position += 1;
371 }
372 let content: String = chars[content_start..].iter().collect();
373 (content, chars.len())
374}
375
376fn extract_string_literal(chars: &[char], start: usize, len: usize) -> (String, usize) {
390 let quote: char = chars[start];
391 let mut position: usize = start + 1;
392 let mut result: String = String::new();
393 result.push(quote);
394 while position < len {
395 if chars[position] == CHAR_SLASH_BACK && position + 1 < len {
396 result.push(chars[position]);
397 result.push(chars[position + 1]);
398 position += 2;
399 continue;
400 }
401 result.push(chars[position]);
402 if chars[position] == quote {
403 return (result, position + 1);
404 }
405 position += 1;
406 }
407 (result, position)
408}
409
410fn extract_line_comment(chars: &[char], start: usize, len: usize) -> (String, usize) {
422 let mut position: usize = start;
423 let mut result: String = String::new();
424 while position < len && chars[position] != CHAR_NEWLINE {
425 result.push(chars[position]);
426 position += 1;
427 }
428 if position < len {
429 result.push(CHAR_NEWLINE);
430 position += 1;
431 }
432 (result, position)
433}
434
435fn extract_block_comment(chars: &[char], start: usize, len: usize) -> (String, usize) {
447 let mut position: usize = start + 2;
448 let mut result: String = String::from(BLOCK_COMMENT_START);
449 while position + 1 < len {
450 result.push(chars[position]);
451 if chars[position] == CHAR_ASTERISK && chars[position + 1] == CHAR_SLASH_FORWARD {
452 result.push(CHAR_SLASH_FORWARD);
453 return (result, position + 2);
454 }
455 position += 1;
456 }
457 while position < len {
458 result.push(chars[position]);
459 position += 1;
460 }
461 (result, position)
462}
463
464pub fn format_macro_body(body: &str) -> String {
465 let raw: String = format_macro_body_raw(body);
466 add_indentation(&raw)
467}
468
469fn format_macro_body_raw(body: &str) -> String {
489 let chars: Vec<char> = body.chars().collect();
490 let len: usize = chars.len();
491 let mut result: String = String::new();
492 let mut position: usize = 0;
493 while position < len {
494 if chars[position] == CHAR_DOUBLE_QUOTE || chars[position] == CHAR_SINGLE_QUOTE {
495 let (literal, end) = extract_string_literal(&chars, position, len);
496 result.push_str(&literal);
497 position = end;
498 continue;
499 }
500 if position + 1 < len
501 && chars[position] == CHAR_SLASH_FORWARD
502 && chars[position + 1] == CHAR_SLASH_FORWARD
503 {
504 let (comment, end) = extract_line_comment(&chars, position, len);
505 result.push_str(&comment);
506 position = end;
507 continue;
508 }
509 if position + 1 < len
510 && chars[position] == CHAR_SLASH_FORWARD
511 && chars[position + 1] == CHAR_ASTERISK
512 {
513 let (comment, end) = extract_block_comment(&chars, position, len);
514 result.push_str(&comment);
515 position = end;
516 continue;
517 }
518 if is_if_keyword(&chars, position, len) {
519 let after_if_colon: usize = skip_spaces_on_same_line(&chars, position + 2, len);
520 if after_if_colon < len
521 && chars[after_if_colon] == CHAR_COLON
522 && (after_if_colon + 1 >= len || chars[after_if_colon + 1] != CHAR_COLON)
523 {
524 result.push_str(KEYWORD_IF);
525 position += 2;
526 if position < len && chars[position] == CHAR_SPACE {
527 result.push(CHAR_SPACE);
528 position += 1;
529 }
530 continue;
531 }
532 result.push_str(KEYWORD_IF);
533 position += 2;
534 let after_if: usize = skip_spaces_on_same_line(&chars, position, len);
535 if position < len && chars[after_if] == CHAR_BRACE_LEFT {
536 result.push(CHAR_SPACE);
537 let (block, end) = extract_brace_block(&chars, after_if);
538 result.push_str(&format_brace_block(&block));
539 position = end;
540 position = skip_spaces_on_same_line(&chars, position, len);
541 if position < len && chars[position] == CHAR_BRACE_LEFT {
542 result.push(CHAR_SPACE);
543 }
544 } else {
545 result.push(CHAR_SPACE);
546 position = after_if;
547 }
548 continue;
549 }
550 if is_else_keyword(&chars, position, len) {
551 if !result.ends_with(CHAR_SPACE)
552 && !result.ends_with(CHAR_NEWLINE)
553 && !result.ends_with(CHAR_TAB)
554 {
555 result.push(CHAR_SPACE);
556 }
557 result.push_str(KEYWORD_ELSE);
558 position += 4;
559 position = skip_spaces_on_same_line(&chars, position, len);
560 if is_if_keyword(&chars, position, len) {
561 result.push(CHAR_SPACE);
562 continue;
563 }
564 if position < len && chars[position] == CHAR_BRACE_LEFT {
565 result.push(CHAR_SPACE);
566 }
567 continue;
568 }
569 if is_match_keyword(&chars, position, len) {
570 let after_match_colon: usize = skip_spaces_on_same_line(&chars, position + 5, len);
571 if after_match_colon < len
572 && chars[after_match_colon] == CHAR_COLON
573 && (after_match_colon + 1 >= len || chars[after_match_colon + 1] != CHAR_COLON)
574 {
575 result.push_str(KEYWORD_MATCH);
576 position += 5;
577 if position < len && chars[position] == CHAR_SPACE {
578 result.push(CHAR_SPACE);
579 position += 1;
580 }
581 continue;
582 }
583 result.push_str(KEYWORD_MATCH);
584 position += 5;
585 let after_match: usize = skip_spaces_on_same_line(&chars, position, len);
586 if after_match < len && chars[after_match] == CHAR_BRACE_LEFT {
587 result.push(CHAR_SPACE);
588 let (block, end) = extract_brace_block(&chars, after_match);
589 result.push_str(&format_brace_block(&block));
590 position = end;
591 position = skip_spaces_on_same_line(&chars, position, len);
592 if position < len && chars[position] == CHAR_BRACE_LEFT {
593 result.push(CHAR_SPACE);
594 }
595 } else {
596 result.push(CHAR_SPACE);
597 position = after_match;
598 }
599 continue;
600 }
601 if is_for_keyword(&chars, position, len) {
602 let after_for: usize = skip_spaces_on_same_line(&chars, position + 3, len);
603 if after_for < len
604 && chars[after_for] == CHAR_COLON
605 && (after_for + 1 >= len || chars[after_for + 1] != CHAR_COLON)
606 {
607 result.push_str(KEYWORD_FOR);
608 position += 3;
609 if position < len && chars[position] == CHAR_SPACE {
610 result.push(CHAR_SPACE);
611 position += 1;
612 }
613 continue;
614 }
615 result.push_str(KEYWORD_FOR);
616 position += 3;
617 position = skip_spaces_on_same_line(&chars, position, len);
618 if position < len && !is_in_keyword(&chars, position, len) {
619 result.push(CHAR_SPACE);
620 }
621 while position < len && !is_in_keyword(&chars, position, len) {
622 if chars[position] == CHAR_BRACE_LEFT {
623 let (block, end) = extract_brace_block(&chars, position);
624 result.push_str(&format_brace_block(&block));
625 position = end;
626 continue;
627 }
628 if chars[position] == CHAR_DOUBLE_QUOTE || chars[position] == CHAR_SINGLE_QUOTE {
629 let (literal, end) = extract_string_literal(&chars, position, len);
630 result.push_str(&literal);
631 position = end;
632 continue;
633 }
634 result.push(chars[position]);
635 position += 1;
636 }
637 if result.ends_with(CHAR_SPACE) {
638 result.truncate(result.len() - 1);
639 }
640 position = skip_spaces_on_same_line(&chars, position, len);
641 if is_in_keyword(&chars, position, len) {
642 result.push(CHAR_SPACE);
643 result.push_str(KEYWORD_IN);
644 position += 2;
645 position = skip_spaces_on_same_line(&chars, position, len);
646 if position < len && chars[position] == CHAR_BRACE_LEFT {
647 result.push(CHAR_SPACE);
648 let (block, end) = extract_brace_block(&chars, position);
649 result.push_str(&format_brace_block(&block));
650 position = end;
651 position = skip_spaces_on_same_line(&chars, position, len);
652 if position < len && chars[position] == CHAR_BRACE_LEFT {
653 result.push(CHAR_SPACE);
654 }
655 } else {
656 result.push(CHAR_SPACE);
657 while position < len && chars[position] != CHAR_BRACE_LEFT {
658 result.push(chars[position]);
659 position += 1;
660 }
661 if result.ends_with(CHAR_SPACE) {
662 result.truncate(result.len() - 1);
663 }
664 if position < len && chars[position] == CHAR_BRACE_LEFT {
665 result.push(CHAR_SPACE);
666 }
667 }
668 }
669 continue;
670 }
671 if chars[position] == CHAR_COLON && position + 1 < len && chars[position + 1] != CHAR_COLON
672 {
673 if result.ends_with(CHAR_COLON) {
674 result.push(CHAR_COLON);
675 position += 1;
676 continue;
677 }
678 let colon_prefix: String = find_ident_before(&result);
679 if is_raw_ident_before(&result, &colon_prefix) {
680 result.push(CHAR_COLON);
681 position += 1;
682 continue;
683 }
684 if !colon_prefix.is_empty() {
685 let before_colon: String = remove_trailing_spaces(&result, colon_prefix.len());
686 result = before_colon;
687 result.push_str(&colon_prefix);
688 }
689 result.push(CHAR_COLON);
690 position += 1;
691 while position < len && (chars[position] == CHAR_SPACE || chars[position] == CHAR_TAB) {
692 position += 1;
693 }
694 if position < len
695 && chars[position] != CHAR_NEWLINE
696 && chars[position] != CHAR_CARRIAGE_RETURN
697 && !is_pseudo_selector_after_colon(&chars, position, len)
698 {
699 result.push(CHAR_SPACE);
700 }
701 continue;
702 }
703 if position + 1 < len
704 && chars[position] == CHAR_EQUALS
705 && chars[position + 1] == CHAR_GREATER_THAN
706 {
707 let trailing: String = find_trailing_spaces(&result);
708 if !trailing.is_empty() {
709 result.truncate(result.len() - trailing.len());
710 }
711 result.push(CHAR_SPACE);
712 result.push_str(ARROW_FAT);
713 position += 2;
714 while position < len && (chars[position] == CHAR_SPACE || chars[position] == CHAR_TAB) {
715 position += 1;
716 }
717 result.push(CHAR_SPACE);
718 continue;
719 }
720 if chars[position] == CHAR_BRACE_LEFT {
721 let (inner, end) = extract_brace_content(&chars, position);
722 let trimmed_inner: &str = inner.trim();
723 if trimmed_inner.is_empty() {
724 result.push(CHAR_BRACE_LEFT);
725 result.push(CHAR_BRACE_RIGHT);
726 } else {
727 let formatted_inner: String = format_macro_body_raw(trimmed_inner);
728 result.push(CHAR_BRACE_LEFT);
729 result.push(CHAR_NEWLINE);
730 result.push_str(&formatted_inner);
731 result.push(CHAR_NEWLINE);
732 result.push(CHAR_BRACE_RIGHT);
733 }
734 position = end;
735 continue;
736 }
737 if is_ident_char(chars[position]) {
738 let start: usize = position;
739 while position < len && is_ident_char(chars[position]) {
740 position += 1;
741 }
742 let ident: String = chars[start..position].iter().collect();
743 result.push_str(&ident);
744 let ws_start: usize = position;
745 while position < len && (chars[position] == CHAR_SPACE || chars[position] == CHAR_TAB) {
746 position += 1;
747 }
748 let had_whitespace: bool = position > ws_start;
749 if position < len && chars[position] == CHAR_BRACE_LEFT {
750 result.push(CHAR_SPACE);
751 } else if had_whitespace {
752 let next_pos: usize = position;
753 if next_pos < len && is_ident_char(chars[next_pos]) {
754 let mut ident_end: usize = next_pos;
755 while ident_end < len && is_ident_char(chars[ident_end]) {
756 ident_end += 1;
757 }
758 let after_ident: usize = skip_spaces_on_same_line(&chars, ident_end, len);
759 if after_ident < len
760 && chars[after_ident] == CHAR_COLON
761 && (after_ident + 1 >= len || chars[after_ident + 1] != CHAR_COLON)
762 {
763 result.push(CHAR_NEWLINE);
764 continue;
765 }
766 }
767 let ws: String = chars[ws_start..position].iter().collect();
768 result.push_str(&ws);
769 }
770 continue;
771 }
772 result.push(chars[position]);
773 position += 1;
774 }
775 result
776}
777
778fn add_indentation(body: &str) -> String {
791 let chars: Vec<char> = body.chars().collect();
792 let len: usize = chars.len();
793 let mut result: String = String::new();
794 let mut depth: i32 = 0;
795 let mut index: usize = 0;
796 while index < len {
797 if chars[index] == CHAR_DOUBLE_QUOTE || chars[index] == CHAR_SINGLE_QUOTE {
798 let quote: char = chars[index];
799 result.push(chars[index]);
800 index += 1;
801 while index < len {
802 if chars[index] == CHAR_SLASH_BACK && index + 1 < len {
803 result.push(chars[index]);
804 result.push(chars[index + 1]);
805 index += 2;
806 continue;
807 }
808 result.push(chars[index]);
809 if chars[index] == quote {
810 index += 1;
811 break;
812 }
813 index += 1;
814 }
815 continue;
816 }
817 if index + 1 < len
818 && chars[index] == CHAR_SLASH_FORWARD
819 && chars[index + 1] == CHAR_SLASH_FORWARD
820 {
821 while index < len && chars[index] != CHAR_NEWLINE {
822 result.push(chars[index]);
823 index += 1;
824 }
825 continue;
826 }
827 if index + 1 < len
828 && chars[index] == CHAR_SLASH_FORWARD
829 && chars[index + 1] == CHAR_ASTERISK
830 {
831 result.push(chars[index]);
832 result.push(chars[index + 1]);
833 index += 2;
834 while index + 1 < len {
835 if chars[index] == CHAR_ASTERISK && chars[index + 1] == CHAR_SLASH_FORWARD {
836 result.push(chars[index]);
837 result.push(chars[index + 1]);
838 index += 2;
839 break;
840 }
841 result.push(chars[index]);
842 index += 1;
843 }
844 continue;
845 }
846 if chars[index] == CHAR_NEWLINE {
847 result.push(CHAR_NEWLINE);
848 index += 1;
849 while index < len && (chars[index] == CHAR_SPACE || chars[index] == CHAR_TAB) {
850 index += 1;
851 }
852 if index >= len || chars[index] == CHAR_NEWLINE || chars[index] == CHAR_CARRIAGE_RETURN
853 {
854 continue;
855 }
856 let mut closing_count: i32 = 0;
857 let mut peek: usize = index;
858 while peek < len && chars[peek] == CHAR_BRACE_RIGHT {
859 closing_count += 1;
860 peek += 1;
861 }
862 let indent_depth: i32 = (depth - closing_count).max(0);
863 for _ in 0..indent_depth * 4 {
864 result.push(CHAR_SPACE);
865 }
866 continue;
867 }
868 if chars[index] == CHAR_BRACE_LEFT {
869 depth += 1;
870 result.push(CHAR_BRACE_LEFT);
871 index += 1;
872 continue;
873 }
874 if chars[index] == CHAR_BRACE_RIGHT {
875 depth -= 1;
876 result.push(CHAR_BRACE_RIGHT);
877 index += 1;
878 let mut peek: usize = index;
879 while peek < len && (chars[peek] == CHAR_SPACE || chars[peek] == CHAR_TAB) {
880 peek += 1;
881 }
882 if peek < len
883 && chars[peek] != CHAR_BRACE_RIGHT
884 && chars[peek] != CHAR_BRACE_LEFT
885 && chars[peek] != CHAR_NEWLINE
886 && chars[peek] != CHAR_CARRIAGE_RETURN
887 && chars[peek] != CHAR_RIGHT_PAREN
888 && chars[peek] != CHAR_COMMA
889 && chars[peek] != CHAR_SEMICOLON
890 {
891 let next_chars: String = chars[peek..(peek + 4).min(len)].iter().collect();
892 if next_chars != "else" {
893 result.push(CHAR_NEWLINE);
894 let indent_depth: i32 = depth.max(0);
895 for _ in 0..indent_depth * 4 {
896 result.push(CHAR_SPACE);
897 }
898 index = peek;
899 }
900 }
901 continue;
902 }
903 result.push(chars[index]);
904 index += 1;
905 }
906
907 result
908}
909
910fn is_if_keyword(chars: &[char], pos: usize, len: usize) -> bool {
922 if pos + 2 > len {
923 return false;
924 }
925 chars[pos] == CHAR_LETTER_I
926 && chars[pos + 1] == CHAR_LETTER_F
927 && (pos + 2 >= len || !is_ident_char(chars[pos + 2]))
928 && (pos == 0 || !is_ident_char(chars[pos - 1]))
929 && !is_raw_prefix(chars, pos)
930}
931
932fn is_else_keyword(chars: &[char], pos: usize, len: usize) -> bool {
944 if pos + 4 > len {
945 return false;
946 }
947 chars[pos] == CHAR_LETTER_E
948 && chars[pos + 1] == CHAR_LETTER_L
949 && chars[pos + 2] == CHAR_LETTER_S
950 && chars[pos + 3] == CHAR_LETTER_E
951 && (pos + 4 >= len || !is_ident_char(chars[pos + 4]))
952 && (pos == 0 || !is_ident_char(chars[pos - 1]))
953 && !is_raw_prefix(chars, pos)
954}
955
956fn is_match_keyword(chars: &[char], pos: usize, len: usize) -> bool {
968 if pos + 5 > len {
969 return false;
970 }
971 chars[pos] == CHAR_LETTER_M
972 && chars[pos + 1] == CHAR_LETTER_A
973 && chars[pos + 2] == CHAR_LETTER_T
974 && chars[pos + 3] == CHAR_LETTER_C
975 && chars[pos + 4] == CHAR_LETTER_H
976 && (pos + 5 >= len || !is_ident_char(chars[pos + 5]))
977 && (pos == 0 || !is_ident_char(chars[pos - 1]))
978 && !is_raw_prefix(chars, pos)
979}
980
981fn is_for_keyword(chars: &[char], pos: usize, len: usize) -> bool {
993 if pos + 3 > len {
994 return false;
995 }
996 chars[pos] == CHAR_LETTER_F
997 && chars[pos + 1] == CHAR_LETTER_O
998 && chars[pos + 2] == CHAR_LETTER_R
999 && (pos + 3 >= len || !is_ident_char(chars[pos + 3]))
1000 && (pos == 0 || !is_ident_char(chars[pos - 1]))
1001 && !is_raw_prefix(chars, pos)
1002}
1003
1004fn is_in_keyword(chars: &[char], pos: usize, len: usize) -> bool {
1016 if pos + 2 > len {
1017 return false;
1018 }
1019 chars[pos] == CHAR_LETTER_I
1020 && chars[pos + 1] == CHAR_LETTER_N
1021 && (pos + 2 >= len || !is_ident_char(chars[pos + 2]))
1022 && (pos == 0 || !is_ident_char(chars[pos - 1]))
1023 && !is_raw_prefix(chars, pos)
1024}
1025
1026fn format_brace_block(block: &str) -> String {
1040 let block_chars: Vec<char> = block.chars().collect();
1041 if block_chars.len() < 2
1042 || block_chars[0] != CHAR_BRACE_LEFT
1043 || *block_chars.last().unwrap() != CHAR_BRACE_RIGHT
1044 {
1045 return block.to_string();
1046 }
1047 let inner: String = block_chars[1..block_chars.len() - 1].iter().collect();
1048 if inner.contains(CHAR_NEWLINE) {
1049 return block.to_string();
1050 }
1051 let trimmed_inner: &str = inner.trim();
1052 if trimmed_inner.is_empty() {
1053 let mut empty_result: String = String::new();
1054 empty_result.push(CHAR_BRACE_LEFT);
1055 empty_result.push(CHAR_BRACE_RIGHT);
1056 return empty_result;
1057 }
1058 let mut formatted: String = String::new();
1059 formatted.push(CHAR_BRACE_LEFT);
1060 formatted.push(CHAR_SPACE);
1061 formatted.push_str(trimmed_inner);
1062 formatted.push(CHAR_SPACE);
1063 formatted.push(CHAR_BRACE_RIGHT);
1064 formatted
1065}
1066
1067fn skip_spaces_on_same_line(chars: &[char], mut pos: usize, len: usize) -> usize {
1079 while pos < len && (chars[pos] == CHAR_SPACE || chars[pos] == CHAR_TAB) {
1080 pos += 1;
1081 }
1082 pos
1083}
1084
1085fn is_pseudo_selector_after_colon(chars: &[char], mut pos: usize, len: usize) -> bool {
1110 if pos >= len || !is_ident_char(chars[pos]) {
1111 return false;
1112 }
1113 let ident_start: usize = pos;
1114 while pos < len && is_ident_char(chars[pos]) {
1115 pos += 1;
1116 }
1117 let first_ident: String = chars[ident_start..pos].iter().collect();
1118 if is_rust_keyword(&first_ident) {
1119 return false;
1120 }
1121 while pos < len && chars[pos] == CHAR_HYPHEN && pos + 1 < len && is_ident_char(chars[pos + 1]) {
1122 pos += 1;
1123 while pos < len && is_ident_char(chars[pos]) {
1124 pos += 1;
1125 }
1126 }
1127 let after_ident: usize = skip_spaces_on_same_line(chars, pos, len);
1128 if after_ident < len && chars[after_ident] == CHAR_BRACE_LEFT {
1129 return true;
1130 }
1131 if after_ident < len && chars[after_ident] == CHAR_LEFT_PAREN {
1132 let mut depth: i32 = 0;
1133 let mut paren_pos: usize = after_ident;
1134 while paren_pos < len {
1135 if chars[paren_pos] == CHAR_LEFT_PAREN {
1136 depth += 1;
1137 } else if chars[paren_pos] == CHAR_RIGHT_PAREN {
1138 depth -= 1;
1139 if depth == 0 {
1140 let after_paren: usize = skip_spaces_on_same_line(chars, paren_pos + 1, len);
1141 return after_paren < len && chars[after_paren] == CHAR_BRACE_LEFT;
1142 }
1143 }
1144 paren_pos += 1;
1145 }
1146 }
1147 false
1148}
1149
1150fn is_rust_keyword(ident: &str) -> bool {
1163 matches!(ident, KEYWORD_IF | KEYWORD_MATCH | KEYWORD_FOR)
1164}
1165
1166fn is_raw_ident_before(result: &str, ident: &str) -> bool {
1180 result
1181 .trim_end()
1182 .ends_with(&format!("{RAW_IDENT_PREFIX}{ident}"))
1183}
1184
1185fn find_ident_before(result: &str) -> String {
1195 let chars: Vec<char> = result.chars().collect();
1196 let mut end: usize = chars.len();
1197 while end > 0 && chars[end - 1] == CHAR_SPACE {
1198 end -= 1;
1199 }
1200 let mut start: usize = end;
1201 while start > 0 && is_ident_char(chars[start - 1]) {
1202 start -= 1;
1203 }
1204 if start < end {
1205 chars[start..end].iter().collect()
1206 } else {
1207 String::new()
1208 }
1209}
1210
1211fn remove_trailing_spaces(result: &str, prefix_len: usize) -> String {
1222 let chars: Vec<char> = result.chars().collect();
1223 let total_len: usize = chars.len();
1224 let mut end: usize = total_len;
1225 while end > 0 && chars[end - 1] == CHAR_SPACE {
1226 end -= 1;
1227 }
1228 if prefix_len > end {
1229 return result.to_string();
1230 }
1231 let new_end: usize = end - prefix_len;
1232 chars[..new_end].iter().collect()
1233}
1234
1235fn find_trailing_spaces(result: &str) -> String {
1245 let mut spaces: String = String::new();
1246 for ch in result.chars().rev() {
1247 if ch == CHAR_SPACE || ch == CHAR_TAB {
1248 spaces.push(ch);
1249 } else {
1250 break;
1251 }
1252 }
1253 spaces.chars().rev().collect()
1254}
1255
1256pub async fn format_dir(path: &Path, mode: FmtMode) -> Result<(), EuvError> {
1270 if path.is_file() {
1271 let changed: bool = format_file(path, &mode).await?;
1272 match mode {
1273 FmtMode::Check => {
1274 if changed {
1275 return Err(EuvError::Message(format!(
1276 "{} needs formatting.",
1277 path.display()
1278 )));
1279 }
1280 log::info!("{} is properly formatted.", path.display());
1281 }
1282 FmtMode::Write => {
1283 if changed {
1284 log::info!("Formatted: {}", path.display());
1285 } else {
1286 log::info!("Already formatted: {}", path.display());
1287 }
1288 }
1289 }
1290 return Ok(());
1291 }
1292 let mut entries: Vec<PathBuf> = collect_rs_files(path).await?;
1293 entries.sort();
1294 let mut changed_count: usize = 0;
1295 let mut unchanged_count: usize = 0;
1296 for entry in entries {
1297 match format_file(&entry, &mode).await {
1298 Ok(changed) => {
1299 if changed {
1300 changed_count += 1;
1301 } else {
1302 unchanged_count += 1;
1303 }
1304 }
1305 Err(error) => {
1306 log::warn!("Failed to format {}: {error}", entry.display());
1307 }
1308 }
1309 }
1310 match mode {
1311 FmtMode::Check => {
1312 if changed_count > 0 {
1313 return Err(EuvError::Message(format!(
1314 "{} file(s) need formatting. Run `euv fmt` to fix.",
1315 changed_count
1316 )));
1317 }
1318 log::info!("All {} file(s) are properly formatted.", unchanged_count);
1319 }
1320 FmtMode::Write => {
1321 log::info!(
1322 "Formatted {} file(s), {} unchanged.",
1323 changed_count,
1324 unchanged_count
1325 );
1326 }
1327 }
1328 Ok(())
1329}
1330
1331async fn collect_rs_files(path: &Path) -> Result<Vec<PathBuf>, EuvError> {
1341 let mut result: Vec<PathBuf> = Vec::new();
1342 let mut stack: Vec<PathBuf> = vec![path.to_path_buf()];
1343 while let Some(dir) = stack.pop() {
1344 let mut entries: ReadDir =
1345 read_dir(&dir)
1346 .await
1347 .map_err(|error: io::Error| EuvError::IoPath {
1348 message: String::from("Failed to read directory"),
1349 path: dir.clone(),
1350 error,
1351 })?;
1352 while let Some(entry) =
1353 entries
1354 .next_entry()
1355 .await
1356 .map_err(|error: io::Error| EuvError::IoPath {
1357 message: String::from("Failed to read entry in directory"),
1358 path: dir.clone(),
1359 error,
1360 })?
1361 {
1362 let entry_path: PathBuf = entry.path();
1363 if entry_path.is_dir() {
1364 let file_name: String = entry_path
1365 .file_name()
1366 .unwrap_or_default()
1367 .to_string_lossy()
1368 .to_string();
1369 if file_name != TARGET_DIR_NAME && file_name != NODE_MODULES_DIR_NAME {
1370 stack.push(entry_path);
1371 }
1372 } else if entry_path
1373 .extension()
1374 .is_some_and(|ext: &std::ffi::OsStr| ext == RS_EXTENSION)
1375 {
1376 result.push(entry_path);
1377 }
1378 }
1379 }
1380 Ok(result)
1381}
1382
1383async fn format_file(path: &Path, mode: &FmtMode) -> Result<bool, EuvError> {
1394 let content: String =
1395 read_to_string(path)
1396 .await
1397 .map_err(|error: io::Error| EuvError::IoPath {
1398 message: String::from("Failed to read"),
1399 path: path.to_path_buf(),
1400 error,
1401 })?;
1402 let fmt_result: FmtResult = format_source(&content);
1403 if fmt_result.get_changed() {
1404 match mode {
1405 FmtMode::Write => {
1406 write(path, fmt_result.get_output())
1407 .await
1408 .map_err(|error: io::Error| EuvError::IoPath {
1409 message: String::from("Failed to write"),
1410 path: path.to_path_buf(),
1411 error,
1412 })?;
1413 log::info!("Formatted: {}", path.display());
1414 }
1415 FmtMode::Check => {
1416 log::warn!("Needs formatting: {}", path.display());
1417 }
1418 }
1419 }
1420 Ok(fmt_result.get_changed())
1421}