use std::path::Path;
use anyhow::{Result, bail};
use crate::config::{self, Config};
use super::assets;
use super::build;
use super::recipe::{builtin_recipes, find_recipe};
pub fn run(args: &[String]) -> Result<()> {
let cfg = Config::load(config::default_path().as_deref())?;
let grammar_dir = cfg.grammar_dir.as_path();
let query_dir = cfg.query_dir.as_path();
match args.split_first() {
None => {
print_usage();
Ok(())
}
Some((cmd, rest)) => match cmd.as_str() {
"list" | "ls" => list(grammar_dir, query_dir),
"install" | "add" => install(rest, grammar_dir, query_dir),
"install-queries" | "refresh-queries" => install_queries(rest, query_dir),
"remove" | "rm" | "uninstall" => remove(rest, grammar_dir),
"help" | "-h" | "--help" => {
print_usage();
Ok(())
}
other => {
print_usage();
bail!("unknown grammar subcommand: `{}`", other);
}
},
}
}
fn print_usage() {
eprintln!("usage: vorto grammar <command> [args]");
eprintln!();
eprintln!("commands:");
eprintln!(" list show built-in recipes and install status");
eprintln!(" install <name>... | --all build and install one or more grammars");
eprintln!(" install-queries <name>... | --all overwrite installed .scm files from the");
eprintln!(" vendored bundle (no library rebuild)");
eprintln!(" remove <name>... delete installed grammar libraries");
eprintln!();
eprintln!("examples:");
eprintln!(" vorto grammar install rust python");
eprintln!(" vorto grammar install --all");
eprintln!(" vorto grammar install-queries python");
eprintln!(" vorto grammar list");
}
fn list(grammar_dir: &Path, query_dir: &Path) -> Result<()> {
println!("grammar dir: {}", grammar_dir.display());
println!("query dir: {}", query_dir.display());
println!();
for r in builtin_recipes() {
let lib_status = match build::installed_path(r.name, grammar_dir) {
Some(_) => "lib ✓",
None => "lib ✗",
};
let installed = build::installed_queries(r.name, query_dir);
let bundled = assets::bundled_query_names(r.name);
let query_status = match (installed.is_empty(), bundled.is_empty()) {
(false, _) => format!("queries: {} (installed)", installed.join(",")),
(true, false) => format!("queries: {} (bundled, not installed)", bundled.join(",")),
(true, true) => "queries: none bundled".to_string(),
};
let subpath = r.subpath.map(|s| format!(" [{}]", s)).unwrap_or_default();
println!(
" {:<12} {}{}\n {} | {}",
r.name, r.repo, subpath, lib_status, query_status
);
}
Ok(())
}
fn install(args: &[String], grammar_dir: &Path, query_dir: &Path) -> Result<()> {
let recipes = match args.first().map(String::as_str) {
None => {
bail!("install: need at least one grammar name (or `--all`)");
}
Some("--all") => builtin_recipes(),
_ => {
let mut out = Vec::new();
for name in args {
match find_recipe(name) {
Some(r) => out.push(r),
None => bail!(
"unknown grammar `{}`. Try `vorto grammar list` to see built-ins.",
name
),
}
}
out
}
};
let mut pending = Vec::new();
for r in &recipes {
if build::is_fully_installed(r.name, grammar_dir, query_dir) {
eprintln!("==> {} already installed, skipping", r.name);
} else {
pending.push(r.clone());
}
}
if pending.is_empty() {
return Ok(());
}
let workers = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4)
.min(pending.len());
let next = std::sync::atomic::AtomicUsize::new(0);
let stderr_lock = std::sync::Mutex::new(());
let failures = std::sync::Mutex::new(Vec::<&str>::new());
std::thread::scope(|s| {
for _ in 0..workers {
s.spawn(|| {
loop {
let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if i >= pending.len() {
break;
}
let r = &pending[i];
let result = build::install(r, grammar_dir, query_dir);
let mut buf = String::new();
use std::fmt::Write;
writeln!(buf, "==> installing {} ({})", r.name, r.repo).ok();
match result {
Ok(report) => {
writeln!(buf, " lib: {}", report.library.display()).ok();
if report.queries.is_empty() {
writeln!(buf, " queries: none shipped in upstream `queries/`")
.ok();
} else {
let names: Vec<String> = report
.queries
.iter()
.filter_map(|p| {
p.file_name().map(|n| n.to_string_lossy().into_owned())
})
.collect();
writeln!(
buf,
" queries: {} ({} files)",
names.join(", "),
report.queries.len()
)
.ok();
}
}
Err(e) => {
writeln!(buf, " failed: {:#}", e).ok();
failures.lock().unwrap().push(r.name);
}
}
let _g = stderr_lock.lock().unwrap();
eprint!("{}", buf);
}
});
}
});
let failures = failures.into_inner().unwrap();
if !failures.is_empty() {
bail!("failed to install: {}", failures.join(", "));
}
Ok(())
}
fn install_queries(args: &[String], query_dir: &Path) -> Result<()> {
let recipes = match args.first().map(String::as_str) {
None => {
bail!("install-queries: need at least one grammar name (or `--all`)");
}
Some("--all") => builtin_recipes(),
_ => {
let mut out = Vec::new();
for name in args {
match find_recipe(name) {
Some(r) => out.push(r),
None => bail!(
"unknown grammar `{}`. Try `vorto grammar list` to see built-ins.",
name
),
}
}
out
}
};
let mut failures = Vec::new();
for r in &recipes {
eprintln!("==> refreshing queries for {}", r.name);
match build::write_vendored_queries(query_dir, r.name) {
Ok(written) if written.is_empty() => {
eprintln!(" queries: none bundled for `{}`", r.name);
}
Ok(written) => {
let names: Vec<String> = written
.iter()
.filter_map(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
.collect();
eprintln!(
" queries: {} ({} files)",
names.join(", "),
written.len()
);
}
Err(e) => {
eprintln!(" failed: {:#}", e);
failures.push(r.name);
}
}
}
if !failures.is_empty() {
bail!("failed to refresh: {}", failures.join(", "));
}
Ok(())
}
fn remove(args: &[String], grammar_dir: &Path) -> Result<()> {
if args.is_empty() {
bail!("remove: need at least one grammar name");
}
for name in args {
let removed = build::remove(name, grammar_dir)?;
if removed {
eprintln!("removed: {}", name);
} else {
eprintln!("not installed: {}", name);
}
}
Ok(())
}