1use 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
34pub struct StorageConfigLoader;
39
40impl StorageConfigLoader {
41 pub fn new() -> Self {
43 Self
44 }
45
46 pub fn load_registry(&self) -> Result<LspServerRegistry> {
57 info!("Loading LSP server registry from storage");
58
59 let mut servers: HashMap<String, Vec<LspServerConfig>> = HashMap::new();
61 let mut global_settings = GlobalLspSettings::default();
62
63 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 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 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 fn load_project_config(&self) -> Result<Value> {
88 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 fn load_user_config(&self) -> Result<Value> {
115 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 fn load_builtin_config(&self) -> Result<Value> {
146 let mut servers: HashMap<String, Vec<LspServerConfig>> = HashMap::new();
148
149 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 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 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 fn merge_config(
209 &self,
210 servers: &mut HashMap<String, Vec<LspServerConfig>>,
211 global_settings: &mut GlobalLspSettings,
212 config: Value,
213 ) -> Result<()> {
214 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 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 pub fn resolve_executable_path(&self, executable: &str) -> Result<PathBuf> {
258 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 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 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 pub fn cache_server_state(&self, language: &str, _state: Value) -> Result<()> {
297 debug!("Caching server state for language: {}", language);
298
299 Ok(())
304 }
305
306 pub fn load_cached_server_state(&self, language: &str) -> Result<Option<Value>> {
316 debug!("Loading cached server state for language: {}", language);
317
318 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 let _loader = StorageConfigLoader::new();
341 }
342}