use std::io::{Read, Write};
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use crate::config::CodeLang;
use crate::parser::check_pragma;
use crate::sentence::SentenceSplitter;
pub const FORMATTER_TIMEOUT_SECS: u64 = 30;
pub fn reflow_code_body(
body: &str,
cfg: &CodeLang,
splitter: &dyn SentenceSplitter,
format_code: bool,
) -> String {
let after_comment_reflow = reflow_comments(body, cfg, splitter);
if format_code {
if let Some(ref argv) = cfg.formatter {
match run_formatter(&after_comment_reflow, argv) {
Ok(out) => return out,
Err(diag) => {
eprintln!("snapper: {diag}");
return after_comment_reflow;
}
}
}
}
after_comment_reflow
}
fn reflow_comments(body: &str, cfg: &CodeLang, splitter: &dyn SentenceSplitter) -> String {
let mut out = String::with_capacity(body.len());
let mut iter = body.lines().peekable();
let mut pragma_off = false;
let trailing_newline = body.ends_with('\n');
while let Some(line) = iter.next() {
if let Some(on) = check_pragma_for(line, cfg) {
pragma_off = !on;
out.push_str(line);
out.push('\n');
continue;
}
if pragma_off {
out.push_str(line);
out.push('\n');
continue;
}
if let Some(ref pair) = cfg.block_comment {
let [open, close] = [pair[0].as_str(), pair[1].as_str()];
if !open.is_empty() {
if let Some((indent, after_open)) = split_at_marker(line, open) {
let trimmed_after = after_open.trim_start();
if !close.is_empty() {
if let Some(idx) = trimmed_after.find(close) {
let interior = &trimmed_after[..idx];
emit_block_comment(&mut out, indent, open, close, interior, splitter);
continue;
}
}
let mut interior = after_open.to_string();
let mut close_indent: Option<String> = None;
let mut closed = false;
for next in iter.by_ref() {
if let Some(idx) = next.find(close) {
let pre = &next[..idx];
let pre_trim = pre.trim();
if !pre_trim.is_empty() {
if !interior.is_empty() && !interior.ends_with(' ') {
interior.push(' ');
}
interior.push_str(pre_trim);
}
close_indent =
Some(next[..next.len() - next.trim_start().len()].to_string());
closed = true;
break;
}
let stripped = next.trim_start();
let stripped = stripped
.strip_prefix("* ")
.or_else(|| stripped.strip_prefix('*'))
.unwrap_or(stripped);
if !interior.is_empty() && !interior.ends_with(' ') {
interior.push(' ');
}
interior.push_str(stripped.trim());
}
if closed {
let ci = close_indent.unwrap_or_else(|| indent.to_string());
emit_block_comment_multi(
&mut out,
indent,
open,
close,
&ci,
interior.trim(),
splitter,
);
continue;
}
out.push_str(line);
out.push('\n');
if !interior.is_empty() {
out.push_str(interior.trim_end());
out.push('\n');
}
continue;
}
}
}
if let Some(ref marker) = cfg.line_comment {
if let Some((indent, rest)) = strip_line_comment(line, marker) {
let prose = rest.trim();
if prose.is_empty() {
out.push_str(line);
out.push('\n');
continue;
}
let sentences = splitter.split(prose);
if sentences.is_empty() {
out.push_str(line);
out.push('\n');
continue;
}
for s in &sentences {
out.push_str(indent);
out.push_str(marker);
out.push(' ');
out.push_str(s);
out.push('\n');
}
continue;
}
}
out.push_str(line);
out.push('\n');
}
if !trailing_newline && out.ends_with('\n') {
out.pop();
}
out
}
fn check_pragma_for(line: &str, cfg: &CodeLang) -> Option<bool> {
if let Some(b) = check_pragma(line) {
return Some(b);
}
let trimmed = line.trim();
if let Some(ref marker) = cfg.line_comment {
if let Some(rest) = trimmed.strip_prefix(marker.as_str()) {
let rest = rest.trim();
if rest == "snapper:off" {
return Some(false);
}
if rest == "snapper:on" {
return Some(true);
}
}
}
None
}
fn split_at_marker<'a>(line: &'a str, marker: &str) -> Option<(&'a str, &'a str)> {
let leading = line.len() - line.trim_start().len();
let (indent, rest) = line.split_at(leading);
rest.strip_prefix(marker).map(|after| (indent, after))
}
fn strip_line_comment<'a>(line: &'a str, marker: &str) -> Option<(&'a str, &'a str)> {
let leading = line.len() - line.trim_start().len();
let (indent, rest) = line.split_at(leading);
let after = rest.strip_prefix(marker)?;
let after = after.strip_prefix(' ').unwrap_or(after);
Some((indent, after))
}
fn emit_block_comment(
out: &mut String,
indent: &str,
open: &str,
close: &str,
interior: &str,
splitter: &dyn SentenceSplitter,
) {
out.push_str(indent);
out.push_str(open);
out.push('\n');
let sentences = splitter.split(interior.trim());
for s in &sentences {
out.push_str(indent);
out.push(' ');
out.push_str(s);
out.push('\n');
}
out.push_str(indent);
out.push_str(close);
out.push('\n');
}
fn emit_block_comment_multi(
out: &mut String,
indent: &str,
open: &str,
close: &str,
close_indent: &str,
interior: &str,
splitter: &dyn SentenceSplitter,
) {
out.push_str(indent);
out.push_str(open);
out.push('\n');
let sentences = splitter.split(interior);
for s in &sentences {
out.push_str(indent);
out.push(' ');
out.push_str(s);
out.push('\n');
}
out.push_str(close_indent);
out.push_str(close);
out.push('\n');
}
pub fn run_formatter(body: &str, argv: &[String]) -> Result<String, String> {
if argv.is_empty() {
return Err("formatter argv is empty".to_string());
}
let mut cmd = Command::new(&argv[0]);
cmd.args(&argv[1..])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
return Err(format!("formatter not found: {}", argv[0]));
}
return Err(format!("formatter spawn failed: {}: {e}", argv[0]));
}
};
if let Some(mut stdin) = child.stdin.take() {
let body_owned = body.to_string();
let _ = thread::spawn(move || {
let _ = stdin.write_all(body_owned.as_bytes());
});
}
let (done_tx, done_rx) = mpsc::channel::<()>();
let child_id = child.id();
let watchdog = thread::spawn(move || {
match done_rx.recv_timeout(Duration::from_secs(FORMATTER_TIMEOUT_SECS)) {
Ok(_) | Err(mpsc::RecvTimeoutError::Disconnected) => {
}
Err(mpsc::RecvTimeoutError::Timeout) => {
#[cfg(unix)]
unsafe {
libc_kill(child_id as i32);
}
#[cfg(not(unix))]
{
let _ = std::process::Command::new("taskkill")
.args(["/F", "/PID", &child_id.to_string()])
.output();
}
}
}
});
let output = child.wait_with_output();
let _ = done_tx.send(());
let _ = watchdog.join();
let output = match output {
Ok(o) => o,
Err(e) => return Err(format!("formatter wait failed: {}: {e}", argv[0])),
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!(
"formatter {} exited non-zero (status {:?}): {}",
argv[0],
output.status.code(),
stderr.trim()
));
}
String::from_utf8(output.stdout)
.map_err(|e| format!("formatter {} produced non-UTF-8 output: {e}", argv[0]))
}
#[cfg(unix)]
unsafe fn libc_kill(pid: i32) {
unsafe extern "C" {
fn kill(pid: i32, sig: i32) -> i32;
}
const SIGKILL: i32 = 9;
unsafe {
let _ = kill(pid, SIGKILL);
}
}
#[doc(hidden)]
pub fn read_to_string(mut r: impl Read) -> std::io::Result<String> {
let mut s = String::new();
r.read_to_string(&mut s)?;
Ok(s)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sentence::unicode::UnicodeSentenceSplitter;
fn rust_cfg() -> CodeLang {
CodeLang {
line_comment: Some("//".to_string()),
block_comment: Some(["/*".to_string(), "*/".to_string()]),
formatter: None,
}
}
#[test]
fn line_comment_two_sentences_split() {
let body = "// First sentence. Second sentence.\nfn main() {}\n";
let out = reflow_comments(body, &rust_cfg(), &UnicodeSentenceSplitter::new());
assert_eq!(
out,
"// First sentence.\n// Second sentence.\nfn main() {}\n"
);
}
#[test]
fn indented_comment_preserved() {
let body = " // First. Second.\n fn x() {}\n";
let out = reflow_comments(body, &rust_cfg(), &UnicodeSentenceSplitter::new());
assert_eq!(out, " // First.\n // Second.\n fn x() {}\n");
}
#[test]
fn non_comment_passes_through() {
let body = "fn main() { println!(\"hi\"); }\n";
let out = reflow_comments(body, &rust_cfg(), &UnicodeSentenceSplitter::new());
assert_eq!(out, body);
}
#[test]
fn block_comment_one_liner_splits() {
let body = "/* First. Second. */\n";
let out = reflow_comments(body, &rust_cfg(), &UnicodeSentenceSplitter::new());
assert_eq!(out, "/*\n First.\n Second.\n*/\n");
}
#[test]
fn pragma_freezes_run() {
let body = "// snapper:off\n// Long.\n// Off.\n// snapper:on\n// Reflow this. Now.\n";
let out = reflow_comments(body, &rust_cfg(), &UnicodeSentenceSplitter::new());
let expected = concat!(
"// snapper:off\n",
"// Long.\n",
"// Off.\n",
"// snapper:on\n",
"// Reflow this.\n",
"// Now.\n",
);
assert_eq!(out, expected);
}
}