1use std::io::{Read, Write};
26use std::process::{Command, Stdio};
27use std::sync::mpsc;
28use std::thread;
29use std::time::Duration;
30
31use crate::config::CodeLang;
32use crate::parser::check_pragma;
33use crate::sentence::SentenceSplitter;
34
35pub const FORMATTER_TIMEOUT_SECS: u64 = 30;
37
38pub fn reflow_code_body(
46 body: &str,
47 cfg: &CodeLang,
48 splitter: &dyn SentenceSplitter,
49 format_code: bool,
50) -> String {
51 let after_comment_reflow = reflow_comments(body, cfg, splitter);
52 if format_code {
53 if let Some(ref argv) = cfg.formatter {
54 match run_formatter(&after_comment_reflow, argv) {
55 Ok(out) => return out,
56 Err(diag) => {
57 eprintln!("snapper: {diag}");
58 return after_comment_reflow;
59 }
60 }
61 }
62 }
63 after_comment_reflow
64}
65
66fn reflow_comments(body: &str, cfg: &CodeLang, splitter: &dyn SentenceSplitter) -> String {
68 let mut out = String::with_capacity(body.len());
69 let mut iter = body.lines().peekable();
70 let mut pragma_off = false;
71 let trailing_newline = body.ends_with('\n');
74
75 while let Some(line) = iter.next() {
76 if let Some(on) = check_pragma_for(line, cfg) {
78 pragma_off = !on;
79 out.push_str(line);
80 out.push('\n');
81 continue;
82 }
83 if pragma_off {
84 out.push_str(line);
85 out.push('\n');
86 continue;
87 }
88
89 if let Some(ref pair) = cfg.block_comment {
92 let [open, close] = [pair[0].as_str(), pair[1].as_str()];
93 if !open.is_empty() {
94 if let Some((indent, after_open)) = split_at_marker(line, open) {
95 let trimmed_after = after_open.trim_start();
97 if !close.is_empty() {
98 if let Some(idx) = trimmed_after.find(close) {
99 let interior = &trimmed_after[..idx];
100 emit_block_comment(&mut out, indent, open, close, interior, splitter);
102 continue;
103 }
104 }
105 let mut interior = after_open.to_string();
107 let mut close_indent: Option<String> = None;
108 let mut closed = false;
109 for next in iter.by_ref() {
110 if let Some(idx) = next.find(close) {
111 let pre = &next[..idx];
115 let pre_trim = pre.trim();
116 if !pre_trim.is_empty() {
117 if !interior.is_empty() && !interior.ends_with(' ') {
118 interior.push(' ');
119 }
120 interior.push_str(pre_trim);
121 }
122 close_indent =
123 Some(next[..next.len() - next.trim_start().len()].to_string());
124 closed = true;
125 break;
126 }
127 let stripped = next.trim_start();
128 let stripped = stripped
131 .strip_prefix("* ")
132 .or_else(|| stripped.strip_prefix('*'))
133 .unwrap_or(stripped);
134 if !interior.is_empty() && !interior.ends_with(' ') {
135 interior.push(' ');
136 }
137 interior.push_str(stripped.trim());
138 }
139 if closed {
140 let ci = close_indent.unwrap_or_else(|| indent.to_string());
141 emit_block_comment_multi(
142 &mut out,
143 indent,
144 open,
145 close,
146 &ci,
147 interior.trim(),
148 splitter,
149 );
150 continue;
151 }
152 out.push_str(line);
155 out.push('\n');
156 if !interior.is_empty() {
157 out.push_str(interior.trim_end());
158 out.push('\n');
159 }
160 continue;
161 }
162 }
163 }
164
165 if let Some(ref marker) = cfg.line_comment {
167 if let Some((indent, rest)) = strip_line_comment(line, marker) {
168 let prose = rest.trim();
169 if prose.is_empty() {
170 out.push_str(line);
171 out.push('\n');
172 continue;
173 }
174 let sentences = splitter.split(prose);
177 if sentences.is_empty() {
178 out.push_str(line);
179 out.push('\n');
180 continue;
181 }
182 for s in &sentences {
183 out.push_str(indent);
184 out.push_str(marker);
185 out.push(' ');
186 out.push_str(s);
187 out.push('\n');
188 }
189 continue;
190 }
191 }
192
193 out.push_str(line);
195 out.push('\n');
196 }
197
198 if !trailing_newline && out.ends_with('\n') {
200 out.pop();
201 }
202 out
203}
204
205fn check_pragma_for(line: &str, cfg: &CodeLang) -> Option<bool> {
209 if let Some(b) = check_pragma(line) {
210 return Some(b);
211 }
212 let trimmed = line.trim();
213 if let Some(ref marker) = cfg.line_comment {
214 if let Some(rest) = trimmed.strip_prefix(marker.as_str()) {
215 let rest = rest.trim();
216 if rest == "snapper:off" {
217 return Some(false);
218 }
219 if rest == "snapper:on" {
220 return Some(true);
221 }
222 }
223 }
224 None
225}
226
227fn split_at_marker<'a>(line: &'a str, marker: &str) -> Option<(&'a str, &'a str)> {
232 let leading = line.len() - line.trim_start().len();
233 let (indent, rest) = line.split_at(leading);
234 rest.strip_prefix(marker).map(|after| (indent, after))
235}
236
237fn strip_line_comment<'a>(line: &'a str, marker: &str) -> Option<(&'a str, &'a str)> {
240 let leading = line.len() - line.trim_start().len();
241 let (indent, rest) = line.split_at(leading);
242 let after = rest.strip_prefix(marker)?;
243 let after = after.strip_prefix(' ').unwrap_or(after);
247 Some((indent, after))
248}
249
250fn emit_block_comment(
254 out: &mut String,
255 indent: &str,
256 open: &str,
257 close: &str,
258 interior: &str,
259 splitter: &dyn SentenceSplitter,
260) {
261 out.push_str(indent);
262 out.push_str(open);
263 out.push('\n');
264 let sentences = splitter.split(interior.trim());
265 for s in &sentences {
266 out.push_str(indent);
267 out.push(' ');
268 out.push_str(s);
269 out.push('\n');
270 }
271 out.push_str(indent);
272 out.push_str(close);
273 out.push('\n');
274}
275
276fn emit_block_comment_multi(
279 out: &mut String,
280 indent: &str,
281 open: &str,
282 close: &str,
283 close_indent: &str,
284 interior: &str,
285 splitter: &dyn SentenceSplitter,
286) {
287 out.push_str(indent);
288 out.push_str(open);
289 out.push('\n');
290 let sentences = splitter.split(interior);
291 for s in &sentences {
292 out.push_str(indent);
293 out.push(' ');
294 out.push_str(s);
295 out.push('\n');
296 }
297 out.push_str(close_indent);
298 out.push_str(close);
299 out.push('\n');
300}
301
302pub fn run_formatter(body: &str, argv: &[String]) -> Result<String, String> {
311 if argv.is_empty() {
312 return Err("formatter argv is empty".to_string());
313 }
314 let mut cmd = Command::new(&argv[0]);
315 cmd.args(&argv[1..])
316 .stdin(Stdio::piped())
317 .stdout(Stdio::piped())
318 .stderr(Stdio::piped());
319
320 let mut child = match cmd.spawn() {
321 Ok(c) => c,
322 Err(e) => {
323 if e.kind() == std::io::ErrorKind::NotFound {
324 return Err(format!("formatter not found: {}", argv[0]));
325 }
326 return Err(format!("formatter spawn failed: {}: {e}", argv[0]));
327 }
328 };
329
330 if let Some(mut stdin) = child.stdin.take() {
332 let body_owned = body.to_string();
333 let _ = thread::spawn(move || {
334 let _ = stdin.write_all(body_owned.as_bytes());
335 });
337 }
338
339 let (done_tx, done_rx) = mpsc::channel::<()>();
341 let child_id = child.id();
342 let watchdog = thread::spawn(move || {
343 match done_rx.recv_timeout(Duration::from_secs(FORMATTER_TIMEOUT_SECS)) {
344 Ok(_) | Err(mpsc::RecvTimeoutError::Disconnected) => {
345 }
347 Err(mpsc::RecvTimeoutError::Timeout) => {
348 #[cfg(unix)]
351 unsafe {
352 libc_kill(child_id as i32);
353 }
354 #[cfg(not(unix))]
355 {
356 let _ = std::process::Command::new("taskkill")
357 .args(["/F", "/PID", &child_id.to_string()])
358 .output();
359 }
360 }
361 }
362 });
363
364 let output = child.wait_with_output();
367 let _ = done_tx.send(());
369 let _ = watchdog.join();
370
371 let output = match output {
372 Ok(o) => o,
373 Err(e) => return Err(format!("formatter wait failed: {}: {e}", argv[0])),
374 };
375
376 if !output.status.success() {
377 let stderr = String::from_utf8_lossy(&output.stderr);
378 return Err(format!(
379 "formatter {} exited non-zero (status {:?}): {}",
380 argv[0],
381 output.status.code(),
382 stderr.trim()
383 ));
384 }
385
386 String::from_utf8(output.stdout)
387 .map_err(|e| format!("formatter {} produced non-UTF-8 output: {e}", argv[0]))
388}
389
390#[cfg(unix)]
394unsafe fn libc_kill(pid: i32) {
395 unsafe extern "C" {
397 fn kill(pid: i32, sig: i32) -> i32;
398 }
399 const SIGKILL: i32 = 9;
400 unsafe {
401 let _ = kill(pid, SIGKILL);
402 }
403}
404
405#[doc(hidden)]
408pub fn read_to_string(mut r: impl Read) -> std::io::Result<String> {
409 let mut s = String::new();
410 r.read_to_string(&mut s)?;
411 Ok(s)
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417 use crate::sentence::unicode::UnicodeSentenceSplitter;
418
419 fn rust_cfg() -> CodeLang {
420 CodeLang {
421 line_comment: Some("//".to_string()),
422 block_comment: Some(["/*".to_string(), "*/".to_string()]),
423 formatter: None,
424 }
425 }
426
427 #[test]
428 fn line_comment_two_sentences_split() {
429 let body = "// First sentence. Second sentence.\nfn main() {}\n";
430 let out = reflow_comments(body, &rust_cfg(), &UnicodeSentenceSplitter::new());
431 assert_eq!(
432 out,
433 "// First sentence.\n// Second sentence.\nfn main() {}\n"
434 );
435 }
436
437 #[test]
438 fn indented_comment_preserved() {
439 let body = " // First. Second.\n fn x() {}\n";
440 let out = reflow_comments(body, &rust_cfg(), &UnicodeSentenceSplitter::new());
441 assert_eq!(out, " // First.\n // Second.\n fn x() {}\n");
442 }
443
444 #[test]
445 fn non_comment_passes_through() {
446 let body = "fn main() { println!(\"hi\"); }\n";
447 let out = reflow_comments(body, &rust_cfg(), &UnicodeSentenceSplitter::new());
448 assert_eq!(out, body);
449 }
450
451 #[test]
452 fn block_comment_one_liner_splits() {
453 let body = "/* First. Second. */\n";
454 let out = reflow_comments(body, &rust_cfg(), &UnicodeSentenceSplitter::new());
455 assert_eq!(out, "/*\n First.\n Second.\n*/\n");
456 }
457
458 #[test]
459 fn pragma_freezes_run() {
460 let body = "// snapper:off\n// Long.\n// Off.\n// snapper:on\n// Reflow this. Now.\n";
461 let out = reflow_comments(body, &rust_cfg(), &UnicodeSentenceSplitter::new());
462 let expected = concat!(
463 "// snapper:off\n",
464 "// Long.\n",
465 "// Off.\n",
466 "// snapper:on\n",
467 "// Reflow this.\n",
468 "// Now.\n",
469 );
470 assert_eq!(out, expected);
471 }
472}