use super::ConfigError;
use tree_sitter::Query;
pub(crate) struct AnnotationQuery {
pub(crate) query: Query,
pub(crate) patterns: Vec<Pattern>,
pub(crate) sources: Vec<String>,
}
pub(crate) struct Pattern {
pub(crate) tags: Vec<String>,
pub(crate) source: usize,
}
pub(crate) struct QuerySource {
pub(crate) name: String,
pub(crate) text: String,
}
impl AnnotationQuery {
pub(crate) fn compile(
grammar: &tree_sitter::Language,
sources: &[QuerySource],
) -> Result<Self, ConfigError> {
let mut text = String::new();
let mut starts = Vec::with_capacity(sources.len());
for source in sources {
starts.push(text.len());
text.push_str(&source.text);
text.push('\n');
}
let owner = |offset: usize| starts.partition_point(|&start| start <= offset) - 1;
let query = Query::new(grammar, &text).map_err(|error| {
if sources.is_empty() {
return ConfigError(error.to_string());
}
let index = owner(error.offset.min(text.len().saturating_sub(1)));
let line = text[starts[index]..error.offset].matches('\n').count() + 1;
ConfigError(format!(
"{}:{line}: {:?} error: {}",
sources[index].name, error.kind, error.message
))
})?;
let named = |pattern: usize, message: String| {
ConfigError(format!(
"{}: {message}",
sources[owner(query.start_byte_for_pattern(pattern))].name
))
};
for name in query.capture_names() {
if name.starts_with('_') || matches!(*name, "fold" | "fold.open" | "fold.close") {
continue;
}
let source = sources
.iter()
.find(|source| source.text.contains(&format!("@{name}")))
.expect("a capture name appears in the text that declared it");
return Err(ConfigError(format!(
"{}: unsupported capture @{name}; use an underscore prefix for helper captures",
source.name
)));
}
let mut patterns = Vec::new();
for index in 0..query.pattern_count() {
let mut pattern = Pattern {
tags: Vec::new(),
source: owner(query.start_byte_for_pattern(index)),
};
if !query.property_predicates(index).is_empty() {
return Err(named(index, "#is? and #is-not? are not supported".into()));
}
for property in query.property_settings(index) {
if property.key.as_ref() != "tag" {
return Err(named(
index,
format!("unsupported #set! property {}", property.key),
));
}
if property
.capture_id
.is_some_and(|id| query.capture_names()[id] != "fold")
{
return Err(named(
index,
"tag metadata must target @fold or its pattern".into(),
));
}
let tag = property
.value
.as_deref()
.filter(|tag| !tag.is_empty())
.ok_or_else(|| named(index, "#set! tag requires a nonempty string".into()))?;
pattern.tags.push(tag.to_owned());
}
if let Some(predicate) = query.general_predicates(index).first() {
return Err(named(
index,
format!("unsupported directive #{}", predicate.operator),
));
}
patterns.push(pattern);
}
Ok(Self {
query,
patterns,
sources: sources.iter().map(|source| source.name.clone()).collect(),
})
}
}