use std::collections::{BTreeMap, HashMap};
use std::path::Path;
use anyhow::{Context, Result};
use ignore::WalkBuilder;
use crate::{fetch, registry};
pub fn provision_project(root: &Path, dry_run: bool) -> Result<Vec<String>> {
let (ext_map, online) = extension_map();
let mut counts: BTreeMap<String, usize> = BTreeMap::new();
for entry in WalkBuilder::new(root).build().flatten() {
let p = entry.path();
if p.is_file() {
if let Some(ext) = p.extension().and_then(|e| e.to_str()) {
if let Some(lang) = ext_map.get(ext) {
*counts.entry(lang.clone()).or_default() += 1;
}
}
}
}
if counts.is_empty() {
println!(" no files matched a known grammar.");
println!(" nothing to do.");
return Ok(Vec::new());
}
for (lang, n) in &counts {
println!(" detected {:<10} {} files", lang, n);
}
let langs: Vec<String> = counts.keys().cloned().collect();
if dry_run {
println!("\n (dry run — no files written)");
return Ok(Vec::new());
}
let missing: Vec<String> = langs.iter().filter(|l| !is_cached(l)).cloned().collect();
if !missing.is_empty() {
if online {
println!("\n fetching {} grammar(s): {}\n", missing.len(), missing.join(", "));
fetch::run(&missing, false).context("auto-fetching detected grammars")?;
} else {
println!(
"\n note {} not cached: {}\n offline — run `grove fetch {}` to enable them.",
if missing.len() == 1 { "language" } else { "languages" },
missing.join(", "),
missing.join(" "),
);
}
}
let langs: Vec<String> = langs.into_iter().filter(|l| is_cached(l)).collect();
if langs.is_empty() {
println!("\n no grammars available — nothing written.");
return Ok(Vec::new());
}
let n = registry::write_lock_for(&langs, &root.join("grove.lock"))?;
Ok(vec![format!("grove.lock ({n} grammars)")])
}
fn extension_map() -> (HashMap<String, String>, bool) {
match fetch::catalog_grammars() {
Ok(grammars) => {
let mut m = HashMap::new();
for g in &grammars {
for ext in &g.extensions {
m.insert(ext.clone(), g.name.clone());
}
}
(m, true)
}
Err(e) => {
eprintln!(" note: catalog unavailable ({e}); detecting from cached grammars only.");
let mut m = HashMap::new();
for man in registry::manifests() {
for ext in &man.extensions {
m.insert(ext.clone(), man.name.clone());
}
}
(m, false)
}
}
}
fn is_cached(lang: &str) -> bool {
registry::cache_root().is_some_and(|c| c.join(lang).join("grammar.wasm").exists())
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp(tag: &str) -> std::path::PathBuf {
let dir =
std::env::temp_dir().join(format!("grove_provision_test_{}_{tag}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn provision_empty_project_writes_nothing_and_returns_empty() {
let dir = tmp("empty");
let wrote = provision_project(&dir, false).unwrap();
assert!(wrote.is_empty(), "no files matched → no provisioning: {wrote:?}");
assert!(!dir.join("grove.lock").exists(), "no lock written on empty short-circuit");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn provision_dry_run_writes_no_lock_and_returns_empty() {
let dir = tmp("dry_run");
std::fs::write(dir.join("main.rs"), "fn main() {}\n").unwrap();
let wrote = provision_project(&dir, true).unwrap();
assert!(wrote.is_empty(), "dry-run provisions nothing: {wrote:?}");
assert!(!dir.join("grove.lock").exists(), "dry-run writes no lock");
std::fs::remove_dir_all(&dir).ok();
}
}