1use std::io::Write;
7use std::path::Path;
8use std::process::{Command, Stdio};
9use std::time::Duration;
10
11use super::Editor;
12use crate::config::{FormatterConfig, OnSaveAction};
13use crate::model::event::Event;
14use crate::services::lsp::manager::detect_language;
15use rust_i18n::t;
16
17enum ActionResult {
19 Success(String),
21 CommandNotFound(String),
23 Error(String),
25}
26
27impl Editor {
28 pub fn run_on_save_actions(&mut self) -> Result<bool, String> {
33 let path = match self.active_state().buffer.file_path() {
34 Some(p) => p.to_path_buf(),
35 None => return Ok(false),
36 };
37
38 let mut ran_any_action = false;
39
40 if self.config.editor.trim_trailing_whitespace_on_save {
42 if self.trim_trailing_whitespace()? {
43 ran_any_action = true;
44 }
45 }
46
47 if self.config.editor.ensure_final_newline_on_save {
48 if self.ensure_final_newline()? {
49 ran_any_action = true;
50 }
51 }
52
53 if ran_any_action {
55 if let Err(e) = self.active_state_mut().buffer.save() {
56 return Err(format!("Failed to re-save after whitespace cleanup: {}", e));
57 }
58 self.active_event_log_mut().mark_saved();
59 }
60
61 let language = match detect_language(&path, &self.config.languages) {
63 Some(lang) => lang,
64 None => return Ok(ran_any_action),
65 };
66
67 let lang_config = match self.config.languages.get(&language) {
68 Some(lc) => lc.clone(),
69 None => return Ok(ran_any_action),
70 };
71
72 if lang_config.format_on_save {
74 if let Some(ref formatter) = lang_config.formatter {
75 match self.run_formatter(formatter, &path) {
76 ActionResult::Success(output) => {
77 self.replace_buffer_with_output(&output)?;
78 if let Err(e) = self.active_state_mut().buffer.save() {
80 return Err(format!("Failed to re-save after format: {}", e));
81 }
82 self.active_event_log_mut().mark_saved();
83 ran_any_action = true;
84 }
85 ActionResult::CommandNotFound(cmd) => {
86 self.status_message = Some(format!(
87 "Formatter '{}' not found (install it for auto-formatting)",
88 cmd
89 ));
90 }
91 ActionResult::Error(e) => {
92 return Err(e);
93 }
94 }
95 }
96 }
97
98 let project_root = std::env::current_dir()
100 .unwrap_or_else(|_| path.parent().unwrap_or(Path::new(".")).to_path_buf());
101
102 for action in &lang_config.on_save {
103 if !action.enabled {
104 continue;
105 }
106
107 match self.run_on_save_action(action, &path, &project_root) {
108 ActionResult::Success(_) => {
109 ran_any_action = true;
110 }
111 ActionResult::CommandNotFound(_) => {
112 }
114 ActionResult::Error(e) => {
115 return Err(e);
116 }
117 }
118 }
119
120 Ok(ran_any_action)
121 }
122
123 pub fn format_buffer(&mut self) -> Result<(), String> {
126 let path = match self.active_state().buffer.file_path() {
127 Some(p) => p.to_path_buf(),
128 None => {
129 return Err(
130 "Cannot format unsaved buffer (save first to detect language)".to_string(),
131 )
132 }
133 };
134
135 let language = match detect_language(&path, &self.config.languages) {
137 Some(lang) => lang,
138 None => return Err("No language detected for this file".to_string()),
139 };
140
141 let formatter = self
143 .config
144 .languages
145 .get(&language)
146 .and_then(|lc| lc.formatter.clone());
147
148 let formatter = match formatter {
149 Some(f) => f,
150 None => return Err(format!("No formatter configured for {}", language)),
151 };
152
153 match self.run_formatter(&formatter, &path) {
154 ActionResult::Success(output) => {
155 self.replace_buffer_with_output(&output)?;
156 self.set_status_message(
157 t!(
158 "format.formatted_with",
159 formatter = formatter.command.clone()
160 )
161 .to_string(),
162 );
163 Ok(())
164 }
165 ActionResult::CommandNotFound(cmd) => Err(format!("Formatter '{}' not found", cmd)),
166 ActionResult::Error(e) => Err(e),
167 }
168 }
169
170 fn run_formatter(&mut self, formatter: &FormatterConfig, file_path: &Path) -> ActionResult {
172 let file_path_str = file_path.display().to_string();
173
174 if !command_exists(&formatter.command) {
176 return ActionResult::CommandNotFound(formatter.command.clone());
177 }
178
179 let shell = detect_shell();
181
182 let mut cmd_parts = vec![formatter.command.clone()];
184 for arg in &formatter.args {
185 cmd_parts.push(arg.replace("$FILE", &file_path_str));
186 }
187
188 let full_command = cmd_parts.join(" ");
189
190 let project_root = std::env::current_dir()
192 .unwrap_or_else(|_| file_path.parent().unwrap_or(Path::new(".")).to_path_buf());
193
194 let mut cmd = Command::new(&shell);
196 cmd.args(["-c", &full_command])
197 .current_dir(&project_root)
198 .stdout(Stdio::piped())
199 .stderr(Stdio::piped());
200
201 if formatter.stdin {
202 cmd.stdin(Stdio::piped());
203 } else {
204 cmd.stdin(Stdio::null());
205 }
206
207 let mut child = match cmd.spawn() {
209 Ok(c) => c,
210 Err(e) => {
211 return ActionResult::Error(format!(
212 "Failed to run '{}': {}",
213 formatter.command, e
214 ));
215 }
216 };
217
218 if formatter.stdin {
220 let content = self.active_state().buffer.to_string().unwrap_or_default();
221 if let Some(mut stdin) = child.stdin.take() {
222 if let Err(e) = stdin.write_all(content.as_bytes()) {
223 return ActionResult::Error(format!("Failed to write to stdin: {}", e));
224 }
225 }
226 }
227
228 let timeout = Duration::from_millis(formatter.timeout_ms);
230 let start = std::time::Instant::now();
231
232 loop {
233 match child.try_wait() {
234 Ok(Some(status)) => {
235 let output = match child.wait_with_output() {
236 Ok(o) => o,
237 Err(e) => {
238 return ActionResult::Error(format!("Failed to get output: {}", e))
239 }
240 };
241
242 if status.success() {
243 return match String::from_utf8(output.stdout) {
244 Ok(s) => ActionResult::Success(s),
245 Err(e) => {
246 ActionResult::Error(format!("Invalid UTF-8 in output: {}", e))
247 }
248 };
249 } else {
250 let stderr = String::from_utf8_lossy(&output.stderr);
251 let stdout = String::from_utf8_lossy(&output.stdout);
252 let error_output = if !stderr.is_empty() {
253 stderr.trim().to_string()
254 } else if !stdout.is_empty() {
255 stdout.trim().to_string()
256 } else {
257 format!("exit code {:?}", status.code())
258 };
259 return ActionResult::Error(format!(
260 "Formatter '{}' failed: {}",
261 formatter.command, error_output
262 ));
263 }
264 }
265 Ok(None) => {
266 if start.elapsed() > timeout {
267 let _ = child.kill();
268 return ActionResult::Error(format!(
269 "Formatter '{}' timed out after {}ms",
270 formatter.command, formatter.timeout_ms
271 ));
272 }
273 std::thread::sleep(Duration::from_millis(10));
274 }
275 Err(e) => {
276 return ActionResult::Error(format!(
277 "Failed to wait for '{}': {}",
278 formatter.command, e
279 ));
280 }
281 }
282 }
283 }
284
285 fn run_on_save_action(
287 &mut self,
288 action: &OnSaveAction,
289 file_path: &Path,
290 project_root: &Path,
291 ) -> ActionResult {
292 let file_path_str = file_path.display().to_string();
293
294 if !command_exists(&action.command) {
296 return ActionResult::CommandNotFound(action.command.clone());
297 }
298
299 let shell = detect_shell();
301
302 let mut cmd_parts = vec![action.command.clone()];
303 for arg in &action.args {
304 cmd_parts.push(arg.replace("$FILE", &file_path_str));
305 }
306
307 let has_file_arg = action.args.iter().any(|a| a.contains("$FILE"));
309 if !has_file_arg && !action.stdin {
310 cmd_parts.push(file_path_str.clone());
311 }
312
313 let full_command = cmd_parts.join(" ");
314
315 let working_dir = action
317 .working_dir
318 .as_ref()
319 .map(|wd| {
320 let expanded = wd.replace("$FILE", &file_path_str);
321 Path::new(&expanded).to_path_buf()
322 })
323 .unwrap_or_else(|| project_root.to_path_buf());
324
325 let mut cmd = Command::new(&shell);
327 cmd.args(["-c", &full_command])
328 .current_dir(&working_dir)
329 .stdout(Stdio::piped())
330 .stderr(Stdio::piped());
331
332 if action.stdin {
333 cmd.stdin(Stdio::piped());
334 } else {
335 cmd.stdin(Stdio::null());
336 }
337
338 let mut child = match cmd.spawn() {
340 Ok(c) => c,
341 Err(e) => {
342 return ActionResult::Error(format!("Failed to run '{}': {}", action.command, e));
343 }
344 };
345
346 if action.stdin {
348 let content = self.active_state().buffer.to_string().unwrap_or_default();
349 if let Some(mut stdin) = child.stdin.take() {
350 if let Err(e) = stdin.write_all(content.as_bytes()) {
351 return ActionResult::Error(format!("Failed to write to stdin: {}", e));
352 }
353 }
354 }
355
356 let timeout = Duration::from_millis(action.timeout_ms);
358 let start = std::time::Instant::now();
359
360 loop {
361 match child.try_wait() {
362 Ok(Some(status)) => {
363 let output = match child.wait_with_output() {
364 Ok(o) => o,
365 Err(e) => {
366 return ActionResult::Error(format!("Failed to get output: {}", e))
367 }
368 };
369
370 if status.success() {
371 return match String::from_utf8(output.stdout) {
372 Ok(s) => ActionResult::Success(s),
373 Err(e) => {
374 ActionResult::Error(format!("Invalid UTF-8 in output: {}", e))
375 }
376 };
377 } else {
378 let stderr = String::from_utf8_lossy(&output.stderr);
379 let stdout = String::from_utf8_lossy(&output.stdout);
380 let error_output = if !stderr.is_empty() {
381 stderr.trim().to_string()
382 } else if !stdout.is_empty() {
383 stdout.trim().to_string()
384 } else {
385 format!("exit code {:?}", status.code())
386 };
387 return ActionResult::Error(format!(
388 "On-save action '{}' failed: {}",
389 action.command, error_output
390 ));
391 }
392 }
393 Ok(None) => {
394 if start.elapsed() > timeout {
395 let _ = child.kill();
396 return ActionResult::Error(format!(
397 "On-save action '{}' timed out after {}ms",
398 action.command, action.timeout_ms
399 ));
400 }
401 std::thread::sleep(Duration::from_millis(10));
402 }
403 Err(e) => {
404 return ActionResult::Error(format!(
405 "Failed to wait for '{}': {}",
406 action.command, e
407 ));
408 }
409 }
410 }
411 }
412
413 fn replace_buffer_with_output(&mut self, output: &str) -> Result<(), String> {
415 let cursor_id = self.active_state().cursors.primary_id();
416
417 let buffer_content = self.active_state().buffer.to_string().unwrap_or_default();
419
420 if buffer_content == output {
422 return Ok(());
423 }
424
425 let buffer_len = buffer_content.len();
426
427 let old_cursor_pos = self.active_state().cursors.primary().position;
429 let old_anchor = self.active_state().cursors.primary().anchor;
430 let old_sticky_column = self.active_state().cursors.primary().sticky_column;
431
432 let delete_event = Event::Delete {
434 range: 0..buffer_len,
435 deleted_text: buffer_content,
436 cursor_id,
437 };
438 let insert_event = Event::Insert {
439 position: 0,
440 text: output.to_string(),
441 cursor_id,
442 };
443
444 let new_buffer_len = output.len();
447 let new_cursor_pos = old_cursor_pos.min(new_buffer_len);
448
449 let mut events = vec![delete_event, insert_event];
451 if new_cursor_pos != new_buffer_len {
452 let move_cursor_event = Event::MoveCursor {
453 cursor_id,
454 old_position: new_buffer_len, new_position: new_cursor_pos,
456 old_anchor: None,
457 new_anchor: old_anchor.map(|a| a.min(new_buffer_len)),
458 old_sticky_column: 0,
459 new_sticky_column: old_sticky_column,
460 };
461 events.push(move_cursor_event);
462 }
463
464 let batch = Event::Batch {
466 events,
467 description: "On-save format".to_string(),
468 };
469 self.active_event_log_mut().append(batch.clone());
470 self.apply_event_to_active_buffer(&batch);
471
472 Ok(())
473 }
474
475 pub fn trim_trailing_whitespace(&mut self) -> Result<bool, String> {
478 let content = self.active_state().buffer.to_string().unwrap_or_default();
479
480 let trimmed: String = content
482 .lines()
483 .map(|line| line.trim_end())
484 .collect::<Vec<_>>()
485 .join("\n");
486
487 let trimmed = if content.ends_with('\n') && !trimmed.ends_with('\n') {
489 format!("{}\n", trimmed)
490 } else {
491 trimmed
492 };
493
494 if trimmed == content {
495 return Ok(false);
496 }
497
498 self.replace_buffer_with_output(&trimmed)?;
499 Ok(true)
500 }
501
502 pub fn ensure_final_newline(&mut self) -> Result<bool, String> {
505 let content = self.active_state().buffer.to_string().unwrap_or_default();
506
507 if content.is_empty() {
509 return Ok(false);
510 }
511
512 if content.ends_with('\n') {
513 return Ok(false);
514 }
515
516 let with_newline = format!("{}\n", content);
517 self.replace_buffer_with_output(&with_newline)?;
518 Ok(true)
519 }
520}
521
522fn command_exists(command: &str) -> bool {
524 #[cfg(unix)]
526 {
527 Command::new("which")
528 .arg(command)
529 .stdout(Stdio::null())
530 .stderr(Stdio::null())
531 .status()
532 .map(|s| s.success())
533 .unwrap_or(false)
534 }
535
536 #[cfg(windows)]
537 {
538 Command::new("where")
539 .arg(command)
540 .stdout(Stdio::null())
541 .stderr(Stdio::null())
542 .status()
543 .map(|s| s.success())
544 .unwrap_or(false)
545 }
546
547 #[cfg(not(any(unix, windows)))]
548 {
549 true
551 }
552}
553
554fn detect_shell() -> String {
556 if let Ok(shell) = std::env::var("SHELL") {
558 if !shell.is_empty() {
559 return shell;
560 }
561 }
562
563 #[cfg(unix)]
565 {
566 if std::path::Path::new("/bin/bash").exists() {
567 return "/bin/bash".to_string();
568 }
569 if std::path::Path::new("/bin/sh").exists() {
570 return "/bin/sh".to_string();
571 }
572 }
573
574 #[cfg(windows)]
575 {
576 if let Ok(comspec) = std::env::var("COMSPEC") {
577 return comspec;
578 }
579 return "cmd.exe".to_string();
580 }
581
582 "sh".to_string()
584}