1use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9
10use anyhow::Result;
11use tokio::sync::{mpsc, RwLock};
12
13use super::client::LspClient;
14use super::registry::LspServerRegistry;
15use super::types::Diagnostic;
16use crate::config::LspConfig;
17
18#[derive(Debug, Clone)]
25pub enum LspConnectEvent {
26 Started { command: String, ext: String },
28 Failed {
31 command: String,
32 ext: String,
33 error: String,
34 },
35 Warning { ext: String, message: String },
39}
40
41fn extension_to_language_id(ext: &str) -> &str {
43 match ext {
44 "rs" => "rust",
45 "ts" => "typescript",
46 "tsx" => "typescriptreact",
47 "js" => "javascript",
48 "jsx" => "javascriptreact",
49 "py" => "python",
50 "go" => "go",
51 "java" => "java",
52 "c" => "c",
53 "cpp" | "cc" | "cxx" => "cpp",
54 "cs" => "csharp",
55 "rb" => "ruby",
56 "php" => "php",
57 "swift" => "swift",
58 "kt" | "kts" => "kotlin",
59 "scala" => "scala",
60 _ => ext,
61 }
62}
63
64pub struct LspManager {
71 clients: Arc<RwLock<HashMap<String, Arc<LspClient>>>>,
73 registry: LspServerRegistry,
75 project_root: PathBuf,
77 enabled: bool,
79 diagnostics_settle_delay_ms: u64,
81 connect_events: Option<mpsc::UnboundedSender<LspConnectEvent>>,
87}
88
89impl LspManager {
90 pub fn new(
94 project_root: PathBuf,
95 registry: LspServerRegistry,
96 enabled: bool,
97 diagnostics_settle_delay_ms: u64,
98 ) -> Self {
99 Self {
100 clients: Arc::new(RwLock::new(HashMap::new())),
101 registry,
102 project_root,
103 enabled,
104 diagnostics_settle_delay_ms,
105 connect_events: None,
106 }
107 }
108
109 pub fn with_event_channel(
114 project_root: PathBuf,
115 registry: LspServerRegistry,
116 enabled: bool,
117 diagnostics_settle_delay_ms: u64,
118 ) -> (Self, mpsc::UnboundedReceiver<LspConnectEvent>) {
119 let (tx, rx) = mpsc::unbounded_channel();
120 let mgr = Self {
121 clients: Arc::new(RwLock::new(HashMap::new())),
122 registry,
123 project_root,
124 enabled,
125 diagnostics_settle_delay_ms,
126 connect_events: Some(tx),
127 };
128 (mgr, rx)
129 }
130
131 fn emit(&self, event: LspConnectEvent) {
135 if let Some(tx) = &self.connect_events {
136 let _ = tx.send(event);
137 }
138 }
139
140 pub fn diagnostics_settle_delay_ms(&self) -> u64 {
142 self.diagnostics_settle_delay_ms
143 }
144
145 pub async fn ensure_server(&self, file_path: &Path) -> Result<bool> {
149 if !self.enabled {
150 return Ok(false);
151 }
152
153 let ext = match file_path.extension().and_then(|e| e.to_str()) {
154 Some(e) => e.to_string(),
155 None => return Ok(false),
156 };
157
158 {
160 let clients = self.clients.read().await;
161 if clients.contains_key(&ext) {
162 return Ok(true);
163 }
164 }
165
166 let config = match self.registry.get(&ext) {
168 Some(c) => c.clone(),
169 None => return Ok(false),
170 };
171
172 if which::which(&config.command).is_err() {
174 return Ok(false);
175 }
176
177 let language_id = extension_to_language_id(&ext);
178
179 let mut clients = self.clients.write().await;
181 if clients.contains_key(&ext) {
182 return Ok(true);
183 }
184
185 match LspClient::start(&config, &self.project_root, language_id).await {
187 Ok(client) => {
188 let arc = Arc::new(client);
189 clients.insert(ext.clone(), arc);
190 self.emit(LspConnectEvent::Started {
191 command: config.command.clone(),
192 ext,
193 });
194 Ok(true)
195 }
196 Err(e) => {
197 self.emit(LspConnectEvent::Failed {
203 command: config.command.clone(),
204 ext,
205 error: e.to_string(),
206 });
207 Ok(false)
208 }
209 }
210 }
211
212 pub async fn diagnostics(&self, path: &Path) -> Vec<Diagnostic> {
215 let ext = match path.extension().and_then(|e| e.to_str()) {
216 Some(e) => e.to_string(),
217 None => return Vec::new(),
218 };
219
220 let clients = self.clients.read().await;
221 match clients.get(&ext) {
222 Some(client) => client.diagnostics(path).await,
223 None => Vec::new(),
224 }
225 }
226
227 pub async fn all_diagnostics(&self) -> Vec<Diagnostic> {
230 let clients = self.clients.read().await;
231 let mut all = Vec::new();
232 for client in clients.values() {
233 all.extend(client.all_diagnostics().await);
234 }
235 all
236 }
237
238 pub async fn notify_file_changed(&self, path: &Path, content: &str) -> Result<bool> {
242 if !self.ensure_server(path).await? {
243 return Ok(false);
244 }
245
246 let ext = match path.extension().and_then(|e| e.to_str()) {
247 Some(e) => e.to_string(),
248 None => return Ok(false),
249 };
250
251 let clients = self.clients.read().await;
252 if let Some(client) = clients.get(&ext) {
253 let language_id = extension_to_language_id(&ext);
254 client.sync_document(path, content, language_id).await?;
256 return Ok(true);
257 }
258
259 Ok(false)
260 }
261
262 pub async fn active_servers(&self) -> Vec<String> {
265 let clients = self.clients.read().await;
266 let mut exts: Vec<String> = clients.keys().cloned().collect();
267 exts.sort();
268 exts
269 }
270
271 pub async fn shutdown(&self) {
275 let mut clients = self.clients.write().await;
276 for (ext, client) in clients.drain() {
277 if let Err(e) = client.shutdown().await {
278 self.emit(LspConnectEvent::Warning {
282 ext,
283 message: format!("shutdown error: {}", e),
284 });
285 }
286 }
287 }
288}
289
290pub fn build_lsp_manager(config: &LspConfig, project_root: &Path) -> Option<Arc<LspManager>> {
293 if !config.enabled {
294 return None;
295 }
296 let registry = build_registry(config);
297 let manager = LspManager::new(
298 project_root.to_path_buf(),
299 registry,
300 true,
301 config.diagnostics_settle_delay_ms,
302 );
303 Some(Arc::new(manager))
304}
305
306pub fn build_lsp_manager_with_events(
312 config: &LspConfig,
313 project_root: &Path,
314) -> Option<(Arc<LspManager>, mpsc::UnboundedReceiver<LspConnectEvent>)> {
315 if !config.enabled {
316 return None;
317 }
318 let registry = build_registry(config);
319 let (manager, rx) = LspManager::with_event_channel(
320 project_root.to_path_buf(),
321 registry,
322 true,
323 config.diagnostics_settle_delay_ms,
324 );
325 Some((Arc::new(manager), rx))
326}
327
328fn build_registry(config: &LspConfig) -> LspServerRegistry {
331 let mut registry = if config.auto_detect {
332 LspServerRegistry::with_defaults()
333 } else {
334 LspServerRegistry::empty()
335 };
336 registry.merge_user_config(config.servers.clone());
337 registry
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 #[test]
345 fn extension_to_language_id_maps_common_langs() {
346 assert_eq!(extension_to_language_id("rs"), "rust");
347 assert_eq!(extension_to_language_id("ts"), "typescript");
348 assert_eq!(extension_to_language_id("tsx"), "typescriptreact");
349 assert_eq!(extension_to_language_id("py"), "python");
350 assert_eq!(extension_to_language_id("go"), "go");
351 assert_eq!(extension_to_language_id("java"), "java");
352 assert_eq!(extension_to_language_id("js"), "javascript");
353 }
354
355 #[test]
356 fn extension_to_language_id_unknown_returns_self() {
357 assert_eq!(extension_to_language_id("xyz"), "xyz");
358 }
359
360 #[tokio::test]
361 async fn disabled_manager_returns_false() {
362 let registry = LspServerRegistry::with_defaults();
363 let mgr = LspManager::new(PathBuf::from("/tmp"), registry, false, 150);
364 let result = mgr.ensure_server(Path::new("test.rs")).await.unwrap();
365 assert!(!result);
366 }
367
368 #[tokio::test]
369 async fn no_config_for_extension_returns_false() {
370 let registry = LspServerRegistry::with_defaults();
371 let mgr = LspManager::new(PathBuf::from("/tmp"), registry, true, 150);
372 let result = mgr.ensure_server(Path::new("test.xyz")).await.unwrap();
373 assert!(!result);
374 }
375
376 #[tokio::test]
377 async fn no_extension_returns_false() {
378 let registry = LspServerRegistry::with_defaults();
379 let mgr = LspManager::new(PathBuf::from("/tmp"), registry, true, 150);
380 let result = mgr.ensure_server(Path::new("Makefile")).await.unwrap();
381 assert!(!result);
382 }
383
384 #[tokio::test]
385 async fn empty_diagnostics_for_unknown_file() {
386 let registry = LspServerRegistry::with_defaults();
387 let mgr = LspManager::new(PathBuf::from("/tmp"), registry, true, 150);
388 let diags = mgr.diagnostics(Path::new("test.xyz")).await;
389 assert!(diags.is_empty());
390 }
391
392 #[tokio::test]
393 async fn active_servers_empty_initially() {
394 let registry = LspServerRegistry::with_defaults();
395 let mgr = LspManager::new(PathBuf::from("/tmp"), registry, true, 150);
396 assert!(mgr.active_servers().await.is_empty());
397 }
398
399 #[tokio::test]
400 async fn all_diagnostics_empty_initially() {
401 let registry = LspServerRegistry::with_defaults();
402 let mgr = LspManager::new(PathBuf::from("/tmp"), registry, true, 150);
403 assert!(mgr.all_diagnostics().await.is_empty());
404 }
405
406 #[tokio::test]
407 async fn shutdown_on_empty_is_noop() {
408 let registry = LspServerRegistry::with_defaults();
409 let mgr = LspManager::new(PathBuf::from("/tmp"), registry, true, 150);
410 mgr.shutdown().await; }
412
413 #[test]
414 fn build_lsp_manager_returns_none_when_disabled() {
415 let config = LspConfig {
416 enabled: false,
417 auto_detect: true,
418 servers: Default::default(),
419 diagnostics_settle_delay_ms: 150,
420 };
421 let result = build_lsp_manager(&config, Path::new("/tmp"));
422 assert!(result.is_none());
423 }
424
425 #[test]
426 fn build_lsp_manager_returns_some_when_enabled() {
427 let config = LspConfig {
428 enabled: true,
429 auto_detect: true,
430 servers: Default::default(),
431 diagnostics_settle_delay_ms: 150,
432 };
433 let result = build_lsp_manager(&config, Path::new("/tmp"));
434 assert!(result.is_some());
435 }
436
437 #[test]
438 fn build_lsp_manager_respects_auto_detect() {
439 let config = LspConfig {
441 enabled: true,
442 auto_detect: false,
443 servers: Default::default(),
444 diagnostics_settle_delay_ms: 150,
445 };
446 let result = build_lsp_manager(&config, Path::new("/tmp"));
447 assert!(result.is_some());
448 }
450
451 #[test]
452 fn build_lsp_manager_merges_user_servers() {
453 let mut servers = std::collections::HashMap::new();
454 servers.insert(
455 "xyz".to_string(),
456 super::super::registry::LspServerConfig {
457 command: "my-lsp".to_string(),
458 args: vec![],
459 root_markers: vec![],
460 },
461 );
462 let config = LspConfig {
463 enabled: true,
464 auto_detect: true,
465 servers,
466 diagnostics_settle_delay_ms: 150,
467 };
468 let result = build_lsp_manager(&config, Path::new("/tmp"));
469 assert!(result.is_some());
470 }
471
472 #[tokio::test]
476 async fn with_event_channel_yields_empty_receiver_initially() {
477 let registry = LspServerRegistry::with_defaults();
478 let (mgr, mut rx) =
479 LspManager::with_event_channel(PathBuf::from("/tmp"), registry, true, 150);
480 assert!(mgr.active_servers().await.is_empty());
481 assert!(rx.try_recv().is_err(), "no events expected before any ensure_server call");
482 }
483
484 #[tokio::test]
494 async fn ensure_server_silent_when_command_missing() {
495 let mut servers = std::collections::HashMap::new();
496 servers.insert(
497 "xyz".to_string(),
498 super::super::registry::LspServerConfig {
499 command: "atomcode-lsp-does-not-exist".to_string(),
500 args: vec![],
501 root_markers: vec![],
502 },
503 );
504 let mut registry = LspServerRegistry::empty();
505 registry.merge_user_config(servers);
506 let (mgr, mut rx) =
507 LspManager::with_event_channel(PathBuf::from("/tmp"), registry, true, 150);
508 let result = mgr.ensure_server(Path::new("test.xyz")).await.unwrap();
509 assert!(!result, "missing command must return Ok(false)");
510 assert!(rx.try_recv().is_err(), "no event expected for missing command");
515 }
516
517 #[tokio::test]
521 async fn emit_no_op_when_receiver_dropped() {
522 let registry = LspServerRegistry::with_defaults();
523 let (mgr, rx) =
524 LspManager::with_event_channel(PathBuf::from("/tmp"), registry, true, 150);
525 drop(rx);
526 mgr.emit(LspConnectEvent::Warning {
530 ext: "rs".to_string(),
531 message: "synthetic post-drop emit".to_string(),
532 });
533 }
535}