1use serde::{Deserialize, Serialize};
20use std::collections::HashMap;
21use std::path::PathBuf;
22
23pub mod config;
24pub mod formatter;
25pub mod parser;
26pub mod rules;
27pub mod writer;
28
29#[derive(Debug, Clone)]
31pub struct FormatResult {
32 pub file_path: PathBuf,
34 pub original_content: String,
36 pub formatted_content: String,
38 pub has_changes: bool,
40 pub error: Option<String>,
42}
43
44impl FormatResult {
45 pub fn new(file_path: PathBuf, original_content: String) -> Self {
47 Self {
48 file_path,
49 original_content: original_content.clone(),
50 formatted_content: original_content,
51 has_changes: false,
52 error: None,
53 }
54 }
55
56 pub fn set_formatted_content(&mut self, content: String) {
58 self.has_changes = content != self.original_content;
59 self.formatted_content = content;
60 }
61
62 pub fn set_error(&mut self, error: String) {
64 self.error = Some(error);
65 }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct FormatterConfig {
71 pub indent_style: IndentStyle,
73 pub indent_width: usize,
75 pub line_width: usize,
77 pub line_ending: LineEnding,
79 pub brace_style: BraceStyle,
81 pub trailing_comma: bool,
83 pub space_around_operators: bool,
85 pub max_empty_lines: usize,
87}
88
89impl Default for FormatterConfig {
90 fn default() -> Self {
91 Self {
92 indent_style: IndentStyle::Space,
93 indent_width: 4,
94 line_width: 100,
95 line_ending: LineEnding::Lf,
96 brace_style: BraceStyle::SameLine,
97 trailing_comma: true,
98 space_around_operators: true,
99 max_empty_lines: 2,
100 }
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub enum IndentStyle {
107 Space,
109 Tab,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub enum LineEnding {
116 Lf,
118 Crlf,
120 Auto,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub enum BraceStyle {
127 SameLine,
129 NextLine,
131}
132
133#[derive(Debug)]
135pub struct Formatter {
136 config: FormatterConfig,
137 rules: rules::FormatRules,
138}
139
140impl Formatter {
141 pub fn new(config: FormatterConfig) -> Self {
143 let rules = rules::FormatRules::new(&config);
144 Self { config, rules }
145 }
146
147 pub fn default() -> Self {
149 Self::new(FormatterConfig::default())
150 }
151
152 pub async fn from_config_file() -> Result<Self, Box<dyn std::error::Error>> {
154 let config = config::load_config().await?;
155 Ok(Self::new(config))
156 }
157
158 pub async fn format_file(&self, file_path: &PathBuf) -> Result<FormatResult, Box<dyn std::error::Error>> {
160 let content = tokio::fs::read_to_string(file_path).await?;
161 let mut result = FormatResult::new(file_path.clone(), content);
162
163 match self.format_content(&result.original_content).await {
164 Ok(formatted) => {
165 result.set_formatted_content(formatted);
166 Ok(result)
167 }
168 Err(e) => {
169 result.set_error(e.to_string());
170 Ok(result)
171 }
172 }
173 }
174
175 pub async fn format_content(&self, content: &str) -> Result<String, Box<dyn std::error::Error>> {
177 let mut lines = content.lines().collect::<Vec<_>>();
181 let mut formatted_lines = Vec::new();
182
183 for line in lines {
184 let formatted = self.format_line(line);
185 formatted_lines.push(formatted);
186 }
187
188 self.cleanup_empty_lines(&mut formatted_lines);
190
191 Ok(formatted_lines.join(&self.get_line_ending()))
192 }
193
194 fn format_line(&self, line: &str) -> String {
196 let line = line.trim();
197
198 if line.is_empty() {
199 return String::new();
200 }
201
202 let line = self.format_braces(line);
204 let line = self.format_operators(&line);
205 let line = self.format_commas(&line);
206
207 line
208 }
209
210 fn format_braces(&self, line: &str) -> String {
212 line.to_string()
214 }
215
216 fn format_operators(&self, line: &str) -> String {
218 if !self.config.space_around_operators {
219 return line.to_string();
220 }
221
222 line.replace("=", " = ")
224 .replace("==", " == ")
225 .replace("!=", " != ")
226 .replace("<", " < ")
227 .replace(">", " > ")
228 .replace("<=", " <= ")
229 .replace(">=", " >= ")
230 .replace("+", " + ")
231 .replace("-", " - ")
232 .replace("*", " * ")
233 .replace("/", " / ")
234 .replace("&&", " && ")
235 .replace("||", " || ")
236 .replace("->", " -> ")
237 .replace(":", " : ")
238 .split_whitespace()
240 .collect::<Vec<_>>()
241 .join(" ")
242 }
243
244 fn format_commas(&self, line: &str) -> String {
246 if self.config.trailing_comma {
247 line.replace(",", ", ")
249 } else {
250 line.to_string()
251 }
252 }
253
254 fn cleanup_empty_lines(&self, lines: &mut Vec<String>) {
256 let mut result = Vec::new();
257 let mut empty_count = 0;
258
259 for line in &mut *lines {
260 if line.trim().is_empty() {
261 empty_count += 1;
262 if empty_count <= self.config.max_empty_lines {
263 result.push(line.clone());
264 }
265 } else {
266 empty_count = 0;
267 result.push(line.clone());
268 }
269 }
270
271 *lines = result;
272 }
273
274 fn get_line_ending(&self) -> String {
276 match self.config.line_ending {
277 LineEnding::Lf => "\n".to_string(),
278 LineEnding::Crlf => "\r\n".to_string(),
279 LineEnding::Auto => "\n".to_string(),
280 }
281 }
282}
283
284pub async fn format_files(files: Vec<PathBuf>, check_only: bool) -> Result<Vec<FormatResult>, Box<dyn std::error::Error>> {
286 let formatter = Formatter::default();
287 let mut results = Vec::new();
288
289 for file in files {
290 let result = formatter.format_file(&file).await?;
291 results.push(result);
292 }
293
294 Ok(results)
295}
296
297pub async fn format_directory(dir: PathBuf, check_only: bool) -> Result<Vec<FormatResult>, Box<dyn std::error::Error>> {
298 let mut files = Vec::new();
299
300 find_kotoba_files(dir, &mut files).await?;
302
303 format_files(files, check_only).await
304}
305
306async fn find_kotoba_files(dir: PathBuf, files: &mut Vec<PathBuf>) -> Result<(), Box<dyn std::error::Error>> {
308 let mut entries = tokio::fs::read_dir(&dir).await?;
309
310 while let Some(entry) = entries.next_entry().await? {
311 let path = entry.path();
312
313 if path.is_dir() {
314 if !path.ends_with("node_modules") && !path.ends_with(".git") {
316 Box::pin(find_kotoba_files(path, files)).await?;
317 }
318 } else if path.extension().map_or(false, |ext| ext == "kotoba") {
319 files.push(path);
320 }
321 }
322
323 Ok(())
324}
325
326pub use config::*;
328pub use formatter::*;
329pub use parser::*;
330pub use rules::*;
331pub use writer::*;