ricecoder_lsp/config/
types.rs

1//! Configuration types for language-agnostic, configuration-driven architecture
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use thiserror::Error;
6
7/// Configuration error type
8#[derive(Debug, Error)]
9pub enum ConfigError {
10    /// IO error
11    #[error("IO error: {0}")]
12    IoError(#[from] std::io::Error),
13
14    /// YAML parsing error
15    #[error("YAML parsing error: {0}")]
16    YamlError(#[from] serde_yaml::Error),
17
18    /// JSON parsing error
19    #[error("JSON parsing error: {0}")]
20    JsonError(#[from] serde_json::Error),
21
22    /// Validation error
23    #[error("Validation error: {0}")]
24    ValidationError(String),
25
26    /// Missing configuration
27    #[error("Missing configuration: {0}")]
28    MissingConfig(String),
29}
30
31/// Result type for configuration operations
32pub type ConfigResult<T> = Result<T, ConfigError>;
33
34/// Language configuration
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct LanguageConfig {
37    /// Language identifier (e.g., "rust", "typescript", "python")
38    pub language: String,
39
40    /// File extensions for this language
41    #[serde(default)]
42    pub extensions: Vec<String>,
43
44    /// Parser plugin reference (e.g., "tree-sitter-rust")
45    pub parser_plugin: Option<String>,
46
47    /// Diagnostic rules for this language
48    #[serde(default)]
49    pub diagnostic_rules: Vec<DiagnosticRule>,
50
51    /// Code action transformations for this language
52    #[serde(default)]
53    pub code_actions: Vec<CodeActionTemplate>,
54}
55
56impl LanguageConfig {
57    /// Validate the language configuration
58    pub fn validate(&self) -> ConfigResult<()> {
59        if self.language.is_empty() {
60            return Err(ConfigError::ValidationError(
61                "Language name cannot be empty".to_string(),
62            ));
63        }
64
65        if self.extensions.is_empty() {
66            return Err(ConfigError::ValidationError(format!(
67                "Language '{}' must have at least one file extension",
68                self.language
69            )));
70        }
71
72        Ok(())
73    }
74}
75
76/// Diagnostic rule configuration
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct DiagnosticRule {
79    /// Rule name
80    pub name: String,
81
82    /// Pattern to match (regex or simple pattern)
83    pub pattern: String,
84
85    /// Severity level: "error", "warning", "info"
86    pub severity: String,
87
88    /// Diagnostic message
89    pub message: String,
90
91    /// Optional fix template
92    pub fix_template: Option<String>,
93
94    /// Rule code for identification
95    pub code: Option<String>,
96}
97
98impl DiagnosticRule {
99    /// Validate the diagnostic rule
100    pub fn validate(&self) -> ConfigResult<()> {
101        if self.name.is_empty() {
102            return Err(ConfigError::ValidationError(
103                "Rule name cannot be empty".to_string(),
104            ));
105        }
106
107        if self.pattern.is_empty() {
108            return Err(ConfigError::ValidationError(
109                "Rule pattern cannot be empty".to_string(),
110            ));
111        }
112
113        match self.severity.as_str() {
114            "error" | "warning" | "info" => {}
115            _ => {
116                return Err(ConfigError::ValidationError(format!(
117                    "Invalid severity level: {}",
118                    self.severity
119                )))
120            }
121        }
122
123        Ok(())
124    }
125}
126
127/// Code action template configuration
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct CodeActionTemplate {
130    /// Action name
131    pub name: String,
132
133    /// Action title
134    pub title: String,
135
136    /// Action kind: "quickfix", "refactor", "source"
137    pub kind: String,
138
139    /// Transformation template
140    pub transformation: String,
141}
142
143impl CodeActionTemplate {
144    /// Validate the code action template
145    pub fn validate(&self) -> ConfigResult<()> {
146        if self.name.is_empty() {
147            return Err(ConfigError::ValidationError(
148                "Action name cannot be empty".to_string(),
149            ));
150        }
151
152        if self.title.is_empty() {
153            return Err(ConfigError::ValidationError(
154                "Action title cannot be empty".to_string(),
155            ));
156        }
157
158        match self.kind.as_str() {
159            "quickfix" | "refactor" | "source" => {}
160            _ => {
161                return Err(ConfigError::ValidationError(format!(
162                    "Invalid action kind: {}",
163                    self.kind
164                )))
165            }
166        }
167
168        Ok(())
169    }
170}
171
172/// Completion configuration
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct CompletionConfig {
175    /// Enable/disable completion
176    #[serde(default = "default_true")]
177    pub enabled: bool,
178
179    /// Completion timeout in milliseconds
180    #[serde(default = "default_completion_timeout")]
181    pub timeout_ms: u64,
182
183    /// Maximum number of completions to return
184    #[serde(default = "default_max_completions")]
185    pub max_completions: usize,
186
187    /// Enable ghost text display
188    #[serde(default = "default_true")]
189    pub ghost_text_enabled: bool,
190
191    /// Minimum prefix length to trigger completion
192    #[serde(default = "default_min_prefix_length")]
193    pub min_prefix_length: usize,
194
195    /// Enable fuzzy matching
196    #[serde(default = "default_true")]
197    pub fuzzy_matching: bool,
198
199    /// Enable frequency-based ranking
200    #[serde(default = "default_true")]
201    pub frequency_ranking: bool,
202
203    /// Enable recency-based ranking
204    #[serde(default = "default_true")]
205    pub recency_ranking: bool,
206
207    /// Language-specific completion providers
208    #[serde(default)]
209    pub providers: HashMap<String, String>,
210}
211
212impl Default for CompletionConfig {
213    fn default() -> Self {
214        Self {
215            enabled: true,
216            timeout_ms: 100,
217            max_completions: 50,
218            ghost_text_enabled: true,
219            min_prefix_length: 1,
220            fuzzy_matching: true,
221            frequency_ranking: true,
222            recency_ranking: true,
223            providers: HashMap::new(),
224        }
225    }
226}
227
228impl CompletionConfig {
229    /// Validate the completion configuration
230    pub fn validate(&self) -> ConfigResult<()> {
231        if self.timeout_ms == 0 {
232            return Err(ConfigError::ValidationError(
233                "Completion timeout must be greater than 0".to_string(),
234            ));
235        }
236
237        if self.max_completions == 0 {
238            return Err(ConfigError::ValidationError(
239                "Max completions must be greater than 0".to_string(),
240            ));
241        }
242
243        Ok(())
244    }
245}
246
247/// Default value for enabled fields
248fn default_true() -> bool {
249    true
250}
251
252/// Default completion timeout (100ms)
253fn default_completion_timeout() -> u64 {
254    100
255}
256
257/// Default max completions (50)
258fn default_max_completions() -> usize {
259    50
260}
261
262/// Default min prefix length (1)
263fn default_min_prefix_length() -> usize {
264    1
265}
266
267/// Configuration registry for managing multiple languages
268pub struct ConfigRegistry {
269    /// Language configurations by language identifier
270    languages: HashMap<String, LanguageConfig>,
271
272    /// Completion configuration
273    completion_config: CompletionConfig,
274}
275
276impl ConfigRegistry {
277    /// Create a new configuration registry
278    pub fn new() -> Self {
279        Self {
280            languages: HashMap::new(),
281            completion_config: CompletionConfig::default(),
282        }
283    }
284
285    /// Create a new configuration registry with custom completion config
286    pub fn with_completion_config(completion_config: CompletionConfig) -> ConfigResult<Self> {
287        completion_config.validate()?;
288        Ok(Self {
289            languages: HashMap::new(),
290            completion_config,
291        })
292    }
293
294    /// Register a language configuration
295    pub fn register(&mut self, config: LanguageConfig) -> ConfigResult<()> {
296        config.validate()?;
297
298        // Validate all diagnostic rules
299        for rule in &config.diagnostic_rules {
300            rule.validate()?;
301        }
302
303        // Validate all code action templates
304        for action in &config.code_actions {
305            action.validate()?;
306        }
307
308        self.languages.insert(config.language.clone(), config);
309        Ok(())
310    }
311
312    /// Get a language configuration by identifier
313    pub fn get(&self, language: &str) -> Option<&LanguageConfig> {
314        self.languages.get(language)
315    }
316
317    /// Get a language configuration by file extension
318    pub fn get_by_extension(&self, extension: &str) -> Option<&LanguageConfig> {
319        self.languages
320            .values()
321            .find(|config| config.extensions.contains(&extension.to_string()))
322    }
323
324    /// List all registered languages
325    pub fn languages(&self) -> Vec<&str> {
326        self.languages.keys().map(|s| s.as_str()).collect()
327    }
328
329    /// Check if a language is configured
330    pub fn has_language(&self, language: &str) -> bool {
331        self.languages.contains_key(language)
332    }
333
334    /// Get completion configuration
335    pub fn completion_config(&self) -> &CompletionConfig {
336        &self.completion_config
337    }
338
339    /// Get mutable completion configuration
340    pub fn completion_config_mut(&mut self) -> &mut CompletionConfig {
341        &mut self.completion_config
342    }
343
344    /// Set completion configuration
345    pub fn set_completion_config(&mut self, config: CompletionConfig) -> ConfigResult<()> {
346        config.validate()?;
347        self.completion_config = config;
348        Ok(())
349    }
350}
351
352impl Default for ConfigRegistry {
353    fn default() -> Self {
354        Self::new()
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361
362    #[test]
363    fn test_language_config_validation() {
364        let config = LanguageConfig {
365            language: "rust".to_string(),
366            extensions: vec!["rs".to_string()],
367            parser_plugin: Some("tree-sitter-rust".to_string()),
368            diagnostic_rules: vec![],
369            code_actions: vec![],
370        };
371
372        assert!(config.validate().is_ok());
373    }
374
375    #[test]
376    fn test_language_config_validation_empty_name() {
377        let config = LanguageConfig {
378            language: String::new(),
379            extensions: vec!["rs".to_string()],
380            parser_plugin: None,
381            diagnostic_rules: vec![],
382            code_actions: vec![],
383        };
384
385        assert!(config.validate().is_err());
386    }
387
388    #[test]
389    fn test_diagnostic_rule_validation() {
390        let rule = DiagnosticRule {
391            name: "unused-import".to_string(),
392            pattern: "use .*".to_string(),
393            severity: "warning".to_string(),
394            message: "Unused import".to_string(),
395            fix_template: None,
396            code: Some("unused-import".to_string()),
397        };
398
399        assert!(rule.validate().is_ok());
400    }
401
402    #[test]
403    fn test_config_registry() {
404        let mut registry = ConfigRegistry::new();
405
406        let config = LanguageConfig {
407            language: "rust".to_string(),
408            extensions: vec!["rs".to_string()],
409            parser_plugin: Some("tree-sitter-rust".to_string()),
410            diagnostic_rules: vec![],
411            code_actions: vec![],
412        };
413
414        assert!(registry.register(config).is_ok());
415        assert!(registry.has_language("rust"));
416        assert!(registry.get("rust").is_some());
417        assert!(registry.get_by_extension("rs").is_some());
418    }
419
420    #[test]
421    fn test_completion_config_default() {
422        let config = CompletionConfig::default();
423        assert!(config.enabled);
424        assert_eq!(config.timeout_ms, 100);
425        assert_eq!(config.max_completions, 50);
426        assert!(config.ghost_text_enabled);
427    }
428
429    #[test]
430    fn test_completion_config_validation() {
431        let config = CompletionConfig::default();
432        assert!(config.validate().is_ok());
433    }
434
435    #[test]
436    fn test_completion_config_validation_invalid_timeout() {
437        let mut config = CompletionConfig::default();
438        config.timeout_ms = 0;
439        assert!(config.validate().is_err());
440    }
441
442    #[test]
443    fn test_config_registry_completion_config() {
444        let mut registry = ConfigRegistry::new();
445        let completion_config = registry.completion_config();
446        assert!(completion_config.enabled);
447
448        let mut new_config = CompletionConfig::default();
449        new_config.enabled = false;
450        assert!(registry.set_completion_config(new_config).is_ok());
451        assert!(!registry.completion_config().enabled);
452    }
453}