use std::fs;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::Args;
use super::grammars::{
Markers, Model, emacs, input_emacs, operators_nvim, operators_sublime, paths, snippets_emacs,
snippets_nvim, snippets_vscode, sublime, textmate, vim, zed,
};
#[derive(Args)]
pub struct GenGrammarsArgs {
#[arg(long)]
check: bool,
#[arg(short, long)]
verbose: bool,
}
pub fn run(args: GenGrammarsArgs) -> ExitCode {
match run_inner(&args) {
Ok(code) => code,
Err(e) => {
eprintln!("rossi gen-grammars: {e}");
ExitCode::from(1)
}
}
}
fn run_inner(args: &GenGrammarsArgs) -> Result<ExitCode, String> {
let root = workspace_root();
let model = Model::build();
let mut targets: Vec<(String, String)> = vec![
(paths::TEXTMATE.to_string(), textmate::render(&model)),
(paths::SUBLIME.to_string(), sublime::render(&model)),
(
paths::SNIPPETS_VSCODE.to_string(),
snippets_vscode::render(),
),
(paths::NVIM_OPERATORS.to_string(), operators_nvim::render()),
(paths::EMACS_INPUT.to_string(), input_emacs::render()),
(
paths::SUBLIME_OPERATORS.to_string(),
operators_sublime::render(),
),
(paths::TS_TOKENS.to_string(), zed::tokens_manifest(&model)),
];
targets.extend(snippets_nvim::render());
targets.extend(snippets_emacs::render());
for (rel, markers, body) in [
(paths::VIM, &vim::MARKERS, vim::render(&model)),
(paths::EMACS, &emacs::MARKERS, emacs::render(&model)),
(
paths::TS_GRAMMAR,
&zed::MARKERS,
zed::render_grammar_region(&model),
),
(
paths::TS_HIGHLIGHTS,
&zed::MARKERS_SCM,
zed::render_highlights_region(&model),
),
] {
let path = root.join(rel);
let existing = fs::read_to_string(&path).map_err(|e| io_err(&path, e))?;
targets.push((rel.to_string(), splice(&existing, markers, &body, &path)?));
}
for (src, dst) in paths::COPIES {
let content = match targets.iter().find(|(rel, _)| rel == src) {
Some((_, content)) => content.clone(),
None => {
let path = root.join(src);
fs::read_to_string(&path).map_err(|e| io_err(&path, e))?
}
};
targets.push((dst.to_string(), content));
}
let mut stale = 0usize;
for (rel, desired) in &targets {
let path = root.join(rel);
let up_to_date = fs::read_to_string(&path).ok().as_deref() == Some(desired.as_str());
match (args.check, up_to_date) {
(true, true) => {
if args.verbose {
eprintln!("ok {rel}");
}
}
(true, false) => {
println!("{rel}");
stale += 1;
}
(false, true) => {
if args.verbose {
eprintln!("unchanged {rel}");
}
}
(false, false) => {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| io_err(&path, e))?;
}
fs::write(&path, desired).map_err(|e| io_err(&path, e))?;
if args.verbose {
eprintln!("wrote {rel}");
}
}
}
}
let wanted: std::collections::HashSet<&str> =
targets.iter().map(|(rel, _)| rel.as_str()).collect();
for dir in [paths::EMACS_SNIPPETS_DIR, paths::TS_EXAMPLES_DIR] {
let entries = match fs::read_dir(root.join(dir)) {
Ok(entries) => entries,
Err(_) => continue,
};
for entry in entries.flatten() {
let path = entry.path();
let name = entry.file_name();
let name = name.to_string_lossy();
if name.starts_with('.') || !path.is_file() {
continue;
}
let rel = format!("{dir}/{name}");
if wanted.contains(rel.as_str()) {
continue;
}
if args.check {
println!("{rel}");
stale += 1;
} else {
fs::remove_file(&path).map_err(|e| io_err(&path, e))?;
if args.verbose {
eprintln!("removed {rel}");
}
}
}
}
if args.check && stale > 0 {
eprintln!(
"\n{stale} editor grammar(s) are out of date; run `rossi gen-grammars` to regenerate."
);
return Ok(ExitCode::from(1));
}
Ok(ExitCode::SUCCESS)
}
fn splice(existing: &str, markers: &Markers, body: &str, path: &Path) -> Result<String, String> {
let begin_at = existing.find(markers.begin).ok_or_else(|| {
format!(
"{}: missing begin marker `{}`",
path.display(),
markers.begin
)
})?;
let after_begin = begin_at + markers.begin.len();
let end_rel = existing[after_begin..]
.find(markers.end)
.ok_or_else(|| format!("{}: missing end marker `{}`", path.display(), markers.end))?;
let end_at = after_begin + end_rel;
let head = &existing[..after_begin];
let tail = &existing[end_at..];
Ok(format!("{head}\n{body}{tail}"))
}
fn workspace_root() -> PathBuf {
let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let root = crate_dir.join("..").join("..");
fs::canonicalize(&root).unwrap_or(root)
}
fn io_err(path: &Path, e: std::io::Error) -> String {
format!("{}: {e}", path.display())
}