use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, anyhow};
use libloading::{Library, Symbol};
use tree_sitter::Language;
use crate::config::Language as LangSpec;
use super::highlight::Highlighter;
pub struct Loader {
grammar_dir: PathBuf,
query_dir: PathBuf,
libs: HashMap<String, Library>,
languages: HashMap<String, Language>,
}
impl Loader {
pub fn new(grammar_dir: PathBuf, query_dir: PathBuf) -> Self {
Self {
grammar_dir,
query_dir,
libs: HashMap::new(),
languages: HashMap::new(),
}
}
pub fn highlighter_for(&mut self, spec: &LangSpec) -> Result<Highlighter> {
let lang = self.load_language(spec)?;
let highlights_src = self.read_query(spec, "highlights")?;
let textobjects_src = self.read_query(spec, "textobjects").ok();
let indents_src = self.read_query(spec, "indents").ok();
Highlighter::new(
lang,
&highlights_src,
textobjects_src.as_deref(),
indents_src.as_deref(),
)
}
fn load_language(&mut self, spec: &LangSpec) -> Result<Language> {
if let Some(lang) = self.languages.get(&spec.grammar) {
return Ok(lang.clone());
}
let dir = spec.grammar_dir.as_ref().unwrap_or(&self.grammar_dir);
let path = library_path(dir, &spec.grammar)
.with_context(|| format!("locating grammar `{}`", spec.grammar))?;
let lib = unsafe { Library::new(&path) }
.with_context(|| format!("loading grammar library {}", path.display()))?;
let symbol_name = format!("tree_sitter_{}", spec.grammar.replace('-', "_"));
let language = unsafe {
let sym: Symbol<unsafe extern "C" fn() -> Language> =
lib.get(symbol_name.as_bytes()).with_context(|| {
format!("symbol `{}` missing in {}", symbol_name, path.display())
})?;
sym()
};
self.libs.insert(spec.grammar.clone(), lib);
self.languages
.insert(spec.grammar.clone(), language.clone());
Ok(language)
}
fn read_query(&self, spec: &LangSpec, kind: &str) -> Result<String> {
let base = spec.query_dir.as_ref().unwrap_or(&self.query_dir);
let mut visited = HashSet::new();
read_query_recursive(base, &spec.name, kind, &mut visited)
}
}
fn read_query_recursive(
base: &Path,
lang_name: &str,
kind: &str,
visited: &mut HashSet<String>,
) -> Result<String> {
if !visited.insert(lang_name.to_string()) {
return Ok(String::new());
}
let path = base.join(lang_name).join(format!("{}.scm", kind));
let content = std::fs::read_to_string(&path)
.with_context(|| format!("reading query {}", path.display()))?;
let inherits = parse_inherits(&content);
if inherits.is_empty() {
return Ok(content);
}
let mut combined = String::new();
for inh in inherits {
match read_query_recursive(base, &inh, kind, visited) {
Ok(s) if !s.is_empty() => {
combined.push_str(&s);
if !combined.ends_with('\n') {
combined.push('\n');
}
}
Ok(_) => {}
Err(e) => {
eprintln!(
"inherited query `{}/{}.scm` (from `{}`) skipped: {:#}",
inh, kind, lang_name, e
);
}
}
}
combined.push_str(&content);
Ok(combined)
}
fn parse_inherits(content: &str) -> Vec<String> {
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if !trimmed.starts_with(';') {
return Vec::new();
}
let stripped = trimmed.trim_start_matches(';').trim();
if let Some(rest) = stripped.strip_prefix("inherits:") {
return rest
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
}
Vec::new()
}
fn library_path(dir: &Path, name: &str) -> Result<PathBuf> {
let candidates = [
format!("{}.so", name),
format!("{}.dylib", name),
format!("{}.dll", name),
format!("lib{}.so", name),
format!("lib{}.dylib", name),
];
for c in &candidates {
let p = dir.join(c);
if p.exists() {
return Ok(p);
}
}
Err(anyhow!(
"no grammar library for `{}` in {}",
name,
dir.display()
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn library_path_reports_missing() {
let dir = std::env::temp_dir();
let result = library_path(&dir, "definitely-not-a-real-grammar");
assert!(result.is_err());
}
#[test]
fn parse_inherits_picks_up_single_lang() {
let src = "; inherits: javascript\n\n(identifier) @variable\n";
assert_eq!(parse_inherits(src), vec!["javascript".to_string()]);
}
#[test]
fn parse_inherits_handles_double_semicolon_and_multiple_langs() {
let src = ";; inherits: ecma, jsx\n";
assert_eq!(
parse_inherits(src),
vec!["ecma".to_string(), "jsx".to_string()]
);
}
#[test]
fn parse_inherits_skips_blank_leading_lines() {
let src = "\n\n; inherits: rust\n";
assert_eq!(parse_inherits(src), vec!["rust".to_string()]);
}
#[test]
fn parse_inherits_returns_empty_when_no_header() {
let src = "; Types\n(identifier) @variable\n";
assert!(parse_inherits(src).is_empty());
}
#[test]
fn parse_inherits_stops_at_first_non_comment() {
let src = "(identifier) @variable\n; inherits: foo\n";
assert!(parse_inherits(src).is_empty());
}
#[test]
fn read_query_recursive_prepends_inherited() {
let dir = std::env::temp_dir().join("vorto_inherits_test");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("parent")).unwrap();
std::fs::create_dir_all(dir.join("child")).unwrap();
std::fs::write(dir.join("parent/highlights.scm"), "PARENT\n").unwrap();
std::fs::write(
dir.join("child/highlights.scm"),
"; inherits: parent\nCHILD\n",
)
.unwrap();
let mut visited = HashSet::new();
let out = read_query_recursive(&dir, "child", "highlights", &mut visited).unwrap();
assert!(out.contains("PARENT"));
assert!(out.contains("CHILD"));
assert!(out.find("PARENT").unwrap() < out.find("CHILD").unwrap());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn read_query_recursive_breaks_cycles() {
let dir = std::env::temp_dir().join("vorto_inherits_cycle");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("a")).unwrap();
std::fs::create_dir_all(dir.join("b")).unwrap();
std::fs::write(dir.join("a/highlights.scm"), "; inherits: b\nA\n").unwrap();
std::fs::write(dir.join("b/highlights.scm"), "; inherits: a\nB\n").unwrap();
let mut visited = HashSet::new();
let out = read_query_recursive(&dir, "a", "highlights", &mut visited).unwrap();
assert!(out.contains("A"));
assert!(out.contains("B"));
let _ = std::fs::remove_dir_all(&dir);
}
}