ricecoder_external_lsp/
storage_integration.rs

1//! Storage integration for external LSP configuration
2//!
3//! This module provides integration with ricecoder-storage for loading and managing
4//! LSP server configurations, using the centralized storage system for path resolution
5//! and configuration hierarchy.
6//!
7//! # Configuration Hierarchy
8//!
9//! Configurations are loaded in the following priority order (highest to lowest):
10//! 1. Runtime overrides (programmatic configuration)
11//! 2. Project-level configuration (`.ricecoder/lsp-servers.yaml`)
12//! 3. User-level configuration (`~/.ricecoder/lsp-servers.yaml`)
13//! 4. Built-in defaults (pre-configured servers)
14//! 5. Fallback (internal providers)
15//!
16//! # Example
17//!
18//! ```ignore
19//! use ricecoder_external_lsp::storage_integration::StorageConfigLoader;
20//! use ricecoder_storage::StorageManager;
21//!
22//! let storage_manager = StorageManager::new()?;
23//! let config_loader = StorageConfigLoader::new(storage_manager);
24//! let registry = config_loader.load_registry()?;
25//! ```
26
27use crate::error::Result;
28use crate::types::{GlobalLspSettings, LspServerConfig, LspServerRegistry};
29use serde_json::Value;
30use std::collections::HashMap;
31use std::path::PathBuf;
32use tracing::{debug, info, warn};
33
34/// Storage-based configuration loader for LSP servers
35///
36/// This loader integrates with ricecoder-storage to load LSP server configurations
37/// from multiple sources with proper hierarchy and path resolution.
38pub struct StorageConfigLoader;
39
40impl StorageConfigLoader {
41    /// Create a new storage-based configuration loader
42    pub fn new() -> Self {
43        Self
44    }
45
46    /// Load LSP server registry from storage
47    ///
48    /// Loads configurations from multiple sources in priority order:
49    /// 1. Project-level configuration
50    /// 2. User-level configuration
51    /// 3. Built-in defaults
52    ///
53    /// # Returns
54    ///
55    /// LSP server registry with all configured servers
56    pub fn load_registry(&self) -> Result<LspServerRegistry> {
57        info!("Loading LSP server registry from storage");
58
59        // Start with built-in defaults
60        let mut servers: HashMap<String, Vec<LspServerConfig>> = HashMap::new();
61        let mut global_settings = GlobalLspSettings::default();
62
63        // Load project-level configuration
64        if let Ok(project_config) = self.load_project_config() {
65            debug!("Loaded project-level LSP configuration");
66            self.merge_config(&mut servers, &mut global_settings, project_config)?;
67        }
68
69        // Load user-level configuration
70        if let Ok(user_config) = self.load_user_config() {
71            debug!("Loaded user-level LSP configuration");
72            self.merge_config(&mut servers, &mut global_settings, user_config)?;
73        }
74
75        // Load built-in defaults
76        let builtin_config = self.load_builtin_config()?;
77        debug!("Loaded built-in LSP configuration");
78        self.merge_config(&mut servers, &mut global_settings, builtin_config)?;
79
80        Ok(LspServerRegistry {
81            servers,
82            global: global_settings,
83        })
84    }
85
86    /// Load project-level configuration
87    fn load_project_config(&self) -> Result<Value> {
88        // Try to load from .ricecoder/lsp-servers.yaml
89        let project_config_path = PathBuf::from(".ricecoder/lsp-servers.yaml");
90        debug!("Loading project config from: {:?}", project_config_path);
91
92        if project_config_path.exists() {
93            let content = std::fs::read_to_string(&project_config_path)
94                .map_err(|e| crate::error::ExternalLspError::ConfigError(format!(
95                    "Failed to read project config: {}",
96                    e
97                )))?;
98
99            let config: Value = serde_yaml::from_str(&content)
100                .map_err(|e| crate::error::ExternalLspError::ConfigError(format!(
101                    "Failed to parse project config: {}",
102                    e
103                )))?;
104
105            Ok(config)
106        } else {
107            Err(crate::error::ExternalLspError::ConfigError(
108                "Project config not found".to_string(),
109            ))
110        }
111    }
112
113    /// Load user-level configuration
114    fn load_user_config(&self) -> Result<Value> {
115        // Try to load from ~/.ricecoder/lsp-servers.yaml
116        let home_dir = dirs::home_dir().ok_or_else(|| {
117            crate::error::ExternalLspError::ConfigError("Could not determine home directory".to_string())
118        })?;
119        
120        let user_config_path = home_dir.join(".ricecoder/lsp-servers.yaml");
121        debug!("Loading user config from: {:?}", user_config_path);
122
123        if user_config_path.exists() {
124            let content = std::fs::read_to_string(&user_config_path)
125                .map_err(|e| crate::error::ExternalLspError::ConfigError(format!(
126                    "Failed to read user config: {}",
127                    e
128                )))?;
129
130            let config: Value = serde_yaml::from_str(&content)
131                .map_err(|e| crate::error::ExternalLspError::ConfigError(format!(
132                    "Failed to parse user config: {}",
133                    e
134                )))?;
135
136            Ok(config)
137        } else {
138            Err(crate::error::ExternalLspError::ConfigError(
139                "User config not found".to_string(),
140            ))
141        }
142    }
143
144    /// Load built-in configuration
145    fn load_builtin_config(&self) -> Result<Value> {
146        // Create built-in defaults for common LSP servers
147        let mut servers: HashMap<String, Vec<LspServerConfig>> = HashMap::new();
148        
149        // Rust
150        servers.insert("rust".to_string(), vec![LspServerConfig {
151            language: "rust".to_string(),
152            extensions: vec![".rs".to_string()],
153            executable: "rust-analyzer".to_string(),
154            args: vec![],
155            env: HashMap::new(),
156            init_options: None,
157            enabled: true,
158            timeout_ms: 5000,
159            max_restarts: 3,
160            idle_timeout_ms: 300000,
161            output_mapping: None,
162        }]);
163        
164        // TypeScript
165        servers.insert("typescript".to_string(), vec![LspServerConfig {
166            language: "typescript".to_string(),
167            extensions: vec![".ts".to_string(), ".tsx".to_string(), ".js".to_string(), ".jsx".to_string()],
168            executable: "typescript-language-server".to_string(),
169            args: vec!["--stdio".to_string()],
170            env: HashMap::new(),
171            init_options: None,
172            enabled: true,
173            timeout_ms: 5000,
174            max_restarts: 3,
175            idle_timeout_ms: 300000,
176            output_mapping: None,
177        }]);
178        
179        // Python
180        servers.insert("python".to_string(), vec![LspServerConfig {
181            language: "python".to_string(),
182            extensions: vec![".py".to_string()],
183            executable: "pylsp".to_string(),
184            args: vec![],
185            env: HashMap::new(),
186            init_options: None,
187            enabled: true,
188            timeout_ms: 5000,
189            max_restarts: 3,
190            idle_timeout_ms: 300000,
191            output_mapping: None,
192        }]);
193
194        let config_value = serde_json::json!({
195            "servers": servers,
196            "global": {
197                "max_processes": 5,
198                "default_timeout_ms": 5000,
199                "enable_fallback": true,
200                "health_check_interval_ms": 30000
201            }
202        });
203        
204        Ok(config_value)
205    }
206
207    /// Merge configuration from multiple sources
208    fn merge_config(
209        &self,
210        servers: &mut HashMap<String, Vec<LspServerConfig>>,
211        global_settings: &mut GlobalLspSettings,
212        config: Value,
213    ) -> Result<()> {
214        // Merge servers
215        if let Some(config_servers) = config.get("servers").and_then(|v| v.as_object()) {
216            for (language, server_configs) in config_servers {
217                if let Ok(configs) = serde_json::from_value::<Vec<LspServerConfig>>(
218                    server_configs.clone(),
219                ) {
220                    servers.insert(language.clone(), configs);
221                }
222            }
223        }
224
225        // Merge global settings
226        if let Some(global) = config.get("global").and_then(|v| v.as_object()) {
227            if let Some(max_processes) = global.get("max_processes").and_then(|v| v.as_u64()) {
228                global_settings.max_processes = max_processes as usize;
229            }
230            if let Some(timeout) = global.get("default_timeout_ms").and_then(|v| v.as_u64()) {
231                global_settings.default_timeout_ms = timeout;
232            }
233            if let Some(enable_fallback) = global.get("enable_fallback").and_then(|v| v.as_bool())
234            {
235                global_settings.enable_fallback = enable_fallback;
236            }
237            if let Some(health_check) = global
238                .get("health_check_interval_ms")
239                .and_then(|v| v.as_u64())
240            {
241                global_settings.health_check_interval_ms = health_check;
242            }
243        }
244
245        Ok(())
246    }
247
248    /// Resolve executable path using storage path resolver
249    ///
250    /// # Arguments
251    ///
252    /// * `executable` - Executable name or path
253    ///
254    /// # Returns
255    ///
256    /// Resolved executable path
257    pub fn resolve_executable_path(&self, executable: &str) -> Result<PathBuf> {
258        // Try to resolve using path resolver
259        // First check if it's an absolute path
260        let path = PathBuf::from(executable);
261        if path.is_absolute() && path.exists() {
262            debug!("Resolved executable: {} -> {:?}", executable, path);
263            return Ok(path);
264        }
265        
266        // Try to find in PATH
267        if let Ok(path_env) = std::env::var("PATH") {
268            for path_dir in std::env::split_paths(&path_env) {
269                let full_path = path_dir.join(executable);
270                if full_path.exists() {
271                    debug!("Resolved executable: {} -> {:?}", executable, full_path);
272                    return Ok(full_path);
273                }
274            }
275        }
276        
277        // Fall back to checking current directory
278        let current_path = PathBuf::from(executable);
279        if current_path.exists() {
280            debug!("Resolved executable: {} -> {:?}", executable, current_path);
281            return Ok(current_path);
282        }
283        
284        warn!("Could not resolve executable: {}", executable);
285        Err(crate::error::ExternalLspError::ServerNotFound {
286            executable: executable.to_string(),
287        })
288    }
289
290    /// Cache server state in storage
291    ///
292    /// # Arguments
293    ///
294    /// * `language` - Programming language
295    /// * `_state` - Server state to cache
296    pub fn cache_server_state(&self, language: &str, _state: Value) -> Result<()> {
297        debug!("Caching server state for language: {}", language);
298        
299        // Use storage manager to cache state
300        // This would integrate with ricecoder-storage's caching system
301        // For now, this is a placeholder for future implementation
302        
303        Ok(())
304    }
305
306    /// Load cached server state from storage
307    ///
308    /// # Arguments
309    ///
310    /// * `language` - Programming language
311    ///
312    /// # Returns
313    ///
314    /// Cached server state, or None if not found
315    pub fn load_cached_server_state(&self, language: &str) -> Result<Option<Value>> {
316        debug!("Loading cached server state for language: {}", language);
317        
318        // Use storage manager to load cached state
319        // This would integrate with ricecoder-storage's caching system
320        // For now, this is a placeholder for future implementation
321        
322        Ok(None)
323    }
324}
325
326impl Default for StorageConfigLoader {
327    fn default() -> Self {
328        Self::new()
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    #[test]
337    fn test_storage_config_loader_creation() {
338        // This test would require a mock StorageManager
339        // For now, we just verify the struct can be created
340        let _loader = StorageConfigLoader::new();
341    }
342}