use karet_core::BytePos;
use karet_core::Span;
use crate::LanguageId;
use crate::Query;
use crate::language_id_from_injection_name;
const CONTENT: &str = "injection.content";
const LANGUAGE: &str = "injection.language";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InjectionRegion {
pub lang: LanguageId,
pub ranges: Vec<Span>,
pub combined: bool,
}
pub(crate) fn extract(tree: &tree_sitter::Tree, query: &Query, text: &str) -> Vec<InjectionRegion> {
use tree_sitter::StreamingIterator;
let names = query.inner.capture_names();
let Some(content_idx) = names.iter().position(|n| *n == CONTENT) else {
return Vec::new();
};
let language_idx = names.iter().position(|n| *n == LANGUAGE);
let mut cursor = tree_sitter::QueryCursor::new();
let mut matches = cursor.matches(&query.inner, tree.root_node(), text.as_bytes());
let mut out = Vec::new();
while let Some(m) = matches.next() {
let props = Properties::read(query, m.pattern_index);
let dynamic = language_idx
.and_then(|i| m.captures.iter().find(|c| c.index as usize == i))
.and_then(|c| c.node.utf8_text(text.as_bytes()).ok());
let Some(lang) = dynamic
.or(props.language.as_deref())
.and_then(language_id_from_injection_name)
else {
continue;
};
for cap in m
.captures
.iter()
.filter(|c| c.index as usize == content_idx)
{
let ranges = content_ranges(cap.node, props.include_children);
if !ranges.is_empty() {
out.push(InjectionRegion {
lang,
ranges,
combined: props.combined,
});
}
}
}
out
}
#[derive(Default)]
struct Properties {
language: Option<String>,
combined: bool,
include_children: bool,
}
impl Properties {
fn read(query: &Query, pattern_index: usize) -> Self {
let mut props = Self::default();
for p in query.inner.property_settings(pattern_index) {
match &*p.key {
"injection.language" => {
props.language = p.value.as_deref().map(str::to_owned);
},
"injection.combined" => props.combined = true,
"injection.include-children" => props.include_children = true,
_ => {},
}
}
props
}
}
fn content_ranges(node: tree_sitter::Node, include_children: bool) -> Vec<Span> {
let (start, end) = (node.start_byte(), node.end_byte());
if include_children || node.named_child_count() == 0 {
return non_empty(start, end).into_iter().collect();
}
let mut ranges = Vec::new();
let mut pos = start;
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
ranges.extend(non_empty(pos, child.start_byte()));
pos = child.end_byte();
}
ranges.extend(non_empty(pos, end));
ranges
}
fn non_empty(start: usize, end: usize) -> Option<Span> {
(end > start).then_some(Span {
start: BytePos(start),
end: BytePos(end),
})
}
pub(crate) fn normalize(mut ranges: Vec<Span>) -> Vec<Span> {
ranges.retain(|s| s.end.0 > s.start.0);
ranges.sort_by_key(|s| (s.start.0, s.end.0));
let mut out: Vec<Span> = Vec::with_capacity(ranges.len());
for span in ranges {
match out.last_mut() {
Some(last) if span.start.0 <= last.end.0 => {
last.end = BytePos(last.end.0.max(span.end.0));
},
_ => out.push(span),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn span(start: usize, end: usize) -> Span {
Span {
start: BytePos(start),
end: BytePos(end),
}
}
#[test]
fn normalize_sorts_and_merges() {
let got = normalize(vec![span(10, 15), span(0, 4), span(3, 8)]);
assert_eq!(got, vec![span(0, 8), span(10, 15)]);
}
#[test]
fn normalize_merges_touching_ranges() {
assert_eq!(normalize(vec![span(0, 4), span(4, 9)]), vec![span(0, 9)]);
}
#[test]
fn normalize_drops_empty_ranges() {
assert!(normalize(vec![span(5, 5), span(9, 2)]).is_empty());
}
#[test]
fn non_empty_rejects_degenerate() {
assert_eq!(non_empty(2, 5), Some(span(2, 5)));
assert_eq!(non_empty(5, 5), None);
assert_eq!(non_empty(6, 5), None);
}
}