pub mod error;
pub(crate) mod extensions;
pub(crate) mod intel;
#[cfg(feature = "serde")]
#[doc(hidden)]
pub mod json_utils;
pub mod pack_config;
pub(crate) mod parse;
pub mod parsing;
pub mod process_config;
pub(crate) mod queries;
pub mod query_cache;
pub mod registry;
pub(crate) mod text_splitter;
#[cfg(feature = "config")]
pub(crate) mod definitions;
#[cfg(feature = "download")]
pub mod download;
pub use error::Error;
pub use extensions::{detect_language_from_content, detect_language_from_extension, detect_language_from_path};
pub use intel::types::{
ChunkContext, CodeChunk, CommentInfo, CommentKind, DataAttribute, DataNode, DataNodeKind, Diagnostic,
DiagnosticSeverity, DocSection, DocstringFormat, DocstringInfo, ExportInfo, ExportKind, FileMetrics, ImportInfo,
ProcessResult, Span, StructureItem, StructureKind, SymbolInfo, SymbolKind,
};
pub use pack_config::{PackConfig, TlsRootsMode};
pub use parsing::{ByteRange, Node, Parser, Point, Tree, TreeCursor};
pub use process_config::ProcessConfig;
pub use queries::{
get_folds_query, get_highlights_query, get_indents_query, get_injections_query, get_locals_query, get_tags_query,
};
pub use query_cache::{QueryKind, get_query};
pub use registry::LanguageRegistry;
pub use tree_sitter::Language;
#[cfg(feature = "download")]
pub use download::DownloadManager;
use std::sync::LazyLock;
#[cfg(feature = "download")]
use std::sync::{Mutex, RwLock};
static REGISTRY: LazyLock<LanguageRegistry> = LazyLock::new(LanguageRegistry::new);
pub(crate) fn recover_poisoned_lock<'a>(
lock_name: &str,
poisoned: std::sync::PoisonError<std::sync::MutexGuard<'a, ()>>,
) -> std::sync::MutexGuard<'a, ()> {
tracing::warn!(
lock = lock_name,
"recovered a poisoned lock after a panicking critical section"
);
poisoned.into_inner()
}
#[cfg(feature = "download")]
static REGISTERED_CACHE_DIR: LazyLock<RwLock<Option<std::path::PathBuf>>> = LazyLock::new(|| RwLock::new(None));
#[cfg(feature = "download")]
static CUSTOM_CACHE_DIR: LazyLock<RwLock<Option<std::path::PathBuf>>> = LazyLock::new(|| RwLock::new(None));
#[cfg(feature = "download")]
static DOWNLOAD_CACHE_LOCK: Mutex<()> = Mutex::new(());
#[tracing::instrument(level = "debug", skip_all, fields(language = name))]
pub fn get_language(name: &str) -> Result<Language, Error> {
#[cfg(feature = "download")]
{
if let Ok(lang) = REGISTRY.get_language(name) {
return Ok(lang);
}
let _cache_guard = DOWNLOAD_CACHE_LOCK
.lock()
.unwrap_or_else(|poisoned| recover_poisoned_lock("download_cache", poisoned));
if let Ok(lang) = REGISTRY.get_language(name) {
return Ok(lang);
}
ensure_cache_registered()?;
let cache_dir = effective_cache_dir()?;
let dm = DownloadManager::with_cache_dir(env!("CARGO_PKG_VERSION"), cache_dir);
let resolved = crate::registry::resolve_alias(name);
dm.ensure_languages(&[resolved])?;
if let Ok(lang) = REGISTRY.get_language(resolved) {
return Ok(lang);
}
dm.ensure_languages(&[resolved])?;
REGISTRY.get_language(resolved)
}
#[cfg(not(feature = "download"))]
{
if let Ok(lang) = REGISTRY.get_language(name) {
return Ok(lang);
}
Err(Error::LanguageNotFound(name.to_string()))
}
}
pub fn get_parser(name: &str) -> Result<Parser, Error> {
let mut parser = Parser::new();
parser.set_language(name)?;
Ok(parser)
}
pub fn detect_language(path: &str) -> Option<&'static str> {
detect_language_from_path(path).or_else(|| detect_language_from_extension(path.trim_start_matches('.')))
}
pub fn available_languages() -> Vec<String> {
#[cfg(feature = "download")]
let _ = ensure_cache_registered();
REGISTRY.available_languages()
}
pub fn has_parser(name: &str) -> bool {
#[cfg(feature = "download")]
let _ = ensure_cache_registered();
REGISTRY.has_parser(name)
}
pub fn has_language(name: &str) -> bool {
#[cfg(feature = "download")]
let _ = ensure_cache_registered();
REGISTRY.has_language(name)
}
pub fn language_count() -> usize {
#[cfg(feature = "download")]
let _ = ensure_cache_registered();
REGISTRY.language_count()
}
#[tracing::instrument(
level = "debug",
skip_all,
fields(language = %config.language, source_bytes = source.len())
)]
pub fn process(source: &str, config: &ProcessConfig) -> Result<ProcessResult, Error> {
config.validate()?;
config.check_source_size(source.len())?;
#[cfg(feature = "download")]
get_language(&config.language)?;
REGISTRY.process(source, config)
}
#[cfg(feature = "download")]
fn ensure_cache_registered() -> Result<(), Error> {
let cache_dir = effective_cache_dir()?;
{
let registered = REGISTERED_CACHE_DIR
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if registered.as_deref() == Some(cache_dir.as_path()) {
return Ok(());
}
}
let mut registered = REGISTERED_CACHE_DIR
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let cache_dir = effective_cache_dir()?;
if registered.as_deref() == Some(cache_dir.as_path()) {
return Ok(());
}
crate::download::verify_cache_dir_if_present(&cache_dir)?;
REGISTRY.try_add_extra_libs_dir(cache_dir.clone())?;
*registered = Some(cache_dir);
Ok(())
}
#[cfg(feature = "download")]
fn effective_cache_dir() -> Result<std::path::PathBuf, Error> {
let custom = CUSTOM_CACHE_DIR
.read()
.map_err(|e| Error::LockPoisoned(e.to_string()))?;
match custom.as_ref() {
Some(dir) => Ok(DownloadManager::cache_dir_from_base(dir, env!("CARGO_PKG_VERSION"))),
None => DownloadManager::default_cache_dir(env!("CARGO_PKG_VERSION")),
}
}
#[cfg(feature = "download")]
#[tracing::instrument(level = "info", skip_all)]
pub fn init(config: &PackConfig) -> Result<(), Error> {
let _cache_guard = DOWNLOAD_CACHE_LOCK
.lock()
.unwrap_or_else(|poisoned| recover_poisoned_lock("download_cache", poisoned));
configure_inner(config)?;
if let Some(ref languages) = config.languages {
let refs: Vec<&str> = languages.iter().map(String::as_str).collect();
download_inner(&refs)?;
}
if let Some(ref groups) = config.groups {
let cache_dir = effective_cache_dir()?;
let dm = DownloadManager::with_cache_dir(env!("CARGO_PKG_VERSION"), cache_dir);
for group in groups {
dm.ensure_group(group)?;
}
}
ensure_cache_registered()?;
tracing::info!(
languages = config.languages.as_ref().map_or(0, Vec::len),
groups = config.groups.as_ref().map_or(0, Vec::len),
"language pack initialized"
);
Ok(())
}
#[cfg(feature = "download")]
#[tracing::instrument(level = "debug", skip_all)]
pub fn configure(config: &PackConfig) -> Result<(), Error> {
let _cache_guard = DOWNLOAD_CACHE_LOCK
.lock()
.unwrap_or_else(|poisoned| recover_poisoned_lock("download_cache", poisoned));
configure_inner(config)
}
#[cfg(feature = "download")]
fn configure_inner(config: &PackConfig) -> Result<(), Error> {
if let Some(ref dir) = config.cache_dir {
let mut custom = CUSTOM_CACHE_DIR
.write()
.map_err(|e| Error::LockPoisoned(e.to_string()))?;
*custom = Some(dir.clone());
}
Ok(())
}
#[cfg(feature = "download")]
#[tracing::instrument(level = "info", skip_all, fields(requested = names.len()))]
pub fn download(names: &[&str]) -> Result<usize, Error> {
let _cache_guard = DOWNLOAD_CACHE_LOCK
.lock()
.unwrap_or_else(|poisoned| recover_poisoned_lock("download_cache", poisoned));
let count = download_inner(names)?;
tracing::info!(count, "ensured languages");
Ok(count)
}
#[cfg(feature = "download")]
fn download_inner(names: &[&str]) -> Result<usize, Error> {
ensure_cache_registered()?;
let cache_dir = effective_cache_dir()?;
let dm = DownloadManager::with_cache_dir(env!("CARGO_PKG_VERSION"), cache_dir);
let resolved: Vec<&str> = names.iter().map(|n| crate::registry::resolve_alias(n)).collect();
let unavailable: Vec<&str> = resolved
.iter()
.copied()
.filter(|name| !REGISTRY.has_language(name))
.collect();
dm.ensure_languages(&unavailable)?;
let unique: std::collections::BTreeSet<&str> = resolved.iter().copied().collect();
Ok(unique.len())
}
#[cfg(feature = "download")]
#[tracing::instrument(level = "info", skip_all, fields(requested = languages.len()))]
pub fn prefetch(languages: &[&str]) -> Result<(), Error> {
let _cache_guard = DOWNLOAD_CACHE_LOCK
.lock()
.unwrap_or_else(|poisoned| recover_poisoned_lock("download_cache", poisoned));
ensure_cache_registered()?;
let resolved: Vec<&str> = languages.iter().map(|n| crate::registry::resolve_alias(n)).collect();
let needs_download: Vec<&str> = resolved
.iter()
.copied()
.filter(|name| REGISTRY.get_language(name).is_err())
.collect();
if !needs_download.is_empty() {
let cache_dir = effective_cache_dir()?;
let dm = DownloadManager::with_cache_dir(env!("CARGO_PKG_VERSION"), cache_dir);
dm.ensure_languages(&needs_download)?;
}
for name in &resolved {
REGISTRY.get_language(name)?;
}
tracing::info!(loaded = resolved.len(), "prefetched languages");
Ok(())
}
#[cfg(not(feature = "download"))]
#[tracing::instrument(level = "info", skip_all, fields(requested = languages.len()))]
pub fn prefetch(languages: &[&str]) -> Result<(), Error> {
for raw in languages {
let name = crate::registry::resolve_alias(raw);
REGISTRY.get_language(name)?;
}
Ok(())
}
#[cfg(feature = "download")]
#[tracing::instrument(level = "info", skip_all)]
pub fn download_all() -> Result<usize, Error> {
let _cache_guard = DOWNLOAD_CACHE_LOCK
.lock()
.unwrap_or_else(|poisoned| recover_poisoned_lock("download_cache", poisoned));
ensure_cache_registered()?;
let cache_dir = effective_cache_dir()?;
let dm = DownloadManager::with_cache_dir(env!("CARGO_PKG_VERSION"), cache_dir);
dm.download_all_best_effort()?;
let count = REGISTRY.language_count();
tracing::info!(count, "downloaded all available languages");
Ok(count)
}
#[cfg(feature = "download")]
#[tracing::instrument(level = "info", skip_all, fields(group = name))]
pub fn download_group(name: &str) -> Result<usize, Error> {
let _cache_guard = DOWNLOAD_CACHE_LOCK
.lock()
.unwrap_or_else(|poisoned| recover_poisoned_lock("download_cache", poisoned));
ensure_cache_registered()?;
let cache_dir = effective_cache_dir()?;
let dm = DownloadManager::with_cache_dir(env!("CARGO_PKG_VERSION"), cache_dir);
dm.ensure_group(name)?;
let count = REGISTRY.language_count();
tracing::info!(group = name, count, "downloaded language group");
Ok(count)
}
#[cfg(feature = "download")]
pub fn manifest_languages() -> Result<Vec<String>, Error> {
let cache_dir = effective_cache_dir()?;
let dm = DownloadManager::with_cache_dir(env!("CARGO_PKG_VERSION"), cache_dir);
let manifest = dm.fetch_manifest()?;
let mut langs: Vec<String> = manifest.languages.keys().cloned().collect();
langs.sort_unstable();
Ok(langs)
}
#[cfg(feature = "download")]
pub fn manifest_groups() -> Result<Vec<String>, Error> {
let cache_dir = effective_cache_dir()?;
let dm = DownloadManager::with_cache_dir(env!("CARGO_PKG_VERSION"), cache_dir);
let manifest = dm.fetch_manifest()?;
let mut groups: Vec<String> = manifest.groups.keys().cloned().collect();
groups.sort_unstable();
Ok(groups)
}
#[cfg(feature = "download")]
pub fn downloaded_languages() -> Vec<String> {
let cache_dir = match effective_cache_dir() {
Ok(dir) => dir,
Err(_) => return Vec::new(),
};
let dm = DownloadManager::with_cache_dir(env!("CARGO_PKG_VERSION"), cache_dir);
dm.installed_languages()
}
#[cfg(feature = "download")]
#[tracing::instrument(level = "info", skip_all)]
pub fn clean_cache() -> Result<(), Error> {
let _cache_guard = DOWNLOAD_CACHE_LOCK
.lock()
.unwrap_or_else(|poisoned| recover_poisoned_lock("download_cache", poisoned));
let cache_dir = effective_cache_dir()?;
let dm = DownloadManager::with_cache_dir(env!("CARGO_PKG_VERSION"), cache_dir);
dm.clean_cache()?;
tracing::info!("cleared parser cache");
Ok(())
}
#[cfg(feature = "download")]
pub fn cache_dir() -> Result<String, Error> {
effective_cache_dir().map(|p| p.to_string_lossy().into_owned())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_available_languages() {
let langs = available_languages();
let _ = langs;
}
#[test]
fn test_has_language() {
let langs = available_languages();
if !langs.is_empty() {
assert!(has_language(&langs[0]));
}
assert!(!has_language("nonexistent_language_xyz"));
}
#[test]
fn test_get_language_invalid() {
let result = get_language("nonexistent_language_xyz");
assert!(result.is_err());
}
#[test]
#[ignore = "loads all 371 dynamic libraries — run with --ignored"]
fn test_get_language_and_parse() {
let langs = available_languages();
for lang_name in &langs {
let lang = get_language(lang_name.as_str())
.unwrap_or_else(|e| panic!("Failed to load language '{lang_name}': {e}"));
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&lang)
.unwrap_or_else(|e| panic!("Failed to set language '{lang_name}': {e}"));
let tree = parser.parse("x", None);
assert!(tree.is_some(), "Parser for '{lang_name}' should parse a string");
}
}
#[test]
fn test_get_parser() {
let langs = available_languages();
if let Some(first) = langs.first() {
let parser = get_parser(first.as_str());
assert!(parser.is_ok(), "get_parser should succeed for '{first}'");
}
}
#[test]
fn test_pack_config_default() {
let config = PackConfig::default();
assert!(config.cache_dir.is_none());
assert!(config.languages.is_none());
assert!(config.groups.is_none());
}
#[cfg(feature = "download")]
#[test]
fn should_keep_configuring_after_a_panic_poisons_the_download_cache_lock() {
let poison_result = std::panic::catch_unwind(|| {
let _guard = DOWNLOAD_CACHE_LOCK
.lock()
.expect("DOWNLOAD_CACHE_LOCK should not already be poisoned");
panic!("intentional panic to poison DOWNLOAD_CACHE_LOCK for this test");
});
assert!(poison_result.is_err(), "the intentional panic should have unwound");
assert!(
DOWNLOAD_CACHE_LOCK.is_poisoned(),
"DOWNLOAD_CACHE_LOCK should be poisoned after the panic"
);
let result = configure(&PackConfig {
cache_dir: None,
languages: None,
groups: None,
});
assert!(
result.is_ok(),
"configure must recover a poisoned DOWNLOAD_CACHE_LOCK instead of failing forever, got: {result:?}"
);
}
#[cfg(feature = "download")]
#[test]
#[ignore = "mutates the process-wide CUSTOM_CACHE_DIR/REGISTERED_CACHE_DIR statics; run \
alone, e.g. `cargo test -- --ignored --test-threads=1`, not inside the default \
parallel suite"]
fn should_register_the_new_cache_dir_after_configure_changes_it() {
let first_base = std::env::temp_dir().join("tslp-item1-regression-first");
let second_base = std::env::temp_dir().join("tslp-item1-regression-second");
let first_dir = DownloadManager::cache_dir_from_base(&first_base, env!("CARGO_PKG_VERSION"));
let second_dir = DownloadManager::cache_dir_from_base(&second_base, env!("CARGO_PKG_VERSION"));
configure(&PackConfig {
cache_dir: Some(first_base.clone()),
languages: None,
groups: None,
})
.expect("configure should accept the first cache dir");
let _ = has_language("definitely_not_a_real_language_xyz");
{
let registered = REGISTERED_CACHE_DIR.read().expect("read lock should not be poisoned");
assert_eq!(registered.as_deref(), Some(first_dir.as_path()));
}
configure(&PackConfig {
cache_dir: Some(second_base.clone()),
languages: None,
groups: None,
})
.expect("configure should accept the second cache dir");
let _ = has_language("definitely_not_a_real_language_xyz");
{
let registered = REGISTERED_CACHE_DIR.read().expect("read lock should not be poisoned");
assert_eq!(
registered.as_deref(),
Some(second_dir.as_path()),
"reconfiguring the cache dir must be observed by the very next call, \
not stuck on the old path forever"
);
}
}
#[cfg(feature = "download")]
#[test]
#[ignore = "mutates the process-wide CUSTOM_CACHE_DIR static; run alone, e.g. \
`cargo test -- --ignored --test-threads=1`, not inside the default parallel suite"]
fn should_root_effective_cache_dir_under_the_configured_base_when_cache_dir_is_custom() {
let base = std::env::temp_dir().join("tslp-h1-regression-base");
configure(&PackConfig {
cache_dir: Some(base.clone()),
languages: None,
groups: None,
})
.expect("configure should accept a custom base directory");
let dir = effective_cache_dir().expect("effective cache dir should resolve");
assert!(
dir.starts_with(&base),
"effective cache dir {} must live under the configured base {}",
dir.display(),
base.display()
);
assert_eq!(
dir,
base.join("tree-sitter-language-pack")
.join(format!("v{}", env!("CARGO_PKG_VERSION")))
.join("libs")
);
}
}