1#![forbid(unsafe_code)]
29
30use std::collections::HashMap;
31use std::sync::Arc;
32
33use hikari_core::{
34 HighlightSpan as HlSpan, Highlighter, HlClass, Language, LanguagePlugin, Selector, SpanSink,
35};
36use serde::{Deserialize, Serialize};
37use thiserror::Error;
38use tree_sitter::{
39 InputEdit, Language as TsLanguage, Parser, Point, Tree,
40};
41use tree_sitter_highlight::{HighlightConfiguration, HighlightEvent, Highlighter as TsHighlighter};
42
43pub use hikari_token::Semantic;
47
48#[derive(Debug, Error)]
51pub enum TsError {
52 #[error("grammar not registered: {0}")]
53 Unknown(String),
54 #[error("tree-sitter: {0}")]
55 Ts(String),
56}
57
58pub type Result<T> = std::result::Result<T, TsError>;
59
60pub struct Grammar {
64 pub name: String,
65 pub language: TsLanguage,
66 pub config: HighlightConfiguration,
67 pub extensions: Vec<String>,
71}
72
73pub struct GrammarRegistry {
75 grammars: HashMap<String, Grammar>,
76 pub highlight_names: Vec<&'static str>,
79}
80
81impl GrammarRegistry {
82 pub fn builtin() -> Result<Self> {
88 let highlight_names = canonical_highlight_names();
89 let mut reg = Self {
90 grammars: HashMap::new(),
91 highlight_names,
92 };
93 reg.register(
94 "rust",
95 &tree_sitter_rust::language(),
96 tree_sitter_rust::HIGHLIGHTS_QUERY,
97 tree_sitter_rust::INJECTIONS_QUERY,
98 &["rs"],
99 )?;
100 reg.register(
101 "python",
102 &tree_sitter_python::language(),
103 tree_sitter_python::HIGHLIGHTS_QUERY,
104 "",
105 &["py", "pyi"],
106 )?;
107 reg.register(
108 "json",
109 &tree_sitter_json::language(),
110 tree_sitter_json::HIGHLIGHTS_QUERY,
111 "",
112 &["json"],
113 )?;
114 reg.register(
115 "bash",
116 &tree_sitter_bash::language(),
117 tree_sitter_bash::HIGHLIGHT_QUERY,
118 "",
119 &["sh", "bash", "zsh"],
120 )?;
121 reg.register(
122 "go",
123 &tree_sitter_go::language(),
124 tree_sitter_go::HIGHLIGHTS_QUERY,
125 "",
126 &["go"],
127 )?;
128 reg.register(
129 "c",
130 &tree_sitter_c::language(),
131 tree_sitter_c::HIGHLIGHT_QUERY,
132 "",
133 &["c", "h"],
134 )?;
135 let cpp_hl = format!(
139 "{}\n{}",
140 tree_sitter_c::HIGHLIGHT_QUERY,
141 tree_sitter_cpp::HIGHLIGHT_QUERY,
142 );
143 reg.register(
144 "cpp",
145 &tree_sitter_cpp::language(),
146 &cpp_hl,
147 "",
148 &["cpp", "cc", "cxx", "hpp", "hh"],
149 )?;
150 reg.register(
151 "css",
152 &tree_sitter_css::language(),
153 tree_sitter_css::HIGHLIGHTS_QUERY,
154 "",
155 &["css", "scss"],
156 )?;
157 reg.register(
158 "html",
159 &tree_sitter_html::language(),
160 tree_sitter_html::HIGHLIGHTS_QUERY,
161 tree_sitter_html::INJECTIONS_QUERY,
162 &["html", "htm"],
163 )?;
164 reg.register(
165 "ruby",
166 &tree_sitter_ruby::language(),
167 tree_sitter_ruby::HIGHLIGHTS_QUERY,
168 "",
169 &["rb"],
170 )?;
171 Ok(reg)
172 }
173
174 fn register(
181 &mut self,
182 name: &str,
183 language: &TsLanguage,
184 highlights: &str,
185 injections: &str,
186 extensions: &[&str],
187 ) -> Result<()> {
188 let mut cfg =
189 HighlightConfiguration::new(language.clone(), name, highlights, injections, "")
190 .map_err(|e| TsError::Ts(format!("{name}: {e}")))?;
191 cfg.configure(&self.highlight_names);
192 self.grammars.insert(
193 name.to_string(),
194 Grammar {
195 name: name.to_string(),
196 language: language.clone(),
197 config: cfg,
198 extensions: extensions.iter().map(|s| (*s).to_string()).collect(),
199 },
200 );
201 Ok(())
202 }
203
204 #[must_use]
205 pub fn get(&self, language: &str) -> Option<&Grammar> {
206 self.grammars.get(language)
207 }
208
209 #[must_use]
211 pub fn from_extension(&self, ext: &str) -> Option<&Grammar> {
212 self.grammars
213 .values()
214 .find(|g| g.extensions.iter().any(|e| e == ext))
215 }
216
217 pub fn add_extension(&mut self, language: &str, ext: impl Into<String>) -> bool {
220 if let Some(g) = self.grammars.get_mut(language) {
221 let ext = ext.into();
222 if !g.extensions.iter().any(|e| *e == ext) {
223 g.extensions.push(ext);
224 }
225 true
226 } else {
227 false
228 }
229 }
230
231 pub fn languages(&self) -> impl Iterator<Item = &str> {
233 self.grammars.keys().map(String::as_str)
234 }
235}
236
237pub struct BufferParser {
241 language: String,
242 parser: Parser,
243 tree: Option<Tree>,
244}
245
246impl BufferParser {
247 pub fn new(language: &str, registry: &GrammarRegistry) -> Result<Self> {
252 let grammar = registry
253 .get(language)
254 .ok_or_else(|| TsError::Unknown(language.to_string()))?;
255 let mut parser = Parser::new();
256 parser
257 .set_language(&grammar.language)
258 .map_err(|e| TsError::Ts(e.to_string()))?;
259 Ok(Self {
260 language: language.to_string(),
261 parser,
262 tree: None,
263 })
264 }
265
266 #[must_use]
267 pub fn language(&self) -> &str {
268 &self.language
269 }
270
271 pub fn reparse(&mut self, src: &str) -> Result<()> {
283 self.tree = self.parser.parse(src, None);
284 Ok(())
285 }
286
287 pub fn reparse_edit(
297 &mut self,
298 old_src: &str,
299 new_src: &str,
300 start_byte: usize,
301 old_end_byte: usize,
302 ) -> Result<()> {
303 if self.tree.is_some() {
304 let edit = TsEdit::from_splice(old_src, new_src, start_byte, old_end_byte);
305 if let Some(tree) = self.tree.as_mut() {
306 tree.edit(&edit.to_input_edit());
307 }
308 self.tree = self.parser.parse(new_src, self.tree.as_ref());
309 } else {
310 self.tree = self.parser.parse(new_src, None);
311 }
312 Ok(())
313 }
314
315 #[must_use]
316 pub fn tree(&self) -> Option<&Tree> {
317 self.tree.as_ref()
318 }
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
326pub struct TsEdit {
327 pub start_byte: usize,
328 pub old_end_byte: usize,
329 pub new_end_byte: usize,
330 pub start_point: (usize, usize),
332 pub old_end_point: (usize, usize),
333 pub new_end_point: (usize, usize),
334}
335
336impl TsEdit {
337 #[must_use]
341 pub fn from_splice(old: &str, new: &str, start_byte: usize, old_end_byte: usize) -> Self {
342 let new_end_byte = new.len() - (old.len() - old_end_byte);
343 Self {
344 start_byte,
345 old_end_byte,
346 new_end_byte,
347 start_point: byte_to_point(old, start_byte),
348 old_end_point: byte_to_point(old, old_end_byte),
349 new_end_point: byte_to_point(new, new_end_byte),
350 }
351 }
352
353 fn to_input_edit(self) -> InputEdit {
354 let pt = |(row, column): (usize, usize)| Point { row, column };
355 InputEdit {
356 start_byte: self.start_byte,
357 old_end_byte: self.old_end_byte,
358 new_end_byte: self.new_end_byte,
359 start_position: pt(self.start_point),
360 old_end_position: pt(self.old_end_point),
361 new_end_position: pt(self.new_end_point),
362 }
363 }
364}
365
366#[must_use]
369fn byte_to_point(text: &str, byte: usize) -> (usize, usize) {
370 let byte = byte.min(text.len());
371 let prefix = &text[..byte];
372 let row = prefix.bytes().filter(|&b| b == b'\n').count();
373 let col = prefix.len() - prefix.rfind('\n').map_or(0, |i| i + 1);
374 (row, col)
375}
376
377#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
381pub struct HighlightSpan {
382 pub start: usize,
383 pub end: usize,
384 pub semantic: Semantic,
385}
386
387pub fn highlight(
392 src: &str,
393 grammar: &Grammar,
394 registry: &GrammarRegistry,
395) -> Result<Vec<HighlightSpan>> {
396 let mut highlighter = TsHighlighter::new();
397 let events = highlighter
398 .highlight(&grammar.config, src.as_bytes(), None, |_| None)
399 .map_err(|e| TsError::Ts(e.to_string()))?;
400
401 let mut stack: Vec<usize> = Vec::new();
402 let mut spans: Vec<HighlightSpan> = Vec::new();
403 let mut run_start: Option<(usize, usize)> = None;
404
405 for ev in events {
406 let ev = ev.map_err(|e| TsError::Ts(e.to_string()))?;
407 match ev {
408 HighlightEvent::HighlightStart(h) => stack.push(h.0),
409 HighlightEvent::HighlightEnd => {
410 stack.pop();
411 run_start = None;
412 }
413 HighlightEvent::Source { start, end } => {
414 if let Some(&top) = stack.last() {
415 let sem = highlight_index_to_semantic(top, ®istry.highlight_names);
416 match run_start {
417 Some((rs, _)) if rs == start => {}
418 _ => {
419 spans.push(HighlightSpan {
420 start,
421 end,
422 semantic: sem,
423 });
424 run_start = Some((start, end));
425 }
426 }
427 }
428 }
429 }
430 }
431
432 Ok(spans)
433}
434
435fn canonical_highlight_names() -> Vec<&'static str> {
438 vec![
439 "keyword",
440 "function",
441 "function.call",
442 "function.method",
443 "type",
444 "type.builtin",
445 "constant",
446 "constant.builtin",
447 "string",
448 "string.special",
449 "number",
450 "boolean",
451 "comment",
452 "operator",
453 "punctuation",
454 "punctuation.bracket",
455 "punctuation.delimiter",
456 "variable",
457 "variable.parameter",
458 "variable.builtin",
459 "attribute",
460 "label",
461 "tag",
462 ]
463}
464
465fn highlight_index_to_semantic(index: usize, names: &[&'static str]) -> Semantic {
466 let name = names.get(index).copied().unwrap_or("");
467 match name {
468 n if n.starts_with("keyword") => Semantic::Keyword,
469 n if n.starts_with("function") => Semantic::Symbol,
470 n if n.starts_with("type") => Semantic::Accent,
471 n if n.starts_with("constant.builtin") || n == "boolean" => Semantic::Literal,
472 n if n.starts_with("constant") => Semantic::Literal,
473 n if n.starts_with("string") => Semantic::String,
474 n if n == "number" => Semantic::Number,
475 n if n.starts_with("comment") => Semantic::Comment,
476 n if n.starts_with("operator") => Semantic::Accent,
477 n if n.starts_with("punctuation") => Semantic::Muted,
478 n if n.starts_with("variable") => Semantic::Symbol,
479 n if n == "attribute" => Semantic::Hint,
480 n if n == "label" => Semantic::Hint,
481 n if n == "tag" => Semantic::Keyword,
482 _ => Semantic::Symbol,
483 }
484}
485
486#[derive(Clone)]
492pub struct TreeSitterHost {
493 registry: Arc<GrammarRegistry>,
494}
495
496impl TreeSitterHost {
497 pub fn builtin() -> Result<Self> {
502 Ok(Self {
503 registry: Arc::new(GrammarRegistry::builtin()?),
504 })
505 }
506
507 pub fn languages(&self) -> impl Iterator<Item = &str> {
509 self.registry.languages()
510 }
511
512 #[must_use]
515 pub fn plugins(&self) -> Vec<Box<dyn LanguagePlugin>> {
516 let mut out: Vec<Box<dyn LanguagePlugin>> = Vec::new();
517 for name in self.registry.languages() {
518 let lang: &'static str = Box::leak(name.to_string().into_boxed_str());
521 let selectors: Vec<Selector> = self
522 .registry
523 .get(name)
524 .map(|g| {
525 g.extensions
526 .iter()
527 .map(|e| Selector::Extension(Box::leak(e.clone().into_boxed_str())))
528 .collect()
529 })
530 .unwrap_or_default();
531 out.push(Box::new(TreeSitterPlugin {
532 language: Language(lang),
533 selectors: selectors.leak(),
534 registry: self.registry.clone(),
535 grammar: lang,
536 }));
537 }
538 out
539 }
540
541 #[must_use]
543 pub fn highlighter(&self, grammar: &'static str) -> TreeSitterHighlighter {
544 TreeSitterHighlighter {
545 registry: self.registry.clone(),
546 grammar,
547 }
548 }
549}
550
551pub struct TreeSitterPlugin {
553 language: Language,
554 selectors: &'static [Selector],
555 registry: Arc<GrammarRegistry>,
556 grammar: &'static str,
557}
558
559impl LanguagePlugin for TreeSitterPlugin {
560 fn language(&self) -> Language {
561 self.language
562 }
563 fn selectors(&self) -> &'static [Selector] {
564 self.selectors
565 }
566 fn make_highlighter(&self) -> Box<dyn Highlighter> {
567 Box::new(TreeSitterHighlighter {
568 registry: self.registry.clone(),
569 grammar: self.grammar,
570 })
571 }
572}
573
574pub struct TreeSitterHighlighter {
578 registry: Arc<GrammarRegistry>,
579 grammar: &'static str,
580}
581
582impl Highlighter for TreeSitterHighlighter {
583 fn highlight(&self, text: &str) -> Vec<HlSpan> {
584 let len = u32::try_from(text.len()).unwrap_or(u32::MAX);
585 let mut sink = SpanSink::for_document(len);
586 if let Some(grammar) = self.registry.get(self.grammar)
587 && let Ok(spans) = highlight(text, grammar, &self.registry)
588 {
589 for s in spans {
590 let class: HlClass = s.semantic.into();
592 sink.push(
593 u32::try_from(s.start).unwrap_or(u32::MAX),
594 u32::try_from(s.end).unwrap_or(u32::MAX),
595 class,
596 );
597 }
598 }
599 sink.finish()
600 }
601}
602
603#[cfg(test)]
604mod tests {
605 use super::{BufferParser, GrammarRegistry, TreeSitterHost, TsEdit, byte_to_point};
606 use hikari_core::{Highlighter, HlClass};
607
608 #[test]
609 fn builtin_host_highlights_rust_with_coverage() {
610 let host = TreeSitterHost::builtin().expect("builtin grammars");
611 let hl = host.highlighter("rust");
612 let src = "fn main() {\n let x = 42;\n}\n";
613 let spans = hl.highlight(src);
614 let mut cursor = 0u32;
616 for s in &spans {
617 assert_eq!(s.span.start, cursor, "gap/overlap at {cursor}");
618 cursor = s.span.end;
619 }
620 assert_eq!(cursor as usize, src.len(), "partition must cover the text");
621 assert!(
622 spans.iter().any(|s| s.class != HlClass::Plain),
623 "tree-sitter should classify something in real Rust",
624 );
625 }
626
627 #[test]
628 fn rust_grammar_is_registered() {
629 let host = TreeSitterHost::builtin().expect("builtin grammars");
630 assert!(host.languages().any(|l| l == "rust"));
631 assert!(!host.plugins().is_empty());
632 }
633
634 #[test]
635 fn byte_to_point_counts_rows_and_byte_columns() {
636 assert_eq!(byte_to_point("abc", 2), (0, 2));
637 assert_eq!(byte_to_point("ab\ncd", 3), (1, 0));
638 assert_eq!(byte_to_point("x\ny\nz", 4), (2, 0));
639 }
640
641 #[test]
643 fn incremental_reparse_equals_full_parse() {
644 let r = GrammarRegistry::builtin().unwrap();
645 let old = "fn main() { let x = 1; }";
646 let new = "fn main() { let x = 42; }";
647 let start = old.find('1').unwrap();
648
649 let mut inc = BufferParser::new("rust", &r).unwrap();
650 inc.reparse(old).unwrap();
651 inc.reparse_edit(old, new, start, start + 1).unwrap();
652
653 let mut full = BufferParser::new("rust", &r).unwrap();
654 full.reparse(new).unwrap();
655
656 assert_eq!(
657 inc.tree().unwrap().root_node().to_sexp(),
658 full.tree().unwrap().root_node().to_sexp(),
659 );
660 }
661
662 #[test]
663 fn ts_edit_new_end_byte_accounts_for_length_delta() {
664 let old = "x = 1;";
665 let new = "x = 42;";
666 let start = old.find('1').unwrap();
667 let e = TsEdit::from_splice(old, new, start, start + 1);
668 assert_eq!(e.new_end_byte, start + 2);
669 }
670}