use anyhow::{Context, Result};
use libloading::Library;
use tree_sitter::Language;
use tree_sitter_language::LanguageFn;
use super::loader::GrammarLoader;
use super::manifest::{LangSpec, ManifestMeta};
pub struct Grammar {
name: String,
language: Language,
highlights_scm: String,
injections_scm: Option<String>,
_lib: Library,
}
impl Grammar {
pub fn name(&self) -> &str {
&self.name
}
pub fn language(&self) -> &Language {
&self.language
}
pub fn highlights_scm(&self) -> &str {
&self.highlights_scm
}
pub fn injections_scm(&self) -> Option<&str> {
self.injections_scm.as_deref()
}
pub fn load(
name: &str,
spec: &LangSpec,
loader: &GrammarLoader,
meta: &ManifestMeta,
) -> Result<Self> {
let so = loader
.load(name, spec, meta)
.with_context(|| format!("resolve grammar {name}"))?;
let lib =
unsafe { Library::new(&so) }.with_context(|| format!("dlopen {}", so.display()))?;
let symbol = format!("tree_sitter_{}", symbol_name(name));
let raw: unsafe extern "C" fn() -> *const () = unsafe {
*lib.get(symbol.as_bytes())
.with_context(|| format!("missing symbol `{symbol}` in {}", so.display()))?
};
let lang_fn = unsafe { LanguageFn::from_raw(raw) };
let language = Language::from(lang_fn);
let parent = so
.parent()
.with_context(|| format!("grammar {} has no parent dir", so.display()))?;
let highlights_path = parent.join(format!("{name}.scm"));
let highlights_scm = std::fs::read_to_string(&highlights_path).with_context(|| {
format!(
"read highlights query for {name} at {}",
highlights_path.display()
)
})?;
let injections_path = parent.join(format!("{name}.injections.scm"));
let injections_scm = match std::fs::read_to_string(&injections_path) {
Ok(s) => Some(s),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
Err(e) => {
return Err(e).with_context(|| {
format!(
"read injections query for {name} at {}",
injections_path.display()
)
});
}
};
Ok(Self {
name: name.to_string(),
language,
highlights_scm,
injections_scm,
_lib: lib,
})
}
pub fn load_from_path(name: &str, so: &std::path::Path) -> Result<Self> {
let lib =
unsafe { Library::new(so) }.with_context(|| format!("dlopen {}", so.display()))?;
let symbol = format!("tree_sitter_{}", symbol_name(name));
let raw: unsafe extern "C" fn() -> *const () = unsafe {
*lib.get(symbol.as_bytes())
.with_context(|| format!("missing symbol `{symbol}` in {}", so.display()))?
};
let lang_fn = unsafe { LanguageFn::from_raw(raw) };
let language = Language::from(lang_fn);
let parent = so
.parent()
.with_context(|| format!("grammar {} has no parent dir", so.display()))?;
let highlights_path = parent.join(format!("{name}.scm"));
let highlights_scm = std::fs::read_to_string(&highlights_path).with_context(|| {
format!(
"read highlights query for {name} at {}",
highlights_path.display()
)
})?;
let injections_path = parent.join(format!("{name}.injections.scm"));
let injections_scm = match std::fs::read_to_string(&injections_path) {
Ok(s) => Some(s),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
Err(e) => {
return Err(e).with_context(|| {
format!(
"read injections query for {name} at {}",
injections_path.display()
)
});
}
};
Ok(Self {
name: name.to_string(),
language,
highlights_scm,
injections_scm,
_lib: lib,
})
}
pub unsafe fn from_parts(
name: impl Into<String>,
lib: Library,
language: Language,
highlights_scm: impl Into<String>,
injections_scm: Option<impl Into<String>>,
) -> Self {
Self {
name: name.into(),
language,
highlights_scm: highlights_scm.into(),
injections_scm: injections_scm.map(|s| s.into()),
_lib: lib,
}
}
}
fn symbol_name(name: &str) -> String {
name.replace('-', "_")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn symbol_name_normalizes_hyphens() {
assert_eq!(symbol_name("rust"), "rust");
assert_eq!(symbol_name("c-sharp"), "c_sharp");
assert_eq!(symbol_name("html-erb"), "html_erb");
}
#[test]
#[ignore = "network + compiler: clones tree-sitter-c + helix queries, builds, installs, dlopens"]
fn load_real_grammar_end_to_end() {
use super::super::compile::GrammarCompiler;
use super::super::manifest::{ManifestMeta, QuerySource};
use super::super::source::{QuerySourceCache, SourceCache};
let tmp = tempfile::tempdir().unwrap();
let sources = SourceCache::new(tmp.path().join("cache"));
let query_sources = QuerySourceCache::new(tmp.path().join("qcache"));
let user_dir = tmp.path().join("user");
let loader = GrammarLoader::new(
vec![],
user_dir,
sources,
query_sources,
GrammarCompiler::new(),
);
let meta = ManifestMeta {
helix_repo: "https://github.com/helix-editor/helix".into(),
helix_rev: "87d5c05c4432a079d3b7aaa10cda1cfe1803c18c".into(),
nvim_treesitter_repo: "https://github.com/nvim-treesitter/nvim-treesitter".into(),
nvim_treesitter_rev: "cf12346a3414fa1b06af75c79faebe7f76df080a".into(),
};
let spec = LangSpec {
git_url: "https://github.com/tree-sitter/tree-sitter-c".into(),
git_rev: "2a265d69a4caf57108a73ad2ed1e6922dd2f998c".into(),
subpath: None,
extensions: vec!["c".into()],
c_files: vec!["src/parser.c".into()],
query_source: QuerySource::Helix,
query_subdir: None,
source: None,
};
let grammar = Grammar::load("c", &spec, &loader, &meta).unwrap();
assert_eq!(grammar.name(), "c");
let q = tree_sitter::Query::new(grammar.language(), grammar.highlights_scm());
assert!(q.is_ok(), "highlights.scm failed to compile: {:?}", q.err());
}
}