1mod client;
4mod language;
5mod server;
6mod types;
7
8pub use language::{LanguageConfig, detect_language, find_server_config, is_file_modifying_tool};
9pub use types::{Diagnostic, DiagnosticSeverity, format_diagnostics};
10
11use std::collections::{HashMap, HashSet};
12use std::path::{Path, PathBuf};
13use std::sync::Mutex;
14
15use server::LspServer;
16
17pub struct LspManager {
23 servers: tokio::sync::Mutex<HashMap<String, LspServer>>,
24 broken: Mutex<HashSet<String>>,
25 workspace_root: PathBuf,
26}
27
28impl LspManager {
29 pub fn new(workspace_root: PathBuf) -> Self {
31 Self {
32 servers: tokio::sync::Mutex::new(HashMap::new()),
33 broken: Mutex::new(HashSet::new()),
34 workspace_root,
35 }
36 }
37
38 pub async fn notify_file_changed(&self, path: &Path) -> Vec<Diagnostic> {
46 let lang_id = match detect_language(path) {
47 Some(id) => id,
48 None => return Vec::new(),
49 };
50
51 {
53 let broken = self.broken.lock().expect("broken lock poisoned");
54 if broken.contains(lang_id) {
55 return Vec::new();
56 }
57 }
58
59 let content = match tokio::fs::read_to_string(path).await {
61 Ok(c) => c,
62 Err(e) => {
63 tracing::debug!(path = %path.display(), error = %e, "failed to read file for LSP");
64 return Vec::new();
65 }
66 };
67
68 let mut servers = self.servers.lock().await;
69
70 if !servers.contains_key(lang_id) {
72 let config = match find_server_config(lang_id) {
73 Some(c) => c,
74 None => return Vec::new(),
75 };
76 tracing::debug!(
77 lang = %lang_id,
78 workspace = %self.workspace_root.display(),
79 "spawning LSP server"
80 );
81 match LspServer::spawn(config, &self.workspace_root).await {
82 Ok(srv) => {
83 tracing::debug!(lang = %lang_id, "LSP server initialized");
84 servers.insert(lang_id.to_string(), srv);
85 }
86 Err(e) => {
87 tracing::warn!(lang = %lang_id, error = %e, "LSP server failed to start, marking broken");
88 self.broken
89 .lock()
90 .expect("broken lock poisoned")
91 .insert(lang_id.to_string());
92 return Vec::new();
93 }
94 }
95 }
96
97 let srv = servers.get_mut(lang_id).expect("server just inserted");
98
99 if let Err(e) = srv.notify_file_changed(path, &content).await {
101 tracing::debug!(error = %e, "failed to notify LSP server of file change");
102 return Vec::new();
103 }
104
105 srv.pull_diagnostics(path).await
107 }
108
109 pub async fn diagnostics(&self, path: &Path) -> Vec<Diagnostic> {
111 let lang_id = match detect_language(path) {
112 Some(id) => id,
113 None => return Vec::new(),
114 };
115
116 let servers = self.servers.lock().await;
117 match servers.get(lang_id) {
118 Some(srv) => srv.pull_diagnostics(path).await,
119 None => Vec::new(),
120 }
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use std::path::PathBuf;
128
129 #[test]
130 fn lsp_manager_new_creates_empty() {
131 let mgr = LspManager::new(PathBuf::from("/tmp/test"));
132 assert!(mgr.broken.lock().unwrap().is_empty());
133 }
134
135 #[tokio::test]
136 async fn notify_unsupported_language_returns_empty() {
137 let mgr = LspManager::new(PathBuf::from("/tmp"));
138 let diagnostics = mgr.notify_file_changed(Path::new("/tmp/README.md")).await;
139 assert!(diagnostics.is_empty());
140 }
141
142 #[tokio::test]
143 async fn diagnostics_without_server_returns_empty() {
144 let mgr = LspManager::new(PathBuf::from("/tmp"));
145 let diagnostics = mgr.diagnostics(Path::new("/tmp/test.rs")).await;
146 assert!(diagnostics.is_empty());
147 }
148
149 #[test]
150 fn broken_server_not_retried() {
151 let mgr = LspManager::new(PathBuf::from("/tmp"));
152 mgr.broken.lock().unwrap().insert("rust".to_string());
153 assert!(mgr.broken.lock().unwrap().contains("rust"));
155 }
156
157 #[tokio::test]
158 async fn notify_broken_language_returns_empty() {
159 let mgr = LspManager::new(PathBuf::from("/tmp"));
160 mgr.broken.lock().unwrap().insert("rust".to_string());
161 let diagnostics = mgr.notify_file_changed(Path::new("/tmp/test.rs")).await;
162 assert!(diagnostics.is_empty());
163 }
164
165 #[tokio::test]
166 async fn notify_nonexistent_file_returns_empty() {
167 let mgr = LspManager::new(PathBuf::from("/tmp"));
168 let diagnostics = mgr
169 .notify_file_changed(Path::new("/tmp/does_not_exist_12345.rs"))
170 .await;
171 assert!(diagnostics.is_empty());
172 }
173}