ricecoder_completion/
config.rs

1/// Configuration loading and management for completion engine
2use crate::types::*;
3use ricecoder_storage::manager::PathResolver;
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6
7/// Completion configuration loader with storage integration
8pub struct ConfigLoader;
9
10impl ConfigLoader {
11    /// Load completion configuration from a YAML file
12    pub fn load_from_yaml(path: &Path) -> CompletionResult<CompletionConfig> {
13        let content = std::fs::read_to_string(path)?;
14        let config: CompletionConfig = serde_yaml::from_str(&content)?;
15        Self::validate_config(&config)?;
16        Ok(config)
17    }
18
19    /// Load completion configuration from a JSON file
20    pub fn load_from_json(path: &Path) -> CompletionResult<CompletionConfig> {
21        let content = std::fs::read_to_string(path)?;
22        let config: CompletionConfig = serde_json::from_str(&content)?;
23        Self::validate_config(&config)?;
24        Ok(config)
25    }
26
27    /// Load completion configuration from a string
28    pub fn load_from_string(
29        content: &str,
30        format: ConfigFormat,
31    ) -> CompletionResult<CompletionConfig> {
32        let config = match format {
33            ConfigFormat::Yaml => serde_yaml::from_str(content)?,
34            ConfigFormat::Json => serde_json::from_str(content)?,
35        };
36        Self::validate_config(&config)?;
37        Ok(config)
38    }
39
40    /// Load completion configuration with hierarchy: Runtime → Project → User → Built-in → Fallback
41    ///
42    /// # Configuration Hierarchy
43    ///
44    /// 1. **Runtime**: Configuration passed at runtime (highest priority)
45    /// 2. **Project**: Configuration in `.agent/completion/languages/`
46    /// 3. **User**: Configuration in `~/.ricecoder/completion/languages/`
47    /// 4. **Built-in**: Built-in configurations embedded in ricecoder-storage
48    /// 5. **Fallback**: Default configuration for the language (lowest priority)
49    pub fn load_with_hierarchy(language: &str) -> CompletionResult<CompletionConfig> {
50        // Try project-level configuration first
51        let project_path = PathResolver::resolve_project_path();
52        let project_completion_path = project_path.join("completion").join("languages");
53
54        if let Ok(config) = Self::load_from_directory(&project_completion_path, language) {
55            return Ok(config);
56        }
57
58        // Try user-level configuration
59        let global_path = PathResolver::resolve_global_path()?;
60        let user_completion_path = global_path.join("completion").join("languages");
61
62        if let Ok(config) = Self::load_from_directory(&user_completion_path, language) {
63            return Ok(config);
64        }
65
66        // Fall back to default configuration
67        Ok(Self::default_for_language(language))
68    }
69
70    /// Load configuration from a directory for a specific language
71    fn load_from_directory(dir: &Path, language: &str) -> CompletionResult<CompletionConfig> {
72        if !dir.is_dir() {
73            return Err(CompletionError::ConfigError(format!(
74                "Configuration directory not found: {}",
75                dir.display()
76            )));
77        }
78
79        // Try YAML first
80        let yaml_path = dir.join(format!("{}.yaml", language));
81        if yaml_path.exists() {
82            return Self::load_from_yaml(&yaml_path);
83        }
84
85        let yml_path = dir.join(format!("{}.yml", language));
86        if yml_path.exists() {
87            return Self::load_from_yaml(&yml_path);
88        }
89
90        // Try JSON
91        let json_path = dir.join(format!("{}.json", language));
92        if json_path.exists() {
93            return Self::load_from_json(&json_path);
94        }
95
96        Err(CompletionError::ConfigError(format!(
97            "No configuration found for language: {}",
98            language
99        )))
100    }
101
102    /// Get the completion configuration directory path
103    pub fn get_completion_config_dir() -> CompletionResult<PathBuf> {
104        let global_path = PathResolver::resolve_global_path()?;
105        Ok(global_path.join("completion").join("languages"))
106    }
107
108    /// Get the project completion configuration directory path
109    pub fn get_project_completion_config_dir() -> PathBuf {
110        let project_path = PathResolver::resolve_project_path();
111        project_path.join("completion").join("languages")
112    }
113
114    /// Validate completion configuration
115    fn validate_config(config: &CompletionConfig) -> CompletionResult<()> {
116        if config.language.is_empty() {
117            return Err(CompletionError::ConfigError(
118                "Language name cannot be empty".to_string(),
119            ));
120        }
121
122        if config.ranking_weights.relevance < 0.0
123            || config.ranking_weights.frequency < 0.0
124            || config.ranking_weights.recency < 0.0
125        {
126            return Err(CompletionError::ConfigError(
127                "Ranking weights must be non-negative".to_string(),
128            ));
129        }
130
131        Ok(())
132    }
133
134    /// Create a default configuration for a language
135    pub fn default_for_language(language: &str) -> CompletionConfig {
136        CompletionConfig::new(language.to_string())
137    }
138}
139
140/// Configuration format
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub enum ConfigFormat {
143    Yaml,
144    Json,
145}
146
147/// Language configuration registry with storage integration
148pub struct LanguageConfigRegistry {
149    configs: HashMap<String, CompletionConfig>,
150}
151
152impl LanguageConfigRegistry {
153    pub fn new() -> Self {
154        Self {
155            configs: HashMap::new(),
156        }
157    }
158
159    /// Create a registry and load configurations from storage hierarchy
160    pub fn with_hierarchy() -> CompletionResult<Self> {
161        let mut registry = Self::new();
162
163        // Load from project directory
164        let project_dir = ConfigLoader::get_project_completion_config_dir();
165        let _ = registry.load_from_directory(&project_dir);
166
167        // Load from user directory (overrides project)
168        if let Ok(user_dir) = ConfigLoader::get_completion_config_dir() {
169            let _ = registry.load_from_directory(&user_dir);
170        }
171
172        Ok(registry)
173    }
174
175    pub fn register(&mut self, config: CompletionConfig) {
176        self.configs.insert(config.language.clone(), config);
177    }
178
179    pub fn get(&self, language: &str) -> Option<&CompletionConfig> {
180        self.configs.get(language)
181    }
182
183    pub fn get_mut(&mut self, language: &str) -> Option<&mut CompletionConfig> {
184        self.configs.get_mut(language)
185    }
186
187    pub fn list_languages(&self) -> Vec<String> {
188        self.configs.keys().cloned().collect()
189    }
190
191    pub fn unregister(&mut self, language: &str) -> Option<CompletionConfig> {
192        self.configs.remove(language)
193    }
194
195    pub fn load_from_directory(&mut self, dir: &Path) -> CompletionResult<()> {
196        if !dir.is_dir() {
197            return Err(CompletionError::ConfigError(format!(
198                "Configuration directory not found: {}",
199                dir.display()
200            )));
201        }
202
203        for entry in std::fs::read_dir(dir)? {
204            let entry = entry?;
205            let path = entry.path();
206
207            if path.is_file() {
208                if let Some(ext) = path.extension() {
209                    let config = match ext.to_str() {
210                        Some("yaml") | Some("yml") => ConfigLoader::load_from_yaml(&path)?,
211                        Some("json") => ConfigLoader::load_from_json(&path)?,
212                        _ => continue,
213                    };
214                    self.register(config);
215                }
216            }
217        }
218
219        Ok(())
220    }
221}
222
223impl Default for LanguageConfigRegistry {
224    fn default() -> Self {
225        Self::new()
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn test_config_loader_validate_empty_language() {
235        let config = CompletionConfig {
236            language: String::new(),
237            keywords: Vec::new(),
238            snippets: Vec::new(),
239            ranking_weights: RankingWeights::default(),
240            provider: None,
241        };
242
243        let result = ConfigLoader::validate_config(&config);
244        assert!(result.is_err());
245    }
246
247    #[test]
248    fn test_config_loader_validate_negative_weights() {
249        let config = CompletionConfig {
250            language: "rust".to_string(),
251            keywords: Vec::new(),
252            snippets: Vec::new(),
253            ranking_weights: RankingWeights {
254                relevance: -0.1,
255                frequency: 0.3,
256                recency: 0.2,
257            },
258            provider: None,
259        };
260
261        let result = ConfigLoader::validate_config(&config);
262        assert!(result.is_err());
263    }
264
265    #[test]
266    fn test_config_loader_validate_valid() {
267        let config = CompletionConfig {
268            language: "rust".to_string(),
269            keywords: vec!["fn".to_string(), "let".to_string()],
270            snippets: Vec::new(),
271            ranking_weights: RankingWeights::default(),
272            provider: None,
273        };
274
275        let result = ConfigLoader::validate_config(&config);
276        assert!(result.is_ok());
277    }
278
279    #[test]
280    fn test_language_config_registry_register() {
281        let mut registry = LanguageConfigRegistry::new();
282        let config = CompletionConfig::new("rust".to_string());
283        registry.register(config);
284
285        assert!(registry.get("rust").is_some());
286        assert_eq!(registry.list_languages().len(), 1);
287    }
288
289    #[test]
290    fn test_language_config_registry_unregister() {
291        let mut registry = LanguageConfigRegistry::new();
292        let config = CompletionConfig::new("rust".to_string());
293        registry.register(config);
294
295        let removed = registry.unregister("rust");
296        assert!(removed.is_some());
297        assert!(registry.get("rust").is_none());
298    }
299
300    #[test]
301    fn test_config_default_for_language() {
302        let config = ConfigLoader::default_for_language("typescript");
303        assert_eq!(config.language, "typescript");
304        assert!(config.keywords.is_empty());
305    }
306
307    #[test]
308    fn test_config_loader_hierarchy_fallback() {
309        // Test that load_with_hierarchy returns default when no config found
310        let config = ConfigLoader::load_with_hierarchy("unknown_language");
311        assert!(config.is_ok());
312        let cfg = config.unwrap();
313        assert_eq!(cfg.language, "unknown_language");
314    }
315
316    #[test]
317    fn test_language_config_registry_with_hierarchy() {
318        // Test that registry can be created with hierarchy
319        let registry = LanguageConfigRegistry::with_hierarchy();
320        assert!(registry.is_ok());
321    }
322}