use std::{ops::Range, sync::OnceLock};
use theme::HighlightKind;
use tree_sitter::Language;
use tree_sitter_highlight::{HighlightConfiguration, HighlightEvent, Highlighter};
use tree_sitter_language::LanguageFn;
pub struct Lang {
pub name: &'static str,
pub aliases: &'static [&'static str],
pub grammar: LanguageFn,
pub query: &'static str,
compiled: OnceLock<Option<Compiled>>,
}
pub struct Compiled {
pub config: HighlightConfiguration,
pub names: Vec<String>,
}
impl Lang {
pub const fn new(
name: &'static str,
aliases: &'static [&'static str],
grammar: LanguageFn,
query: &'static str,
) -> Self {
Self {
name,
aliases,
grammar,
query,
compiled: OnceLock::new(),
}
}
pub fn highlight(&'static self, source: &str) -> Option<Vec<(Range<usize>, HighlightKind)>> {
let compiled = self.compiled()?;
let config = &compiled.config;
let mut highlighter = Highlighter::new();
highlighter.parser().set_language(&config.language).ok()?;
let mut spans = Vec::new();
let mut kinds: Vec<HighlightKind> = Vec::new();
for event in highlighter
.highlight(config, source.as_bytes(), None, |_| None)
.ok()?
.flatten()
{
match event {
HighlightEvent::HighlightStart(hl) => {
let name = compiled.names.get(hl.0).map(String::as_str).unwrap_or("");
kinds.push(kind_of(name));
}
HighlightEvent::HighlightEnd => {
kinds.pop();
}
HighlightEvent::Source { start, end } => {
if let Some(&kind) = kinds.last() {
spans.push((start..end, kind));
}
}
}
}
Some(spans)
}
pub fn compiled(&'static self) -> Option<&'static Compiled> {
self.compiled
.get_or_init(|| {
let grammar: Language = self.grammar.into();
let mut config =
HighlightConfiguration::new(grammar, self.name, self.query, "", "").ok()?;
let names: Vec<String> = config
.query
.capture_names()
.iter()
.map(|s| s.to_string())
.filter(|s| !s.starts_with('_'))
.collect();
config.configure(&names);
Some(Compiled { config, names })
})
.as_ref()
}
}
#[cfg(feature = "rust")]
static RUST: Lang = Lang::new(
"rust",
&["rust", "rs"],
tree_sitter_rust::LANGUAGE,
include_str!("../queries/rust.scm"),
);
#[cfg(feature = "python")]
static PYTHON: Lang = Lang::new(
"python",
&["python", "py"],
tree_sitter_python::LANGUAGE,
include_str!("../queries/python.scm"),
);
#[cfg(feature = "typescript")]
static TYPESCRIPT: Lang = Lang::new(
"typescript",
&["typescript", "ts"],
tree_sitter_typescript::LANGUAGE_TYPESCRIPT,
include_str!("../queries/typescript.scm"),
);
#[cfg(feature = "typescript")]
static TSX: Lang = Lang::new(
"tsx",
&["tsx", "jsx", "javascript", "js"],
tree_sitter_typescript::LANGUAGE_TSX,
include_str!("../queries/tsx.scm"),
);
#[cfg(feature = "json")]
static JSON: Lang = Lang::new(
"json",
&["json", "jsonc"],
tree_sitter_json::LANGUAGE,
include_str!("../queries/json.scm"),
);
#[cfg(feature = "go")]
static GO: Lang = Lang::new(
"go",
&["go", "golang"],
tree_sitter_go::LANGUAGE,
include_str!("../queries/go.scm"),
);
#[cfg(feature = "bash")]
static BASH: Lang = Lang::new(
"bash",
&["bash", "sh", "shell", "zsh", "console"],
tree_sitter_bash::LANGUAGE,
include_str!("../queries/bash.scm"),
);
#[cfg(feature = "toml")]
static TOML: Lang = Lang::new(
"toml",
&["toml"],
tree_sitter_toml_ng::LANGUAGE,
include_str!("../queries/toml.scm"),
);
pub static LANGS: &[&Lang] = &[
#[cfg(feature = "rust")]
&RUST,
#[cfg(feature = "python")]
&PYTHON,
#[cfg(feature = "typescript")]
&TYPESCRIPT,
#[cfg(feature = "typescript")]
&TSX,
#[cfg(feature = "json")]
&JSON,
#[cfg(feature = "go")]
&GO,
#[cfg(feature = "bash")]
&BASH,
#[cfg(feature = "toml")]
&TOML,
];
pub fn resolve(tag: &str) -> Option<&'static Lang> {
let tag = tag
.split([' ', ','])
.next()
.unwrap_or("")
.to_ascii_lowercase();
LANGS
.iter()
.copied()
.find(|l| l.aliases.contains(&tag.as_str()))
}
pub fn kind_of(name: &str) -> HighlightKind {
match name {
"comment" | "comment.documentation" => HighlightKind::Comment,
"keyword"
| "keyword.function"
| "keyword.return"
| "keyword.operator"
| "keyword.conditional"
| "keyword.conditional.ternary"
| "keyword.coroutine"
| "keyword.directive"
| "keyword.exception"
| "keyword.import"
| "keyword.modifier"
| "keyword.repeat"
| "keyword.type" => HighlightKind::Keyword,
"string" => HighlightKind::String,
"string.special" | "string.special.key" | "string.special.url" | "string.regexp"
| "character.special" => HighlightKind::StringSpecial,
"escape" | "string.escape" => HighlightKind::Escape,
"number" => HighlightKind::Number,
"boolean" => HighlightKind::Boolean,
"type" | "type.interface" => HighlightKind::TypeName,
"type.builtin" => HighlightKind::TypeBuiltin,
"constructor" => HighlightKind::Constructor,
"function" | "function.method" | "function.method.call" | "function.call" => {
HighlightKind::Function
}
"function.builtin" => HighlightKind::FunctionBuiltin,
"macro" | "function.macro" => HighlightKind::MacroName,
"property" | "property.definition" | "variable.member" => HighlightKind::Property,
"constant" | "constant.builtin" | "module" | "module.builtin" => HighlightKind::Constant,
"variable" | "variable.builtin" => HighlightKind::Variable,
"variable.special" | "self" => HighlightKind::VariableSpecial,
"variable.parameter" | "parameter" => HighlightKind::Parameter,
"operator" => HighlightKind::Operator,
"punctuation" | "punctuation.bracket" | "punctuation.delimiter" | "punctuation.special" => {
HighlightKind::Punctuation
}
"tag" | "tag.builtin" => HighlightKind::Tag,
"tag.delimiter" => HighlightKind::Punctuation,
"attribute" | "tag.attribute" => HighlightKind::Attribute,
"label" => HighlightKind::Label,
"invalid" => HighlightKind::Invalid,
_ => HighlightKind::Variable,
}
}