vorto 0.11.2

A Vim-flavored modal terminal editor with batteries included: tree-sitter, LSP, fuzzy pickers, vim-surround, multi-cursor, and optional Copilot.
//! Highlights query handling.
//!
//! [`HighlightQuery`] compiles a language's `highlights.scm` once at
//! construction and exposes [`HighlightQuery::captures_in_rows`] to
//! produce the styled spans the UI overlay layer consumes. Capture
//! names are returned as-is — the theme module (`super::theme`) does
//! name → style resolution downstream.

use anyhow::Result;
use tree_sitter::{Language, Query, QueryCursor, StreamingIterator, Tree};

use super::engine::byte_to_char_col;

/// Compiled `highlights.scm` query for one language.
pub struct HighlightQuery {
    query: Query,
    capture_names: Vec<String>,
}

impl HighlightQuery {
    /// Compile `src` against `language`. Errors when the query is
    /// malformed or references nodes that don't exist in the grammar.
    pub(super) fn compile(language: &Language, src: &str) -> Result<Self> {
        let query = Query::new(language, src)?;
        let capture_names = query
            .capture_names()
            .iter()
            .map(|s| s.to_string())
            .collect();
        Ok(Self {
            query,
            capture_names,
        })
    }

    /// Return all captures intersecting rows `[start_row..=end_row]`.
    /// Columns are converted from byte offsets to character offsets so
    /// callers can directly index into character-based line strings.
    /// Caller is responsible for sorting / merging when other capture
    /// sources (injections) are combined in.
    pub(super) fn captures_in_rows(
        &self,
        source: &str,
        tree: &Tree,
        start_row: usize,
        end_row: usize,
    ) -> Vec<Capture> {
        let mut cursor = QueryCursor::new();
        let mut matches = cursor.matches(&self.query, tree.root_node(), source.as_bytes());
        let mut out = Vec::new();
        // `QueryMatches` is a streaming iterator in tree-sitter 0.25+,
        // so we drive it with an explicit `.next()` loop rather than
        // `for ... in`.
        while let Some(m) = matches.next() {
            for cap in m.captures {
                let node = cap.node;
                let start = node.start_position();
                let end = node.end_position();
                if end.row < start_row || start.row > end_row {
                    continue;
                }
                let name = self
                    .capture_names
                    .get(cap.index as usize)
                    .cloned()
                    .unwrap_or_default();
                out.push(Capture {
                    start_row: start.row,
                    start_col: byte_to_char_col(source, start.row, start.column),
                    end_row: end.row,
                    end_col: byte_to_char_col(source, end.row, end.column),
                    name,
                });
            }
        }
        out
    }
}

/// One styled range delivered by the query engine. Coordinates are
/// inclusive on `start`, exclusive on `end`, in *characters* (not
/// bytes) — already converted by [`HighlightQuery::captures_in_rows`].
#[derive(Debug, Clone)]
pub struct Capture {
    pub start_row: usize,
    pub start_col: usize,
    pub end_row: usize,
    pub end_col: usize,
    pub name: String,
}