vorto 0.11.4

A Vim-flavored modal terminal editor with batteries included: tree-sitter, LSP, fuzzy pickers, vim-surround, multi-cursor, and optional Copilot.
//! Capture-name → terminal color mapping.
//!
//! Tree-sitter highlight queries tag nodes with dotted names like
//! `function.method` or `keyword.return`. The theme resolves each one
//! by trying progressively shorter prefixes — so `function.method`
//! falls back to `function` when no exact entry exists. Match order:
//! longest prefix wins.

use ratatui::style::{Color, Modifier, Style};

/// Resolve a tree-sitter capture name (e.g. `function.method`) into a
/// ratatui style. Unknown captures return the default (uncolored)
/// style so the UI degrades to plain text.
pub fn style_for(capture: &str) -> Style {
    let mut candidate = capture;
    loop {
        if let Some(style) = lookup(candidate) {
            return style;
        }
        match candidate.rfind('.') {
            Some(i) => candidate = &candidate[..i],
            None => return Style::default(),
        }
    }
}

fn lookup(name: &str) -> Option<Style> {
    let s = match name {
        "keyword" => Style::default().fg(Color::Magenta),
        // nvim-treesitter-style aliases that some grammars (Kotlin,
        // Java, etc.) emit as top-level capture names instead of
        // `keyword.*`. Map them to the same magenta as `keyword` so
        // `import`, `if/else/when`, `for/while/do`, `try/catch/throw`
        // get colored consistently.
        "include" => Style::default().fg(Color::Magenta),
        "conditional" => Style::default().fg(Color::Magenta),
        "repeat" => Style::default().fg(Color::Magenta),
        "exception" => Style::default().fg(Color::Magenta),
        "namespace" => Style::default().fg(Color::Yellow),
        "parameter" => Style::default().fg(Color::White),
        "constructor" => Style::default().fg(Color::Yellow),
        "string" => Style::default().fg(Color::Green),
        "string.escape" => Style::default().fg(Color::LightGreen),
        "character" => Style::default().fg(Color::Green),
        "number" => Style::default().fg(Color::LightRed),
        "boolean" => Style::default().fg(Color::LightRed),
        "constant" => Style::default().fg(Color::LightRed),
        "constant.builtin" => Style::default()
            .fg(Color::LightRed)
            .add_modifier(Modifier::BOLD),
        "comment" => Style::default()
            .fg(Color::DarkGray)
            .add_modifier(Modifier::ITALIC),
        "function" => Style::default().fg(Color::LightBlue),
        "function.macro" => Style::default().fg(Color::LightMagenta),
        "function.builtin" => Style::default().fg(Color::LightBlue),
        "method" => Style::default().fg(Color::LightBlue),
        "type" => Style::default().fg(Color::Yellow),
        "type.builtin" => Style::default()
            .fg(Color::Magenta)
            .add_modifier(Modifier::BOLD),
        "variable" => Style::default(),
        "variable.parameter" => Style::default().fg(Color::White),
        "variable.builtin" => Style::default()
            .fg(Color::Cyan)
            .add_modifier(Modifier::BOLD),
        "property" => Style::default().fg(Color::White),
        "field" => Style::default().fg(Color::White),
        "label" => Style::default().fg(Color::Yellow),
        "operator" => Style::default().fg(Color::White),
        "punctuation.bracket" => Style::default().fg(Color::Gray),
        "punctuation.delimiter" => Style::default().fg(Color::Gray),
        "punctuation.special" => Style::default().fg(Color::Yellow),
        "attribute" => Style::default().fg(Color::LightMagenta),
        "tag" => Style::default().fg(Color::LightBlue),
        // Markdown / Helix-style `markup.*` family. The whole heading
        // gets fg + bold + a subtle bg band; the marker capture then
        // patches a magenta fg on top to keep `#` keyword-colored
        // without losing the band. Code/links reuse string-green and
        // link-cyan/blue so `[label](url)` stays distinguishable.
        "markup.heading" => Style::default()
            .fg(Color::LightBlue)
            .add_modifier(Modifier::BOLD),
        "markup.heading.marker" => Style::default().fg(Color::Magenta),
        "markup.list" => Style::default().fg(Color::DarkGray),
        "markup.raw" => Style::default().fg(Color::Green),
        "markup.link.url" => Style::default()
            .fg(Color::Cyan)
            .add_modifier(Modifier::UNDERLINED),
        "markup.link.label" => Style::default().fg(Color::LightBlue),
        // Diff capture names follow Helix's `diff.*` convention: plus
        // (additions) is green, minus (deletions) is red. Falls through
        // to fg-only — line bg painting is out of scope for the
        // per-character renderer.
        "diff.plus" => Style::default().fg(Color::Green),
        "diff.minus" => Style::default().fg(Color::Red),
        _ => return None,
    };
    Some(s)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn exact_match() {
        let s = style_for("keyword");
        assert_eq!(s.fg, Some(Color::Magenta));
    }

    #[test]
    fn dotted_falls_back_to_prefix() {
        // `function.method` isn't in the table; should fall back to
        // `function`.
        let s = style_for("function.method");
        assert_eq!(s.fg, Some(Color::LightBlue));
    }

    #[test]
    fn unknown_capture_is_default() {
        let s = style_for("completely-unknown-capture");
        assert_eq!(s.fg, None);
    }

    #[test]
    fn deeply_nested_falls_back() {
        // `keyword.foo.bar.baz` → `keyword`.
        let s = style_for("keyword.foo.bar.baz");
        assert_eq!(s.fg, Some(Color::Magenta));
    }
}