use std::io::Write;
use std::process::Command;
use std::sync::OnceLock;
const FORMATTER_COMMAND: &str = "air";
static FORMATTER_AVAILABLE: OnceLock<bool> = OnceLock::new();
pub fn is_formatter_available() -> bool {
*FORMATTER_AVAILABLE.get_or_init(|| {
Command::new(FORMATTER_COMMAND)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
})
}
pub fn format_code(code: &str) -> String {
if code.trim().is_empty() {
return code.to_string();
}
if !is_formatter_available() {
log::debug!(
"Formatter '{}' not available, skipping format",
FORMATTER_COMMAND
);
return code.to_string();
}
match format_via_temp_file(code) {
Ok(formatted) => formatted,
Err(e) => {
log::debug!("Formatting failed: {}, using original code", e);
code.to_string()
}
}
}
fn format_via_temp_file(code: &str) -> Result<String, FormatterError> {
let temp_dir = std::env::temp_dir();
let temp_path = temp_dir.join("arf-format.R");
let mut file = std::fs::File::create(&temp_path)?;
file.write_all(code.as_bytes())?;
file.flush()?;
drop(file);
let output = Command::new(FORMATTER_COMMAND)
.arg("format")
.arg(&temp_path)
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("Parse") || stderr.contains("parse") {
log::trace!(
"Formatter parse error (expected for incomplete code): {}",
stderr
);
} else {
log::debug!("Formatter returned error: {}", stderr);
}
return Err(FormatterError::FormatFailed(stderr.to_string()));
}
let formatted = std::fs::read_to_string(&temp_path)?;
let _ = std::fs::remove_file(&temp_path);
let formatted = if !code.ends_with('\n') && formatted.ends_with('\n') {
formatted.trim_end_matches('\n').to_string()
} else {
formatted
};
Ok(formatted)
}
#[derive(Debug)]
enum FormatterError {
Io(std::io::Error),
FormatFailed(String),
}
impl From<std::io::Error> for FormatterError {
fn from(e: std::io::Error) -> Self {
FormatterError::Io(e)
}
}
impl std::fmt::Display for FormatterError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FormatterError::Io(e) => write!(f, "I/O error: {}", e),
FormatterError::FormatFailed(msg) => write!(f, "Format failed: {}", msg),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_empty_code() {
let result = format_code("");
assert_eq!(result, "");
let result = format_code(" ");
assert_eq!(result, " ");
}
#[test]
#[ignore] fn test_format_simple_assignment() {
let code = "x<-1+2";
let result = format_code(code);
assert_eq!(result, "x <- 1 + 2");
}
#[test]
#[ignore] fn test_format_function_definition() {
let code = "f=function(x,y){x+y}";
let result = format_code(code);
assert!(result.contains("function(x, y)"));
assert!(result.contains("x + y"));
}
#[test]
#[ignore] fn test_format_preserves_trailing_newline_style() {
let code = "x <- 1";
let result = format_code(code);
assert!(!result.ends_with('\n'));
let code = "x <- 1\n";
let result = format_code(code);
assert!(result.ends_with('\n'));
}
}