Skip to main content

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;