1use once_cell::sync::Lazy;
2use regex::Regex;
3use std::fs::File;
4use std::io::{self, BufRead, BufReader, Write};
5use std::path::Path;
6
7static SINGLE_LINE_EXPORT_REGEX: Lazy<Regex> =
8 Lazy::new(|| Regex::new(r"^export\..*(?:(?:::)|(?:->)).*$").unwrap());
9
10#[derive(Debug, PartialEq, Clone)]
11enum ConstructType {
12 Proc,
13 Export,
14 Begin,
15 End,
16 While,
17 Repeat,
18 If,
19 Else,
20}
21
22impl ConstructType {
23 fn from_str(s: &str) -> Option<Self> {
24 match s {
25 "proc" => Some(Self::Proc),
26 "export" => Some(Self::Export),
27 "begin" => Some(Self::Begin),
28 "end" => Some(Self::End),
29 "while" => Some(Self::While),
30 "repeat" => Some(Self::Repeat),
31 "if" => Some(Self::If),
32 "else" => Some(Self::Else),
33 _ => None,
34 }
35 }
36}
37
38const INDENT: &str = " ";
39
40fn is_comment(line: &str) -> bool {
41 line.trim_start().starts_with('#')
42}
43
44fn is_stack_comment(line: &str) -> bool {
45 line.trim_start().starts_with("# =>")
46}
47
48fn is_single_export_line(line: &str) -> bool {
49 SINGLE_LINE_EXPORT_REGEX.is_match(line)
50}
51
52fn is_use_statement(line: &str) -> bool {
53 line.trim_start().starts_with("use.")
54}
55
56fn extract_and_sort_imports(lines: &[&str]) -> (Vec<String>, usize) {
57 let mut imports = Vec::new();
58 let mut import_end_index = 0;
59
60 for (i, line) in lines.iter().enumerate() {
61 let trimmed = line.trim();
62 if is_use_statement(trimmed) {
63 imports.push(trimmed.to_string());
64 import_end_index = i + 1;
65 } else if !trimmed.is_empty() {
66 break;
68 } else if !imports.is_empty() {
69 import_end_index = i + 1;
71 }
72 }
73
74 imports.sort();
76
77 (imports, import_end_index)
78}
79
80pub fn format_code(code: &str) -> String {
81 let lines: Vec<&str> = code.lines().collect();
82
83 let (sorted_imports, import_end_index) = extract_and_sort_imports(&lines);
85
86 let mut formatted_code = String::new();
87 let mut indentation_level = 0;
88 let mut construct_stack = Vec::new();
89 let mut last_line_was_empty = false;
90 let mut last_was_export_line = false;
91 let mut last_line_was_stack_comment = false;
92
93 for import in sorted_imports {
95 formatted_code.push_str(&import);
96 formatted_code.push('\n');
97 }
98
99 if import_end_index > 0 && import_end_index < lines.len() {
101 if !lines[import_end_index].trim().is_empty() {
102 formatted_code.push('\n');
103 }
104 }
105
106 let remaining_lines = &lines[import_end_index..];
108
109 for line in remaining_lines {
110 let trimmed_line = line.trim();
111
112 if !trimmed_line.is_empty() {
113 if is_comment(trimmed_line) {
114 if is_stack_comment(trimmed_line) {
115 last_line_was_stack_comment = true;
116 } else {
117 last_line_was_stack_comment = false;
118 }
119
120 if last_was_export_line {
121 formatted_code.push_str(trimmed_line);
122 } else {
123 if let Some(prev_line) = formatted_code.lines().last() {
124 let prev_indent_level =
125 prev_line.chars().take_while(|&c| c == ' ').count() / 4;
126 if prev_line.trim_start().starts_with("export") {
127 formatted_code.push_str(&INDENT.repeat(prev_indent_level + 1));
128 } else {
129 formatted_code.push_str(&INDENT.repeat(indentation_level));
130 }
131 } else {
132 formatted_code.push_str(&INDENT.repeat(indentation_level));
133 }
134 formatted_code.push_str(trimmed_line);
135 }
136 formatted_code.push('\n');
137 last_line_was_empty = false;
138 continue;
139 }
140
141 if is_single_export_line(trimmed_line) {
142 formatted_code.push_str(trimmed_line);
143 formatted_code.push('\n');
144 last_line_was_empty = false;
145 last_was_export_line = true;
146 continue;
147 }
148
149 last_was_export_line = false;
150
151 let code_without_comment = trimmed_line.split('#').next().unwrap().trim();
153 let first_word = code_without_comment.split('.').next();
154
155 if last_line_was_stack_comment && first_word.is_some() {
157 let word = first_word.unwrap();
158 if word != "end" && !last_line_was_empty {
159 formatted_code.push('\n');
160 }
161 last_line_was_stack_comment = false;
162 }
163
164 if let Some(word) = first_word {
165 if let Some(construct) = ConstructType::from_str(word) {
166 match construct {
167 ConstructType::End => {
168 if let Some(last_construct) = construct_stack.pop() {
169 if last_construct != ConstructType::End && indentation_level > 0 {
170 indentation_level -= 1;
171 }
172 }
173 }
174 ConstructType::Else => {
175 if let Some(last_construct) = construct_stack.last() {
176 if *last_construct == ConstructType::If && indentation_level > 0 {
177 indentation_level -= 1;
178 }
179 }
180 }
181 _ => {
182 construct_stack.push(construct.clone());
183 }
184 }
185
186 formatted_code.push_str(&INDENT.repeat(indentation_level));
187 formatted_code.push_str(trimmed_line);
188 formatted_code.push('\n');
189 last_line_was_empty = false;
190
191 match construct {
192 ConstructType::Begin
193 | ConstructType::If
194 | ConstructType::Proc
195 | ConstructType::Export
196 | ConstructType::Repeat
197 | ConstructType::While
198 | ConstructType::Else => {
199 indentation_level += 1;
200 }
201 _ => {}
202 }
203
204 continue;
205 }
206 }
207
208 formatted_code.push_str(&INDENT.repeat(indentation_level));
209 formatted_code.push_str(trimmed_line);
210 formatted_code.push('\n');
211 last_line_was_empty = false;
212 } else if !last_line_was_empty {
213 formatted_code.push('\n');
214 last_line_was_empty = true;
215 }
216 }
217
218 while formatted_code.ends_with('\n') {
220 formatted_code.pop();
221 }
222 formatted_code.push('\n');
223
224 formatted_code
225}
226
227pub fn format_file(file_path: &Path) -> io::Result<()> {
228 let file = File::open(file_path)?;
229 let mut input_code = String::new();
230
231 let reader = BufReader::new(file);
232 for line in reader.lines() {
233 input_code.push_str(&line?);
234 input_code.push_str("\n");
235 }
236
237 let formatted_code = format_code(&input_code);
238
239 let mut file = File::create(file_path)?;
240 file.write_all(formatted_code.as_bytes())?;
241
242 Ok(())
243}