1use crate::error::Result;
2use std::path::Path;
3use std::process::Command;
4
5pub enum Formatter {
6 Prettier,
7 Biome,
8}
9
10pub struct FormatterManager;
11
12impl FormatterManager {
13 pub fn detect_formatter() -> Option<Formatter> {
14 Self::detect_formatter_from_dir(Path::new("."))
15 }
16
17 pub fn detect_formatter_from_dir(dir: &Path) -> Option<Formatter> {
18 if Self::has_prettier_in_dir(dir) {
20 return Some(Formatter::Prettier);
21 }
22
23 if Self::has_biome_in_dir(dir) {
25 return Some(Formatter::Biome);
26 }
27
28 None
29 }
30
31 fn has_prettier_in_dir(dir: &Path) -> bool {
32 let package_json = dir.join("package.json");
34 if package_json.exists() {
35 if let Ok(content) = std::fs::read_to_string(&package_json) {
36 if content.contains("prettier") {
37 return true;
38 }
39 }
40 }
41
42 dir.join(".prettierrc").exists()
44 || dir.join(".prettierrc.json").exists()
45 || dir.join(".prettierrc.js").exists()
46 || dir.join("prettier.config.js").exists()
47 }
48
49 fn has_biome_in_dir(dir: &Path) -> bool {
50 dir.join("biome.json").exists() || dir.join("biome.jsonc").exists()
52 }
53
54 pub fn format_files(files: &[std::path::PathBuf], formatter: Formatter) -> Result<()> {
55 match formatter {
56 Formatter::Prettier => Self::format_with_prettier(files),
57 Formatter::Biome => Self::format_with_biome(files),
58 }
59 }
60
61 fn format_with_prettier(files: &[std::path::PathBuf]) -> Result<()> {
62 let file_paths: Vec<String> = files
63 .iter()
64 .filter_map(|p| p.to_str().filter(|s| !s.is_empty()).map(|s| s.to_string()))
65 .collect();
66
67 if file_paths.is_empty() {
68 return Ok(());
69 }
70
71 let output = Command::new("npx")
74 .arg("prettier")
75 .arg("--write")
76 .args(&file_paths)
77 .output();
78
79 match output {
80 Ok(output) => {
81 if !output.status.success() {
82 let stderr = String::from_utf8_lossy(&output.stderr);
83 let stdout = String::from_utf8_lossy(&output.stdout);
84 eprintln!("Warning: Prettier exited with error:");
85 eprintln!(" stderr: {}", stderr);
86 if !stdout.is_empty() {
87 eprintln!(" stdout: {}", stdout);
88 }
89 }
90 Ok(())
91 }
92 Err(e) => {
93 eprintln!("Warning: Failed to run prettier: {}", e);
95 Ok(())
96 }
97 }
98 }
99
100 fn format_with_biome(files: &[std::path::PathBuf]) -> Result<()> {
101 let file_paths: Vec<String> = files
102 .iter()
103 .filter_map(|p| p.to_str().filter(|s| !s.is_empty()).map(|s| s.to_string()))
104 .collect();
105
106 if file_paths.is_empty() {
107 return Ok(());
108 }
109
110 let output = Command::new("npx")
111 .arg("@biomejs/biome")
112 .arg("format")
113 .arg("--write")
114 .args(&file_paths)
115 .output();
116
117 match output {
118 Ok(_) => Ok(()),
119 Err(e) => {
120 eprintln!("Warning: Failed to run biome: {}", e);
122 Ok(())
123 }
124 }
125 }
126
127 pub fn format_content(content: &str, formatter: Formatter, file_path: &Path) -> Result<String> {
131 use std::process::Stdio;
132
133 let work_dir = file_path.parent().unwrap_or(Path::new("."));
135
136 let format_result = match formatter {
138 Formatter::Prettier => {
139 let mut cmd = Command::new("npx");
140 cmd.arg("prettier")
141 .arg("--stdin-filepath")
142 .arg(file_path.to_str().unwrap_or("file.ts"))
143 .current_dir(work_dir)
144 .stdin(Stdio::piped())
145 .stdout(Stdio::piped())
146 .stderr(Stdio::piped());
147
148 let mut child = cmd.spawn().map_err(|e| {
149 crate::error::VikaError::from(crate::error::FileSystemError::ReadFileFailed {
150 path: file_path.display().to_string(),
151 source: e,
152 })
153 })?;
154
155 if let Some(mut stdin) = child.stdin.take() {
157 use std::io::Write;
158 stdin.write_all(content.as_bytes()).map_err(|e| {
159 crate::error::VikaError::from(
160 crate::error::FileSystemError::WriteFileFailed {
161 path: "stdin".to_string(),
162 source: e,
163 },
164 )
165 })?;
166 }
167
168 child.wait_with_output()
169 }
170 Formatter::Biome => {
171 let mut cmd = Command::new("npx");
172 cmd.arg("@biomejs/biome")
173 .arg("format")
174 .arg("--stdin-file-path")
175 .arg(file_path.to_str().unwrap_or("file.ts"))
176 .current_dir(work_dir)
177 .stdin(Stdio::piped())
178 .stdout(Stdio::piped())
179 .stderr(Stdio::piped());
180
181 let mut child = cmd.spawn().map_err(|e| {
182 crate::error::VikaError::from(crate::error::FileSystemError::ReadFileFailed {
183 path: file_path.display().to_string(),
184 source: e,
185 })
186 })?;
187
188 if let Some(mut stdin) = child.stdin.take() {
190 use std::io::Write;
191 stdin.write_all(content.as_bytes()).map_err(|e| {
192 crate::error::VikaError::from(
193 crate::error::FileSystemError::WriteFileFailed {
194 path: "stdin".to_string(),
195 source: e,
196 },
197 )
198 })?;
199 }
200
201 child.wait_with_output()
202 }
203 };
204
205 match format_result {
207 Ok(output) if output.status.success() => {
208 String::from_utf8(output.stdout).map_err(|e| {
209 crate::error::VikaError::from(crate::error::FileSystemError::ReadFileFailed {
210 path: "stdout".to_string(),
211 source: std::io::Error::new(std::io::ErrorKind::InvalidData, e),
212 })
213 })
214 }
215 _ => {
216 Ok(content.to_string())
218 }
219 }
220 }
221}