reovim_driver_syntax/registry.rs
1//! Language metadata and registry types.
2//!
3//! This module defines types for language metadata (file extensions, MIME types,
4//! comment syntax) and the [`LanguageRegistry`] trait for language detection.
5
6/// Information about a supported language.
7///
8/// Contains metadata-only information about a language, not parsing capabilities.
9/// Used for language detection and editor configuration.
10///
11/// # Example
12///
13/// ```
14/// use reovim_driver_syntax::{LanguageInfo, CommentTokens};
15///
16/// let rust = LanguageInfo::new("rust", "Rust")
17/// .with_extensions(["rs"])
18/// .with_mime_types(["text/x-rust"])
19/// .with_comments(CommentTokens::with_block("//", "/*", "*/"));
20///
21/// assert!(rust.matches_extension("rs"));
22/// assert!(rust.matches_extension("RS")); // Case insensitive
23/// ```
24#[derive(Debug, Clone)]
25pub struct LanguageInfo {
26 /// Language identifier (e.g., "rust", "python").
27 pub id: String,
28 /// Human-readable name (e.g., "Rust", "Python").
29 pub name: String,
30 /// File extensions (without dot, e.g., `["rs"]`, `["py", "pyw"]`).
31 pub extensions: Vec<String>,
32 /// MIME types (e.g., `["text/x-rust"]`).
33 pub mime_types: Vec<String>,
34 /// Comment tokens for this language.
35 pub comment_tokens: CommentTokens,
36}
37
38impl LanguageInfo {
39 /// Create new language info with minimal fields.
40 #[must_use]
41 pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
42 Self {
43 id: id.into(),
44 name: name.into(),
45 extensions: Vec::new(),
46 mime_types: Vec::new(),
47 comment_tokens: CommentTokens::default(),
48 }
49 }
50
51 /// Add file extensions.
52 #[must_use]
53 pub fn with_extensions<I, S>(mut self, extensions: I) -> Self
54 where
55 I: IntoIterator<Item = S>,
56 S: Into<String>,
57 {
58 self.extensions = extensions.into_iter().map(Into::into).collect();
59 self
60 }
61
62 /// Add MIME types.
63 #[must_use]
64 pub fn with_mime_types<I, S>(mut self, mime_types: I) -> Self
65 where
66 I: IntoIterator<Item = S>,
67 S: Into<String>,
68 {
69 self.mime_types = mime_types.into_iter().map(Into::into).collect();
70 self
71 }
72
73 /// Set comment tokens.
74 #[must_use]
75 pub fn with_comments(mut self, tokens: CommentTokens) -> Self {
76 self.comment_tokens = tokens;
77 self
78 }
79
80 /// Check if this language matches a file extension.
81 ///
82 /// Comparison is case-insensitive.
83 #[must_use]
84 pub fn matches_extension(&self, ext: &str) -> bool {
85 self.extensions.iter().any(|e| e.eq_ignore_ascii_case(ext))
86 }
87
88 /// Check if this language matches a MIME type.
89 ///
90 /// Comparison is case-insensitive.
91 #[must_use]
92 pub fn matches_mime(&self, mime: &str) -> bool {
93 self.mime_types.iter().any(|m| m.eq_ignore_ascii_case(mime))
94 }
95
96 /// Check if this language has any registered extensions.
97 #[must_use]
98 pub const fn has_extensions(&self) -> bool {
99 !self.extensions.is_empty()
100 }
101
102 /// Check if this language has comment syntax defined.
103 #[must_use]
104 pub const fn has_comments(&self) -> bool {
105 self.comment_tokens.has_any()
106 }
107}
108
109/// Comment syntax for a language.
110///
111/// Describes how to create line and block comments.
112///
113/// # Example
114///
115/// ```
116/// use reovim_driver_syntax::CommentTokens;
117///
118/// // C-style comments
119/// let c_style = CommentTokens::with_block("//", "/*", "*/");
120/// assert!(c_style.has_line_comment());
121/// assert!(c_style.has_block_comment());
122///
123/// // Python-style (line only)
124/// let python = CommentTokens::line_only("#");
125/// assert!(python.has_line_comment());
126/// assert!(!python.has_block_comment());
127///
128/// // HTML-style (block only)
129/// let html = CommentTokens::block_only("<!--", "-->");
130/// assert!(!html.has_line_comment());
131/// assert!(html.has_block_comment());
132/// ```
133#[derive(Debug, Clone, Default, PartialEq, Eq)]
134pub struct CommentTokens {
135 /// Line comment prefix (e.g., "//", "#", "--").
136 pub line: Option<String>,
137 /// Block comment start (e.g., "/*", "<!--").
138 pub block_start: Option<String>,
139 /// Block comment end (e.g., "*/", "-->").
140 pub block_end: Option<String>,
141}
142
143impl CommentTokens {
144 /// Create comment tokens with only a line comment.
145 #[must_use]
146 pub fn line_only(prefix: impl Into<String>) -> Self {
147 Self {
148 line: Some(prefix.into()),
149 block_start: None,
150 block_end: None,
151 }
152 }
153
154 /// Create comment tokens with both line and block comments.
155 #[must_use]
156 pub fn with_block(
157 line: impl Into<String>,
158 block_start: impl Into<String>,
159 block_end: impl Into<String>,
160 ) -> Self {
161 Self {
162 line: Some(line.into()),
163 block_start: Some(block_start.into()),
164 block_end: Some(block_end.into()),
165 }
166 }
167
168 /// Create comment tokens with only block comments (no line comments).
169 #[must_use]
170 pub fn block_only(start: impl Into<String>, end: impl Into<String>) -> Self {
171 Self {
172 line: None,
173 block_start: Some(start.into()),
174 block_end: Some(end.into()),
175 }
176 }
177
178 /// Check if line comments are supported.
179 #[must_use]
180 pub const fn has_line_comment(&self) -> bool {
181 self.line.is_some()
182 }
183
184 /// Check if block comments are supported.
185 #[must_use]
186 pub const fn has_block_comment(&self) -> bool {
187 self.block_start.is_some() && self.block_end.is_some()
188 }
189
190 /// Check if any comment syntax is defined.
191 #[must_use]
192 pub const fn has_any(&self) -> bool {
193 self.line.is_some() || (self.block_start.is_some() && self.block_end.is_some())
194 }
195}
196
197/// Registry for language metadata and detection.
198///
199/// Manages the mapping between file extensions, MIME types, and language IDs.
200/// Does not create syntax drivers - that's the factory's job.
201///
202/// Implementations must be thread-safe (`Send + Sync`).
203pub trait LanguageRegistry: Send + Sync {
204 /// Detect language from a file path.
205 ///
206 /// Uses file extension to determine the language.
207 /// Returns the language ID if detected.
208 fn detect_from_path(&self, path: &str) -> Option<String>;
209
210 /// Detect language from MIME type.
211 ///
212 /// Returns the language ID if a matching language is found.
213 fn detect_from_mime(&self, mime: &str) -> Option<String>;
214
215 /// Get language info by ID.
216 fn get_info(&self, language_id: &str) -> Option<&LanguageInfo>;
217
218 /// Check if a language is registered.
219 fn is_registered(&self, language_id: &str) -> bool;
220
221 /// List all registered language IDs.
222 fn language_ids(&self) -> Vec<String>;
223
224 /// Get the total number of registered languages.
225 fn len(&self) -> usize {
226 self.language_ids().len()
227 }
228
229 /// Check if the registry is empty.
230 fn is_empty(&self) -> bool {
231 self.len() == 0
232 }
233}
234
235/// Concrete implementation of [`LanguageRegistry`] built from [`LanguageInfo`] entries.
236///
237/// Created at bootstrap from `LanguageInfoStore` entries registered by modules.
238///
239/// # Example
240///
241/// ```
242/// use reovim_driver_syntax::{DefaultLanguageRegistry, LanguageInfo, LanguageRegistry};
243///
244/// let registry = DefaultLanguageRegistry::new(vec![
245/// LanguageInfo::new("rust", "Rust").with_extensions(["rs"]),
246/// LanguageInfo::new("markdown", "Markdown").with_extensions(["md"]),
247/// ]);
248///
249/// assert_eq!(registry.detect_from_path("main.rs"), Some("rust".to_string()));
250/// assert_eq!(registry.detect_from_path("README.md"), Some("markdown".to_string()));
251/// assert_eq!(registry.detect_from_path("file.txt"), None);
252/// ```
253pub struct DefaultLanguageRegistry {
254 languages: Vec<LanguageInfo>,
255}
256
257impl DefaultLanguageRegistry {
258 /// Create a registry from a list of language info entries.
259 #[must_use]
260 pub const fn new(languages: Vec<LanguageInfo>) -> Self {
261 Self { languages }
262 }
263}
264
265impl LanguageRegistry for DefaultLanguageRegistry {
266 fn detect_from_path(&self, path: &str) -> Option<String> {
267 let ext = std::path::Path::new(path)
268 .extension()
269 .and_then(|e| e.to_str())?;
270 self.languages
271 .iter()
272 .find(|info| info.matches_extension(ext))
273 .map(|info| info.id.clone())
274 }
275
276 fn detect_from_mime(&self, mime: &str) -> Option<String> {
277 self.languages
278 .iter()
279 .find(|info| info.matches_mime(mime))
280 .map(|info| info.id.clone())
281 }
282
283 fn get_info(&self, language_id: &str) -> Option<&LanguageInfo> {
284 self.languages.iter().find(|info| info.id == language_id)
285 }
286
287 fn is_registered(&self, language_id: &str) -> bool {
288 self.languages.iter().any(|info| info.id == language_id)
289 }
290
291 fn language_ids(&self) -> Vec<String> {
292 self.languages.iter().map(|info| info.id.clone()).collect()
293 }
294}
295
296impl std::fmt::Debug for DefaultLanguageRegistry {
297 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298 f.debug_struct("DefaultLanguageRegistry")
299 .field("languages", &self.language_ids())
300 .finish()
301 }
302}
303
304#[cfg(test)]
305#[path = "registry_tests.rs"]
306mod tests;