reovim_plugin_treesitter/
registry.rs

1//! Language registry for dynamic language registration
2//!
3//! Language plugins implement the `LanguageSupport` trait and register
4//! themselves with the treesitter plugin at runtime.
5
6use std::{
7    collections::HashMap,
8    path::Path,
9    sync::{Arc, RwLock},
10};
11
12use tree_sitter::Language;
13
14/// Trait for language plugins to implement
15///
16/// Language plugins provide the tree-sitter grammar and query files
17/// for a specific language. They register themselves with the treesitter
18/// plugin using the `RegisterLanguage` event.
19pub trait LanguageSupport: Send + Sync + 'static {
20    /// Unique identifier for this language (e.g., "rust", "python")
21    fn language_id(&self) -> &'static str;
22
23    /// File extensions that map to this language (e.g., ["rs"] for Rust)
24    fn file_extensions(&self) -> &'static [&'static str];
25
26    /// The tree-sitter Language for this grammar
27    fn tree_sitter_language(&self) -> Language;
28
29    /// Highlight query (required)
30    fn highlights_query(&self) -> &'static str;
31
32    /// Fold query (optional)
33    fn folds_query(&self) -> Option<&'static str> {
34        None
35    }
36
37    /// Text objects query (optional)
38    fn textobjects_query(&self) -> Option<&'static str> {
39        None
40    }
41
42    /// Decorations query (optional, used by markdown)
43    fn decorations_query(&self) -> Option<&'static str> {
44        None
45    }
46
47    /// Injections query for embedded languages (optional)
48    fn injections_query(&self) -> Option<&'static str> {
49        None
50    }
51
52    /// Context query for scope detection (optional)
53    ///
54    /// Used by the context plugin to detect enclosing scopes for sticky headers
55    /// and statusline breadcrumbs. Returns tree-sitter query patterns that capture
56    /// context-relevant nodes (e.g., functions, classes, headings).
57    ///
58    /// # Example
59    ///
60    /// Rust: `(function_item) @context (impl_item) @context (struct_item) @context`
61    /// Markdown: `(atx_heading) @context`
62    fn context_query(&self) -> Option<&'static str> {
63        None
64    }
65}
66
67/// A registered language with all its data
68pub struct RegisteredLanguage {
69    /// The language support implementation
70    pub support: Arc<dyn LanguageSupport>,
71    /// Compiled tree-sitter Language (cached)
72    language: Language,
73}
74
75impl RegisteredLanguage {
76    /// Create a new registered language
77    pub fn new(support: Arc<dyn LanguageSupport>) -> Self {
78        let language = support.tree_sitter_language();
79        Self { support, language }
80    }
81
82    /// Get the language ID
83    pub fn language_id(&self) -> &'static str {
84        self.support.language_id()
85    }
86
87    /// Get the tree-sitter Language
88    pub fn language(&self) -> &Language {
89        &self.language
90    }
91
92    /// Get file extensions
93    pub fn file_extensions(&self) -> &'static [&'static str] {
94        self.support.file_extensions()
95    }
96
97    /// Get highlights query
98    pub fn highlights_query(&self) -> &'static str {
99        self.support.highlights_query()
100    }
101
102    /// Get folds query
103    pub fn folds_query(&self) -> Option<&'static str> {
104        self.support.folds_query()
105    }
106
107    /// Get text objects query
108    pub fn textobjects_query(&self) -> Option<&'static str> {
109        self.support.textobjects_query()
110    }
111
112    /// Get decorations query
113    pub fn decorations_query(&self) -> Option<&'static str> {
114        self.support.decorations_query()
115    }
116
117    /// Get injections query
118    pub fn injections_query(&self) -> Option<&'static str> {
119        self.support.injections_query()
120    }
121
122    /// Get context query
123    pub fn context_query(&self) -> Option<&'static str> {
124        self.support.context_query()
125    }
126}
127
128/// Registry for dynamically registered languages
129///
130/// Language plugins register themselves at startup, and the treesitter
131/// plugin uses this registry to detect languages and get their grammars.
132pub struct LanguageRegistry {
133    /// Registered languages by ID
134    languages: RwLock<HashMap<String, Arc<RegisteredLanguage>>>,
135    /// Extension to language ID mapping
136    extension_map: RwLock<HashMap<String, String>>,
137}
138
139impl Default for LanguageRegistry {
140    fn default() -> Self {
141        Self::new()
142    }
143}
144
145impl LanguageRegistry {
146    /// Create a new empty language registry
147    #[must_use]
148    pub fn new() -> Self {
149        Self {
150            languages: RwLock::new(HashMap::new()),
151            extension_map: RwLock::new(HashMap::new()),
152        }
153    }
154
155    /// Register a language support implementation
156    pub fn register(&self, support: Arc<dyn LanguageSupport>) {
157        let id = support.language_id().to_string();
158        let extensions = support.file_extensions();
159
160        let registered = Arc::new(RegisteredLanguage::new(support));
161
162        // Register in language map
163        self.languages
164            .write()
165            .unwrap()
166            .insert(id.clone(), registered);
167
168        // Register extensions
169        let mut ext_map = self.extension_map.write().unwrap();
170        for ext in extensions {
171            ext_map.insert((*ext).to_string(), id.clone());
172        }
173
174        tracing::debug!(language_id = %id, extensions = ?extensions, "Registered language");
175    }
176
177    /// Detect language from file path based on extension
178    #[must_use]
179    pub fn detect_language(&self, path: &str) -> Option<String> {
180        Path::new(path)
181            .extension()
182            .and_then(|e| e.to_str())
183            .and_then(|ext| self.extension_map.read().unwrap().get(ext).cloned())
184    }
185
186    /// Get a registered language by ID
187    #[must_use]
188    pub fn get(&self, id: &str) -> Option<Arc<RegisteredLanguage>> {
189        self.languages.read().unwrap().get(id).cloned()
190    }
191
192    /// Get the tree-sitter Language for a language ID
193    #[must_use]
194    pub fn get_language(&self, id: &str) -> Option<Language> {
195        self.languages
196            .read()
197            .unwrap()
198            .get(id)
199            .map(|l| l.language().clone())
200    }
201
202    /// Check if a language is registered
203    #[must_use]
204    pub fn is_registered(&self, id: &str) -> bool {
205        self.languages.read().unwrap().contains_key(id)
206    }
207
208    /// Get all registered language IDs
209    #[must_use]
210    pub fn language_ids(&self) -> Vec<String> {
211        self.languages.read().unwrap().keys().cloned().collect()
212    }
213}