use std::path::{Path, PathBuf};
use std::process::Command;
use super::cli::FmtArgs;
pub(crate) fn run_fmt_cmd(args: FmtArgs) {
let roots = if args.paths.is_empty() {
vec![std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))]
} else {
args.paths.clone()
};
let mut sources = Vec::new();
for root in &roots {
collect(root, &mut sources);
}
sources.sort();
sources.dedup();
let (mut changed, mut failed) = (Vec::new(), Vec::new());
for path in &sources {
match format_file(path, args.check) {
Ok(true) => changed.push(path.clone()),
Ok(false) => {}
Err(e) => failed.push((path.clone(), e)),
}
}
for (path, error) in &failed {
eprintln!("[cargo-telar] {}: {error}", display(path));
}
if args.check {
for path in &changed {
println!("{}", display(path));
}
let verdict = match (changed.len(), failed.len()) {
(0, 0) => return println!("[cargo-telar] {} files already formatted", sources.len()),
(n, 0) => format!("{n} file(s) need formatting"),
(0, f) => format!("{f} file(s) could not be read"),
(n, f) => format!("{n} file(s) need formatting, {f} could not be read"),
};
eprintln!("[cargo-telar] {verdict}");
std::process::exit(1);
}
println!(
"[cargo-telar] formatted {} of {} files",
changed.len(),
sources.len()
);
if !failed.is_empty() {
std::process::exit(1);
}
}
fn collect(root: &Path, out: &mut Vec<PathBuf>) {
if root.is_file() {
if is_source(root) {
out.push(root.to_path_buf());
}
return;
}
let Ok(entries) = std::fs::read_dir(root) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let name = entry.file_name();
let name = name.to_string_lossy();
if path.is_dir() {
if name.starts_with('.') || name == "target" {
continue;
}
collect(&path, out);
} else if is_source(&path) {
out.push(path);
}
}
}
fn is_source(path: &Path) -> bool {
matches!(
path.extension().and_then(|e| e.to_str()),
Some("rsx") | Some("rs")
)
}
fn format_file(path: &Path, check: bool) -> Result<bool, String> {
let source = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
let formatted = match path.extension().and_then(|e| e.to_str()) {
Some("rsx") => match telar_parser::format::format_document(&source) {
Some(formatted) => formatted,
None => return Ok(false),
},
_ => rustfmt(&source)?,
};
if formatted == source {
return Ok(false);
}
if !check {
std::fs::write(path, &formatted).map_err(|e| e.to_string())?;
}
Ok(true)
}
fn rustfmt(source: &str) -> Result<String, String> {
use std::io::Write;
let mut child = Command::new("rustfmt")
.args(["--edition", "2024", "--emit", "stdout", "--quiet"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(|e| format!("could not run rustfmt: {e}"))?;
child
.stdin
.take()
.ok_or("rustfmt took no stdin")?
.write_all(source.as_bytes())
.map_err(|e| e.to_string())?;
let out = child.wait_with_output().map_err(|e| e.to_string())?;
if !out.status.success() {
return Err(String::from_utf8_lossy(&out.stderr).trim().to_string());
}
String::from_utf8(out.stdout).map_err(|e| e.to_string())
}
fn display(path: &Path) -> String {
std::env::current_dir()
.ok()
.and_then(|cwd| path.strip_prefix(cwd).ok().map(Path::to_path_buf))
.unwrap_or_else(|| path.to_path_buf())
.display()
.to_string()
}