use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use std::process::Command;
use syntect::parsing::{SyntaxDefinition, SyntaxSet, SyntaxSetBuilder};
pub const CURATED: &[&str] = &[
"Plain Text",
"Rust",
"Python",
"JavaScript",
"JSON",
"Go",
"C",
"C++",
"Java",
"Ruby",
"PHP",
"Bourne Again Shell (bash)",
"Shell-Unix-Generic",
"YAML",
"Markdown",
"HTML",
"CSS",
"XML",
"SQL",
"Makefile",
"Diff",
];
fn main() {
let git_hash = synaps_repo_hash().unwrap_or_else(|| "release".to_string());
println!("cargo:rustc-env=GIT_HASH={git_hash}");
curated_syntax_dump();
println!("cargo:rerun-if-changed=../../.git/HEAD");
println!("cargo:rerun-if-changed=../../.git/index");
}
fn synaps_repo_hash() -> Option<String> {
let manifest = std::env::var("CARGO_MANIFEST_DIR").ok()?; let ws_root = Path::new(&manifest)
.parent()?
.parent()?
.canonicalize()
.ok()?;
let toplevel = Command::new("git")
.args(["-C", &manifest, "rev-parse", "--show-toplevel"])
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.and_then(|s| Path::new(s.trim()).canonicalize().ok())?;
if toplevel != ws_root {
return None;
}
let hash = Command::new("git")
.args(["-C", &manifest, "rev-parse", "--short", "HEAD"])
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())?;
let dirty = Command::new("git")
.args([
"-C",
&manifest,
"status",
"--porcelain",
"--untracked-files=no",
])
.output()
.ok()
.map(|o| !o.stdout.is_empty())
.unwrap_or(false);
Some(if dirty { format!("{hash}-dirty") } else { hash })
}
fn curated_syntax_dump() {
println!("cargo:rerun-if-env-changed=SYNAPS_TUI_SYNTECT_REPORT");
let out = std::env::var("OUT_DIR").expect("OUT_DIR");
let path = Path::new(&out).join("curated_newlines.packdump");
let full = SyntaxSet::load_defaults_newlines();
let full_unlinked = full.find_unlinked_contexts();
let full_len = syntect::dumps::dump_binary(&full).len();
let defs: Vec<SyntaxDefinition> = full.into_builder().syntaxes().to_vec();
let mut values: Vec<serde_json::Value> = defs
.iter()
.map(|d| serde_json::to_value(d).expect("SyntaxDefinition → json"))
.collect();
let mut keep: BTreeSet<usize> = BTreeSet::new();
let mut work: Vec<usize> = Vec::new();
for name in CURATED {
let idx = defs
.iter()
.position(|d| d.name == *name)
.unwrap_or_else(|| panic!("CURATED grammar {name:?} is not in syntect's default set"));
if keep.insert(idx) {
work.push(idx);
}
}
while let Some(idx) = work.pop() {
let mut refs = BTreeSet::new();
collect_syntax_indices(&values[idx], &mut refs);
for r in refs {
if keep.insert(r) {
work.push(r);
}
}
}
let remap: BTreeMap<usize, usize> = keep.iter().enumerate().map(|(n, &o)| (o, n)).collect();
let mut builder = SyntaxSetBuilder::new();
for &old in &keep {
let mut v = std::mem::take(&mut values[old]);
remap_syntax_indices(&mut v, &remap);
let def: SyntaxDefinition = serde_json::from_value(v).expect("json → SyntaxDefinition");
builder.add(def);
}
let curated = builder.build();
let new_unlinked: Vec<_> = curated
.find_unlinked_contexts()
.difference(&full_unlinked)
.cloned()
.collect();
assert!(
new_unlinked.is_empty(),
"curated syntect set has dangling references (include closure incomplete): {new_unlinked:?}"
);
syntect::dumps::dump_to_uncompressed_file(&curated, &path).expect("write curated packdump");
if std::env::var_os("SYNAPS_TUI_SYNTECT_REPORT").is_some() {
let curated_len = syntect::dumps::dump_binary(&curated).len();
let names: Vec<&str> = curated.syntaxes().iter().map(|s| s.name.as_str()).collect();
println!(
"cargo:warning=syntect curated dump: {} syntaxes / {} bytes vs full {} syntaxes / {} bytes ({:.0}%): {}",
curated.syntaxes().len(),
curated_len,
defs.len(),
full_len,
100.0 * curated_len as f64 / full_len as f64,
names.join(", ")
);
}
}
fn collect_syntax_indices(v: &serde_json::Value, out: &mut BTreeSet<usize>) {
match v {
serde_json::Value::Object(m) => {
if let Some(i) = m.get("syntax_index").and_then(|i| i.as_u64()) {
out.insert(i as usize);
}
m.values().for_each(|x| collect_syntax_indices(x, out));
}
serde_json::Value::Array(a) => a.iter().for_each(|x| collect_syntax_indices(x, out)),
_ => {}
}
}
fn remap_syntax_indices(v: &mut serde_json::Value, remap: &BTreeMap<usize, usize>) {
match v {
serde_json::Value::Object(m) => {
if let Some(i) = m.get("syntax_index").and_then(|i| i.as_u64()) {
let new = remap[&(i as usize)];
m.insert("syntax_index".into(), serde_json::Value::from(new as u64));
}
m.values_mut().for_each(|x| remap_syntax_indices(x, remap));
}
serde_json::Value::Array(a) => a.iter_mut().for_each(|x| remap_syntax_indices(x, remap)),
_ => {}
}
}