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 }
656 }
657 continue;
658 }
659 if chars[position] == CHAR_COLON && position + 1 < len && chars[position + 1] != CHAR_COLON
660 {
661 if result.ends_with(CHAR_COLON) {
662 result.push(CHAR_COLON);
663 position += 1;
664 continue;
665 }
666 let colon_prefix: String = find_ident_before(&result);
667 if is_raw_ident_before(&result, &colon_prefix) {
668 result.push(CHAR_COLON);
669 position += 1;
670 continue;
671 }
672 if !colon_prefix.is_empty() {
673 let before_colon: String = remove_trailing_spaces(&result, colon_prefix.len());
674 result = before_colon;
675 result.push_str(&colon_prefix);
676 }
677 result.push(CHAR_COLON);
678 position += 1;
679 while position < len && (chars[position] == CHAR_SPACE || chars[position] == CHAR_TAB) {
680 position += 1;
681 }
682 if position < len
683 && chars[position] != CHAR_NEWLINE
684 && chars[position] != CHAR_CARRIAGE_RETURN
685 && !is_pseudo_selector_after_colon(&chars, position, len)
686 {
687 result.push(CHAR_SPACE);
688 }
689 continue;
690 }
691 if position + 1 < len
692 && chars[position] == CHAR_EQUALS
693 && chars[position + 1] == CHAR_GREATER_THAN
694 {
695 let trailing: String = find_trailing_spaces(&result);
696 if !trailing.is_empty() {
697 result.truncate(result.len() - trailing.len());
698 }
699 result.push(CHAR_SPACE);
700 result.push_str(ARROW_FAT);
701 position += 2;
702 while position < len && (chars[position] == CHAR_SPACE || chars[position] == CHAR_TAB) {
703 position += 1;
704 }
705 result.push(CHAR_SPACE);
706 continue;
707 }
708 if chars[position] == CHAR_BRACE_LEFT {
709 let (inner, end) = extract_brace_content(&chars, position);
710 let trimmed_inner: &str = inner.trim();
711 if trimmed_inner.is_empty() {
712 result.push(CHAR_BRACE_LEFT);
713 result.push(CHAR_BRACE_RIGHT);
714 } else {
715 let formatted_inner: String = format_macro_body_raw(trimmed_inner);
716 result.push(CHAR_BRACE_LEFT);
717 result.push(CHAR_NEWLINE);
718 result.push_str(&formatted_inner);
719 result.push(CHAR_NEWLINE);
720 result.push(CHAR_BRACE_RIGHT);
721 }
722 position = end;
723 continue;
724 }
725 if is_ident_char(chars[position]) {
726 let start: usize = position;
727 while position < len && is_ident_char(chars[position]) {
728 position += 1;
729 }
730 let ident: String = chars[start..position].iter().collect();
731 result.push_str(&ident);
732 let ws_start: usize = position;
733 while position < len && (chars[position] == CHAR_SPACE || chars[position] == CHAR_TAB) {
734 position += 1;
735 }
736 let had_whitespace: bool = position > ws_start;
737 if position < len && chars[position] == CHAR_BRACE_LEFT {
738 result.push(CHAR_SPACE);
739 } else if had_whitespace {
740 let next_pos: usize = position;
741 if next_pos < len && is_ident_char(chars[next_pos]) {
742 let mut ident_end: usize = next_pos;
743 while ident_end < len && is_ident_char(chars[ident_end]) {
744 ident_end += 1;
745 }
746 let after_ident: usize = skip_spaces_on_same_line(&chars, ident_end, len);
747 if after_ident < len
748 && chars[after_ident] == CHAR_COLON
749 && (after_ident + 1 >= len || chars[after_ident + 1] != CHAR_COLON)
750 {
751 result.push(CHAR_NEWLINE);
752 continue;
753 }
754 }
755 let ws: String = chars[ws_start..position].iter().collect();
756 result.push_str(&ws);
757 }
758 continue;
759 }
760 result.push(chars[position]);
761 position += 1;
762 }
763 result
764}
765
766fn add_indentation(body: &str) -> String {
779 let chars: Vec<char> = body.chars().collect();
780 let len: usize = chars.len();
781 let mut result: String = String::new();
782 let mut depth: i32 = 0;
783 let mut index: usize = 0;
784 while index < len {
785 if chars[index] == CHAR_DOUBLE_QUOTE || chars[index] == CHAR_SINGLE_QUOTE {
786 let quote: char = chars[index];
787 result.push(chars[index]);
788 index += 1;
789 while index < len {
790 if chars[index] == CHAR_SLASH_BACK && index + 1 < len {
791 result.push(chars[index]);
792 result.push(chars[index + 1]);
793 index += 2;
794 continue;
795 }
796 result.push(chars[index]);
797 if chars[index] == quote {
798 index += 1;
799 break;
800 }
801 index += 1;
802 }
803 continue;
804 }
805 if index + 1 < len
806 && chars[index] == CHAR_SLASH_FORWARD
807 && chars[index + 1] == CHAR_SLASH_FORWARD
808 {
809 while index < len && chars[index] != CHAR_NEWLINE {
810 result.push(chars[index]);
811 index += 1;
812 }
813 continue;
814 }
815 if index + 1 < len
816 && chars[index] == CHAR_SLASH_FORWARD
817 && chars[index + 1] == CHAR_ASTERISK
818 {
819 result.push(chars[index]);
820 result.push(chars[index + 1]);
821 index += 2;
822 while index + 1 < len {
823 if chars[index] == CHAR_ASTERISK && chars[index + 1] == CHAR_SLASH_FORWARD {
824 result.push(chars[index]);
825 result.push(chars[index + 1]);
826 index += 2;
827 break;
828 }
829 result.push(chars[index]);
830 index += 1;
831 }
832 continue;
833 }
834 if chars[index] == CHAR_NEWLINE {
835 result.push(CHAR_NEWLINE);
836 index += 1;
837 while index < len && (chars[index] == CHAR_SPACE || chars[index] == CHAR_TAB) {
838 index += 1;
839 }
840 if index >= len || chars[index] == CHAR_NEWLINE || chars[index] == CHAR_CARRIAGE_RETURN
841 {
842 continue;
843 }
844 let mut closing_count: i32 = 0;
845 let mut peek: usize = index;
846 while peek < len && chars[peek] == CHAR_BRACE_RIGHT {
847 closing_count += 1;
848 peek += 1;
849 }
850 let indent_depth: i32 = (depth - closing_count).max(0);
851 for _ in 0..indent_depth * 4 {
852 result.push(CHAR_SPACE);
853 }
854 continue;
855 }
856 if chars[index] == CHAR_BRACE_LEFT {
857 depth += 1;
858 result.push(CHAR_BRACE_LEFT);
859 index += 1;
860 continue;
861 }
862 if chars[index] == CHAR_BRACE_RIGHT {
863 depth -= 1;
864 result.push(CHAR_BRACE_RIGHT);
865 index += 1;
866 let mut peek: usize = index;
867 while peek < len && (chars[peek] == CHAR_SPACE || chars[peek] == CHAR_TAB) {
868 peek += 1;
869 }
870 if peek < len
871 && chars[peek] != CHAR_BRACE_RIGHT
872 && chars[peek] != CHAR_BRACE_LEFT
873 && chars[peek] != CHAR_NEWLINE
874 && chars[peek] != CHAR_CARRIAGE_RETURN
875 && chars[peek] != CHAR_RIGHT_PAREN
876 && chars[peek] != CHAR_COMMA
877 && chars[peek] != CHAR_SEMICOLON
878 {
879 let next_chars: String = chars[peek..(peek + 4).min(len)].iter().collect();
880 if next_chars != "else" {
881 result.push(CHAR_NEWLINE);
882 let indent_depth: i32 = depth.max(0);
883 for _ in 0..indent_depth * 4 {
884 result.push(CHAR_SPACE);
885 }
886 index = peek;
887 }
888 }
889 continue;
890 }
891 result.push(chars[index]);
892 index += 1;
893 }
894
895 result
896}
897
898fn is_if_keyword(chars: &[char], pos: usize, len: usize) -> bool {
910 if pos + 2 > len {
911 return false;
912 }
913 chars[pos] == CHAR_LETTER_I
914 && chars[pos + 1] == CHAR_LETTER_F
915 && (pos + 2 >= len || !is_ident_char(chars[pos + 2]))
916 && (pos == 0 || !is_ident_char(chars[pos - 1]))
917 && !is_raw_prefix(chars, pos)
918}
919
920fn is_else_keyword(chars: &[char], pos: usize, len: usize) -> bool {
932 if pos + 4 > len {
933 return false;
934 }
935 chars[pos] == CHAR_LETTER_E
936 && chars[pos + 1] == CHAR_LETTER_L
937 && chars[pos + 2] == CHAR_LETTER_S
938 && chars[pos + 3] == CHAR_LETTER_E
939 && (pos + 4 >= len || !is_ident_char(chars[pos + 4]))
940 && (pos == 0 || !is_ident_char(chars[pos - 1]))
941 && !is_raw_prefix(chars, pos)
942}
943
944fn is_match_keyword(chars: &[char], pos: usize, len: usize) -> bool {
956 if pos + 5 > len {
957 return false;
958 }
959 chars[pos] == CHAR_LETTER_M
960 && chars[pos + 1] == CHAR_LETTER_A
961 && chars[pos + 2] == CHAR_LETTER_T
962 && chars[pos + 3] == CHAR_LETTER_C
963 && chars[pos + 4] == CHAR_LETTER_H
964 && (pos + 5 >= len || !is_ident_char(chars[pos + 5]))
965 && (pos == 0 || !is_ident_char(chars[pos - 1]))
966 && !is_raw_prefix(chars, pos)
967}
968
969fn is_for_keyword(chars: &[char], pos: usize, len: usize) -> bool {
981 if pos + 3 > len {
982 return false;
983 }
984 chars[pos] == CHAR_LETTER_F
985 && chars[pos + 1] == CHAR_LETTER_O
986 && chars[pos + 2] == CHAR_LETTER_R
987 && (pos + 3 >= len || !is_ident_char(chars[pos + 3]))
988 && (pos == 0 || !is_ident_char(chars[pos - 1]))
989 && !is_raw_prefix(chars, pos)
990}
991
992fn is_in_keyword(chars: &[char], pos: usize, len: usize) -> bool {
1004 if pos + 2 > len {
1005 return false;
1006 }
1007 chars[pos] == CHAR_LETTER_I
1008 && chars[pos + 1] == CHAR_LETTER_N
1009 && (pos + 2 >= len || !is_ident_char(chars[pos + 2]))
1010 && (pos == 0 || !is_ident_char(chars[pos - 1]))
1011 && !is_raw_prefix(chars, pos)
1012}
1013
1014fn format_brace_block(block: &str) -> String {
1028 let block_chars: Vec<char> = block.chars().collect();
1029 if block_chars.len() < 2
1030 || block_chars[0] != CHAR_BRACE_LEFT
1031 || *block_chars.last().unwrap() != CHAR_BRACE_RIGHT
1032 {
1033 return block.to_string();
1034 }
1035 let inner: String = block_chars[1..block_chars.len() - 1].iter().collect();
1036 if inner.contains(CHAR_NEWLINE) {
1037 return block.to_string();
1038 }
1039 let trimmed_inner: &str = inner.trim();
1040 if trimmed_inner.is_empty() {
1041 let mut empty_result: String = String::new();
1042 empty_result.push(CHAR_BRACE_LEFT);
1043 empty_result.push(CHAR_BRACE_RIGHT);
1044 return empty_result;
1045 }
1046 let mut formatted: String = String::new();
1047 formatted.push(CHAR_BRACE_LEFT);
1048 formatted.push(CHAR_SPACE);
1049 formatted.push_str(trimmed_inner);
1050 formatted.push(CHAR_SPACE);
1051 formatted.push(CHAR_BRACE_RIGHT);
1052 formatted
1053}
1054
1055fn skip_spaces_on_same_line(chars: &[char], mut pos: usize, len: usize) -> usize {
1067 while pos < len && (chars[pos] == CHAR_SPACE || chars[pos] == CHAR_TAB) {
1068 pos += 1;
1069 }
1070 pos
1071}
1072
1073fn is_pseudo_selector_after_colon(chars: &[char], mut pos: usize, len: usize) -> bool {
1098 if pos >= len || !is_ident_char(chars[pos]) {
1099 return false;
1100 }
1101 let ident_start: usize = pos;
1102 while pos < len && is_ident_char(chars[pos]) {
1103 pos += 1;
1104 }
1105 let first_ident: String = chars[ident_start..pos].iter().collect();
1106 if is_rust_keyword(&first_ident) {
1107 return false;
1108 }
1109 while pos < len && chars[pos] == CHAR_HYPHEN && pos + 1 < len && is_ident_char(chars[pos + 1]) {
1110 pos += 1;
1111 while pos < len && is_ident_char(chars[pos]) {
1112 pos += 1;
1113 }
1114 }
1115 let after_ident: usize = skip_spaces_on_same_line(chars, pos, len);
1116 if after_ident < len && chars[after_ident] == CHAR_BRACE_LEFT {
1117 return true;
1118 }
1119 if after_ident < len && chars[after_ident] == CHAR_LEFT_PAREN {
1120 let mut depth: i32 = 0;
1121 let mut paren_pos: usize = after_ident;
1122 while paren_pos < len {
1123 if chars[paren_pos] == CHAR_LEFT_PAREN {
1124 depth += 1;
1125 } else if chars[paren_pos] == CHAR_RIGHT_PAREN {
1126 depth -= 1;
1127 if depth == 0 {
1128 let after_paren: usize = skip_spaces_on_same_line(chars, paren_pos + 1, len);
1129 return after_paren < len && chars[after_paren] == CHAR_BRACE_LEFT;
1130 }
1131 }
1132 paren_pos += 1;
1133 }
1134 }
1135 false
1136}
1137
1138fn is_rust_keyword(ident: &str) -> bool {
1151 matches!(ident, KEYWORD_IF | KEYWORD_MATCH | KEYWORD_FOR)
1152}
1153
1154fn is_raw_ident_before(result: &str, ident: &str) -> bool {
1168 result
1169 .trim_end()
1170 .ends_with(&format!("{RAW_IDENT_PREFIX}{ident}"))
1171}
1172
1173fn find_ident_before(result: &str) -> String {
1183 let chars: Vec<char> = result.chars().collect();
1184 let mut end: usize = chars.len();
1185 while end > 0 && chars[end - 1] == CHAR_SPACE {
1186 end -= 1;
1187 }
1188 let mut start: usize = end;
1189 while start > 0 && is_ident_char(chars[start - 1]) {
1190 start -= 1;
1191 }
1192 if start < end {
1193 chars[start..end].iter().collect()
1194 } else {
1195 String::new()
1196 }
1197}
1198
1199fn remove_trailing_spaces(result: &str, prefix_len: usize) -> String {
1210 let chars: Vec<char> = result.chars().collect();
1211 let total_len: usize = chars.len();
1212 let mut end: usize = total_len;
1213 while end > 0 && chars[end - 1] == CHAR_SPACE {
1214 end -= 1;
1215 }
1216 if prefix_len > end {
1217 return result.to_string();
1218 }
1219 let new_end: usize = end - prefix_len;
1220 chars[..new_end].iter().collect()
1221}
1222
1223fn find_trailing_spaces(result: &str) -> String {
1233 let mut spaces: String = String::new();
1234 for ch in result.chars().rev() {
1235 if ch == CHAR_SPACE || ch == CHAR_TAB {
1236 spaces.push(ch);
1237 } else {
1238 break;
1239 }
1240 }
1241 spaces.chars().rev().collect()
1242}
1243
1244pub async fn format_dir(path: &Path, mode: FmtMode) -> Result<(), EuvError> {
1258 if path.is_file() {
1259 let changed: bool = format_file(path, &mode).await?;
1260 match mode {
1261 FmtMode::Check => {
1262 if changed {
1263 return Err(EuvError::Message(format!(
1264 "{} needs formatting.",
1265 path.display()
1266 )));
1267 }
1268 log::info!("{} is properly formatted.", path.display());
1269 }
1270 FmtMode::Write => {
1271 if changed {
1272 log::info!("Formatted: {}", path.display());
1273 } else {
1274 log::info!("Already formatted: {}", path.display());
1275 }
1276 }
1277 }
1278 return Ok(());
1279 }
1280 let mut entries: Vec<PathBuf> = collect_rs_files(path).await?;
1281 entries.sort();
1282 let mut changed_count: usize = 0;
1283 let mut unchanged_count: usize = 0;
1284 for entry in entries {
1285 match format_file(&entry, &mode).await {
1286 Ok(changed) => {
1287 if changed {
1288 changed_count += 1;
1289 } else {
1290 unchanged_count += 1;
1291 }
1292 }
1293 Err(error) => {
1294 log::warn!("Failed to format {}: {error}", entry.display());
1295 }
1296 }
1297 }
1298 match mode {
1299 FmtMode::Check => {
1300 if changed_count > 0 {
1301 return Err(EuvError::Message(format!(
1302 "{} file(s) need formatting. Run `euv fmt` to fix.",
1303 changed_count
1304 )));
1305 }
1306 log::info!("All {} file(s) are properly formatted.", unchanged_count);
1307 }
1308 FmtMode::Write => {
1309 log::info!(
1310 "Formatted {} file(s), {} unchanged.",
1311 changed_count,
1312 unchanged_count
1313 );
1314 }
1315 }
1316 Ok(())
1317}
1318
1319async fn collect_rs_files(path: &Path) -> Result<Vec<PathBuf>, EuvError> {
1329 let mut result: Vec<PathBuf> = Vec::new();
1330 let mut stack: Vec<PathBuf> = vec![path.to_path_buf()];
1331 while let Some(dir) = stack.pop() {
1332 let mut entries: ReadDir =
1333 read_dir(&dir)
1334 .await
1335 .map_err(|error: io::Error| EuvError::IoPath {
1336 message: String::from("Failed to read directory"),
1337 path: dir.clone(),
1338 error,
1339 })?;
1340 while let Some(entry) =
1341 entries
1342 .next_entry()
1343 .await
1344 .map_err(|error: io::Error| EuvError::IoPath {
1345 message: String::from("Failed to read entry in directory"),
1346 path: dir.clone(),
1347 error,
1348 })?
1349 {
1350 let entry_path: PathBuf = entry.path();
1351 if entry_path.is_dir() {
1352 let file_name: String = entry_path
1353 .file_name()
1354 .unwrap_or_default()
1355 .to_string_lossy()
1356 .to_string();
1357 if file_name != TARGET_DIR_NAME && file_name != NODE_MODULES_DIR_NAME {
1358 stack.push(entry_path);
1359 }
1360 } else if entry_path
1361 .extension()
1362 .is_some_and(|ext: &std::ffi::OsStr| ext == RS_EXTENSION)
1363 {
1364 result.push(entry_path);
1365 }
1366 }
1367 }
1368 Ok(result)
1369}
1370
1371async fn format_file(path: &Path, mode: &FmtMode) -> Result<bool, EuvError> {
1382 let content: String =
1383 read_to_string(path)
1384 .await
1385 .map_err(|error: io::Error| EuvError::IoPath {
1386 message: String::from("Failed to read"),
1387 path: path.to_path_buf(),
1388 error,
1389 })?;
1390 let fmt_result: FmtResult = format_source(&content);
1391 if fmt_result.get_changed() {
1392 match mode {
1393 FmtMode::Write => {
1394 write(path, fmt_result.get_output())
1395 .await
1396 .map_err(|error: io::Error| EuvError::IoPath {
1397 message: String::from("Failed to write"),
1398 path: path.to_path_buf(),
1399 error,
1400 })?;
1401 log::info!("Formatted: {}", path.display());
1402 }
1403 FmtMode::Check => {
1404 log::warn!("Needs formatting: {}", path.display());
1405 }
1406 }
1407 }
1408 Ok(fmt_result.get_changed())
1409}