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(
102 &mut out,
103 indent,
104 open,
105 close,
106 interior,
107 splitter,
108 );
109 continue;
110 }
111 }
112 let mut interior = after_open.to_string();
114 let mut close_indent: Option<String> = None;
115 let mut closed = false;
116 for next in iter.by_ref() {
117 if let Some(idx) = next.find(close) {
118 let pre = &next[..idx];
122 let pre_trim = pre.trim();
123 if !pre_trim.is_empty() {
124 if !interior.is_empty() && !interior.ends_with(' ') {
125 interior.push(' ');
126 }
127 interior.push_str(pre_trim);
128 }
129 close_indent = Some(
130 next[..next.len() - next.trim_start().len()].to_string(),
131 );
132 closed = true;
133 break;
134 }
135 let stripped = next.trim_start();
136 let stripped = stripped
139 .strip_prefix("* ")
140 .or_else(|| stripped.strip_prefix('*'))
141 .unwrap_or(stripped);
142 if !interior.is_empty() && !interior.ends_with(' ') {
143 interior.push(' ');
144 }
145 interior.push_str(stripped.trim());
146 }
147 if closed {
148 let ci = close_indent.unwrap_or_else(|| indent.to_string());
149 emit_block_comment_multi(
150 &mut out,
151 indent,
152 open,
153 close,
154 &ci,
155 interior.trim(),
156 splitter,
157 );
158 continue;
159 }
160 out.push_str(line);
163 out.push('\n');
164 if !interior.is_empty() {
165 out.push_str(interior.trim_end());
166 out.push('\n');
167 }
168 continue;
169 }
170 }
171 }
172
173 if let Some(ref marker) = cfg.line_comment {
175 if let Some((indent, rest)) = strip_line_comment(line, marker) {
176 let prose = rest.trim();
177 if prose.is_empty() {
178 out.push_str(line);
179 out.push('\n');
180 continue;
181 }
182 let sentences = splitter.split(prose);
185 if sentences.is_empty() {
186 out.push_str(line);
187 out.push('\n');
188 continue;
189 }
190 for s in &sentences {
191 out.push_str(indent);
192 out.push_str(marker);
193 out.push(' ');
194 out.push_str(s);
195 out.push('\n');
196 }
197 continue;
198 }
199 }
200
201 out.push_str(line);
203 out.push('\n');
204 }
205
206 if !trailing_newline && out.ends_with('\n') {
208 out.pop();
209 }
210 out
211}
212
213fn check_pragma_for(line: &str, cfg: &CodeLang) -> Option<bool> {
217 if let Some(b) = check_pragma(line) {
218 return Some(b);
219 }
220 let trimmed = line.trim();
221 if let Some(ref marker) = cfg.line_comment {
222 if let Some(rest) = trimmed.strip_prefix(marker.as_str()) {
223 let rest = rest.trim();
224 if rest == "snapper:off" {
225 return Some(false);
226 }
227 if rest == "snapper:on" {
228 return Some(true);
229 }
230 }
231 }
232 None
233}
234
235fn split_at_marker<'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 rest.strip_prefix(marker).map(|after| (indent, after))
243}
244
245fn strip_line_comment<'a>(line: &'a str, marker: &str) -> Option<(&'a str, &'a str)> {
248 let leading = line.len() - line.trim_start().len();
249 let (indent, rest) = line.split_at(leading);
250 let after = rest.strip_prefix(marker)?;
251 let after = after.strip_prefix(' ').unwrap_or(after);
255 Some((indent, after))
256}
257
258fn emit_block_comment(
262 out: &mut String,
263 indent: &str,
264 open: &str,
265 close: &str,
266 interior: &str,
267 splitter: &dyn SentenceSplitter,
268) {
269 out.push_str(indent);
270 out.push_str(open);
271 out.push('\n');
272 let sentences = splitter.split(interior.trim());
273 for s in &sentences {
274 out.push_str(indent);
275 out.push(' ');
276 out.push_str(s);
277 out.push('\n');
278 }
279 out.push_str(indent);
280 out.push_str(close);
281 out.push('\n');
282}
283
284fn emit_block_comment_multi(
287 out: &mut String,
288 indent: &str,
289 open: &str,
290 close: &str,
291 close_indent: &str,
292 interior: &str,
293 splitter: &dyn SentenceSplitter,
294) {
295 out.push_str(indent);
296 out.push_str(open);
297 out.push('\n');
298 let sentences = splitter.split(interior);
299 for s in &sentences {
300 out.push_str(indent);
301 out.push(' ');
302 out.push_str(s);
303 out.push('\n');
304 }
305 out.push_str(close_indent);
306 out.push_str(close);
307 out.push('\n');
308}
309
310pub fn run_formatter(body: &str, argv: &[String]) -> Result<String, String> {
319 if argv.is_empty() {
320 return Err("formatter argv is empty".to_string());
321 }
322 let mut cmd = Command::new(&argv[0]);
323 cmd.args(&argv[1..])
324 .stdin(Stdio::piped())
325 .stdout(Stdio::piped())
326 .stderr(Stdio::piped());
327
328 let mut child = match cmd.spawn() {
329 Ok(c) => c,
330 Err(e) => {
331 if e.kind() == std::io::ErrorKind::NotFound {
332 return Err(format!("formatter not found: {}", argv[0]));
333 }
334 return Err(format!("formatter spawn failed: {}: {e}", argv[0]));
335 }
336 };
337
338 if let Some(mut stdin) = child.stdin.take() {
340 let body_owned = body.to_string();
341 let _ = thread::spawn(move || {
342 let _ = stdin.write_all(body_owned.as_bytes());
343 });
345 }
346
347 let (done_tx, done_rx) = mpsc::channel::<()>();
349 let child_id = child.id();
350 let watchdog = thread::spawn(move || {
351 match done_rx.recv_timeout(Duration::from_secs(FORMATTER_TIMEOUT_SECS)) {
352 Ok(_) | Err(mpsc::RecvTimeoutError::Disconnected) => {
353 }
355 Err(mpsc::RecvTimeoutError::Timeout) => {
356 #[cfg(unix)]
359 unsafe {
360 libc_kill(child_id as i32);
361 }
362 #[cfg(not(unix))]
363 {
364 let _ = std::process::Command::new("taskkill")
365 .args(["/F", "/PID", &child_id.to_string()])
366 .output();
367 }
368 }
369 }
370 });
371
372 let output = child.wait_with_output();
375 let _ = done_tx.send(());
377 let _ = watchdog.join();
378
379 let output = match output {
380 Ok(o) => o,
381 Err(e) => return Err(format!("formatter wait failed: {}: {e}", argv[0])),
382 };
383
384 if !output.status.success() {
385 let stderr = String::from_utf8_lossy(&output.stderr);
386 return Err(format!(
387 "formatter {} exited non-zero (status {:?}): {}",
388 argv[0],
389 output.status.code(),
390 stderr.trim()
391 ));
392 }
393
394 String::from_utf8(output.stdout)
395 .map_err(|e| format!("formatter {} produced non-UTF-8 output: {e}", argv[0]))
396}
397
398#[cfg(unix)]
402unsafe fn libc_kill(pid: i32) {
403 unsafe extern "C" {
405 fn kill(pid: i32, sig: i32) -> i32;
406 }
407 const SIGKILL: i32 = 9;
408 unsafe {
409 let _ = kill(pid, SIGKILL);
410 }
411}
412
413#[doc(hidden)]
416pub fn read_to_string(mut r: impl Read) -> std::io::Result<String> {
417 let mut s = String::new();
418 r.read_to_string(&mut s)?;
419 Ok(s)
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425 use crate::sentence::unicode::UnicodeSentenceSplitter;
426
427 fn rust_cfg() -> CodeLang {
428 CodeLang {
429 line_comment: Some("//".to_string()),
430 block_comment: Some(["/*".to_string(), "*/".to_string()]),
431 formatter: None,
432 }
433 }
434
435 #[test]
436 fn line_comment_two_sentences_split() {
437 let body = "// First sentence. Second sentence.\nfn main() {}\n";
438 let out = reflow_comments(body, &rust_cfg(), &UnicodeSentenceSplitter::new());
439 assert_eq!(
440 out,
441 "// First sentence.\n// Second sentence.\nfn main() {}\n"
442 );
443 }
444
445 #[test]
446 fn indented_comment_preserved() {
447 let body = " // First. Second.\n fn x() {}\n";
448 let out = reflow_comments(body, &rust_cfg(), &UnicodeSentenceSplitter::new());
449 assert_eq!(out, " // First.\n // Second.\n fn x() {}\n");
450 }
451
452 #[test]
453 fn non_comment_passes_through() {
454 let body = "fn main() { println!(\"hi\"); }\n";
455 let out = reflow_comments(body, &rust_cfg(), &UnicodeSentenceSplitter::new());
456 assert_eq!(out, body);
457 }
458
459 #[test]
460 fn block_comment_one_liner_splits() {
461 let body = "/* First. Second. */\n";
462 let out = reflow_comments(body, &rust_cfg(), &UnicodeSentenceSplitter::new());
463 assert_eq!(out, "/*\n First.\n Second.\n*/\n");
464 }
465
466 #[test]
467 fn pragma_freezes_run() {
468 let body = "// snapper:off\n// Long.\n// Off.\n// snapper:on\n// Reflow this. Now.\n";
469 let out = reflow_comments(body, &rust_cfg(), &UnicodeSentenceSplitter::new());
470 let expected = concat!(
471 "// snapper:off\n",
472 "// Long.\n",
473 "// Off.\n",
474 "// snapper:on\n",
475 "// Reflow this.\n",
476 "// Now.\n",
477 );
478 assert_eq!(out, expected);
479 }
480}