extern crate self as escriba_ts;
use std::collections::HashMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub enum Semantic {
Keyword,
Symbol,
KeywordArg,
String,
Number,
Literal,
Comment,
Accent,
Muted,
Error,
Warning,
Info,
Hint,
Added,
Removed,
Unchanged,
}
use tree_sitter::{Language, Parser, Tree};
use tree_sitter_highlight::{HighlightConfiguration, HighlightEvent, Highlighter};
#[derive(Debug, Error)]
pub enum TsError {
#[error("grammar not registered: {0}")]
Unknown(String),
#[error("tree-sitter: {0}")]
Ts(String),
}
pub type Result<T> = std::result::Result<T, TsError>;
pub struct Grammar {
pub name: String,
pub language: Language,
pub config: HighlightConfiguration,
pub extensions: Vec<String>,
}
pub struct GrammarRegistry {
grammars: HashMap<String, Grammar>,
pub highlight_names: Vec<&'static str>,
}
impl GrammarRegistry {
#[must_use]
pub fn builtin() -> Result<Self> {
let highlight_names = canonical_highlight_names();
let mut grammars = HashMap::new();
let lang: Language = tree_sitter_rust::language();
let mut cfg = HighlightConfiguration::new(
lang.clone(),
"rust",
tree_sitter_rust::HIGHLIGHTS_QUERY,
tree_sitter_rust::INJECTIONS_QUERY,
"",
)
.map_err(|e| TsError::Ts(format!("rust: {e}")))?;
cfg.configure(&highlight_names);
grammars.insert(
"rust".to_string(),
Grammar {
name: "rust".to_string(),
language: lang,
config: cfg,
extensions: vec!["rs".to_string()],
},
);
Ok(Self {
grammars,
highlight_names,
})
}
#[must_use]
pub fn get(&self, language: &str) -> Option<&Grammar> {
self.grammars.get(language)
}
#[must_use]
pub fn from_extension(&self, ext: &str) -> Option<&Grammar> {
self.grammars
.values()
.find(|g| g.extensions.iter().any(|e| e == ext))
}
pub fn add_extension(&mut self, language: &str, ext: impl Into<String>) -> bool {
if let Some(g) = self.grammars.get_mut(language) {
let ext = ext.into();
if !g.extensions.iter().any(|e| *e == ext) {
g.extensions.push(ext);
}
true
} else {
false
}
}
pub fn languages(&self) -> impl Iterator<Item = &str> {
self.grammars.keys().map(String::as_str)
}
}
pub struct BufferParser {
language: String,
parser: Parser,
tree: Option<Tree>,
}
impl BufferParser {
pub fn new(language: &str, registry: &GrammarRegistry) -> Result<Self> {
let grammar = registry
.get(language)
.ok_or_else(|| TsError::Unknown(language.to_string()))?;
let mut parser = Parser::new();
parser
.set_language(&grammar.language)
.map_err(|e| TsError::Ts(e.to_string()))?;
Ok(Self {
language: language.to_string(),
parser,
tree: None,
})
}
#[must_use]
pub fn language(&self) -> &str {
&self.language
}
pub fn reparse(&mut self, src: &str) -> Result<()> {
let new = self.parser.parse(src, self.tree.as_ref());
self.tree = new;
Ok(())
}
#[must_use]
pub fn tree(&self) -> Option<&Tree> {
self.tree.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HighlightSpan {
pub start: usize,
pub end: usize,
pub semantic: Semantic,
}
pub fn highlight(
src: &str,
grammar: &Grammar,
registry: &GrammarRegistry,
) -> Result<Vec<HighlightSpan>> {
let mut highlighter = Highlighter::new();
let events = highlighter
.highlight(&grammar.config, src.as_bytes(), None, |_| None)
.map_err(|e| TsError::Ts(e.to_string()))?;
let mut stack: Vec<usize> = Vec::new();
let mut spans: Vec<HighlightSpan> = Vec::new();
let mut run_start: Option<(usize, usize)> = None;
for ev in events {
let ev = ev.map_err(|e| TsError::Ts(e.to_string()))?;
match ev {
HighlightEvent::HighlightStart(h) => {
stack.push(h.0);
}
HighlightEvent::HighlightEnd => {
stack.pop();
run_start = None;
}
HighlightEvent::Source { start, end } => {
if let Some(&top) = stack.last() {
let sem = highlight_index_to_semantic(top, ®istry.highlight_names);
match run_start {
Some((rs, _)) if rs == start => {}
_ => {
spans.push(HighlightSpan {
start,
end,
semantic: sem,
});
run_start = Some((start, end));
}
}
}
}
}
}
Ok(spans)
}
fn canonical_highlight_names() -> Vec<&'static str> {
vec![
"keyword",
"function",
"function.call",
"function.method",
"type",
"type.builtin",
"constant",
"constant.builtin",
"string",
"string.special",
"number",
"boolean",
"comment",
"operator",
"punctuation",
"punctuation.bracket",
"punctuation.delimiter",
"variable",
"variable.parameter",
"variable.builtin",
"attribute",
"label",
"tag",
]
}
fn highlight_index_to_semantic(index: usize, names: &[&'static str]) -> Semantic {
let name = names.get(index).copied().unwrap_or("");
match name {
n if n.starts_with("keyword") => Semantic::Keyword,
n if n.starts_with("function") => Semantic::Symbol,
n if n.starts_with("type") => Semantic::Accent,
n if n.starts_with("constant.builtin") || n == "boolean" => Semantic::Literal,
n if n.starts_with("constant") => Semantic::Literal,
n if n.starts_with("string") => Semantic::String,
n if n == "number" => Semantic::Number,
n if n.starts_with("comment") => Semantic::Comment,
n if n.starts_with("operator") => Semantic::Accent,
n if n.starts_with("punctuation") => Semantic::Muted,
n if n.starts_with("variable") => Semantic::Symbol,
n if n == "attribute" => Semantic::Hint,
n if n == "label" => Semantic::Hint,
n if n == "tag" => Semantic::Keyword,
_ => Semantic::Symbol,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builtin_registers_rust() {
let r = GrammarRegistry::builtin().unwrap();
assert!(r.get("rust").is_some());
assert_eq!(
r.from_extension("rs").map(|g| g.name.as_str()),
Some("rust")
);
}
#[test]
fn parser_parses_rust_source() {
let r = GrammarRegistry::builtin().unwrap();
let mut p = BufferParser::new("rust", &r).unwrap();
p.reparse("fn main() { let x = 42; }").unwrap();
assert!(p.tree().is_some());
}
#[test]
fn highlight_produces_spans() {
let r = GrammarRegistry::builtin().unwrap();
let g = r.get("rust").unwrap();
let spans = highlight("fn main() { let x = 42; }", g, &r).unwrap();
assert!(!spans.is_empty(), "expected some spans");
assert!(spans.iter().any(|s| s.semantic == Semantic::Keyword));
}
#[test]
fn unknown_grammar_errors() {
let r = GrammarRegistry::builtin().unwrap();
assert!(BufferParser::new("klingon", &r).is_err());
}
}