1use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::Duration;
11
12use oxi_lsp::{LspClient, LspClientConfig, LspError};
13use parking_lot::RwLock;
14use tracing::info;
15
16#[derive(Debug, Clone)]
18pub struct LspServerConfig {
19 pub name: String,
21 pub command: String,
23 pub args: Vec<String>,
25 pub extensions: Vec<String>,
28}
29
30impl LspServerConfig {
31 pub fn new(
33 name: impl Into<String>,
34 command: impl Into<String>,
35 args: Vec<String>,
36 extension: impl Into<String>,
37 ) -> Self {
38 Self {
39 name: name.into(),
40 command: command.into(),
41 args,
42 extensions: vec![extension.into()],
43 }
44 }
45
46 pub fn multi(
48 name: impl Into<String>,
49 command: impl Into<String>,
50 args: Vec<String>,
51 extensions: Vec<String>,
52 ) -> Self {
53 Self {
54 name: name.into(),
55 command: command.into(),
56 args,
57 extensions,
58 }
59 }
60
61 fn matches(&self, path: &Path) -> bool {
62 path.extension()
63 .and_then(|e| e.to_str())
64 .map(|ext| self.extensions.iter().any(|x| x == ext))
65 .unwrap_or(false)
66 }
67}
68
69pub fn default_servers() -> Vec<LspServerConfig> {
75 let mut out = Vec::new();
76 if which("rust-analyzer").is_some() {
77 out.push(LspServerConfig::new(
78 "rust-analyzer",
79 "rust-analyzer",
80 vec![],
81 "rs",
82 ));
83 }
84 out
85}
86
87fn which(bin: &str) -> Option<PathBuf> {
88 std::env::var_os("PATH").and_then(|paths| {
89 std::env::split_paths(&paths).find_map(|dir| {
90 let candidate = dir.join(bin);
91 if candidate.is_file() {
92 Some(candidate)
93 } else {
94 None
95 }
96 })
97 })
98}
99
100pub struct LspManager {
105 servers: Vec<LspServerConfig>,
107 clients: Arc<RwLock<HashMap<String, Arc<LspClient>>>>,
110 workspace_root: PathBuf,
112}
113
114impl std::fmt::Debug for LspManager {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 f.debug_struct("LspManager")
117 .field("servers", &self.servers)
118 .field("workspace_root", &self.workspace_root)
119 .field(
120 "active_clients",
121 &self.clients.read().keys().collect::<Vec<_>>(),
122 )
123 .finish()
124 }
125}
126
127impl LspManager {
128 pub fn new(servers: Vec<LspServerConfig>, workspace_root: PathBuf) -> Self {
130 Self {
131 servers,
132 clients: Arc::new(RwLock::new(HashMap::new())),
133 workspace_root,
134 }
135 }
136
137 pub fn with_defaults(workspace_root: PathBuf) -> Self {
140 Self::new(default_servers(), workspace_root)
141 }
142
143 pub fn server_count(&self) -> usize {
146 self.servers.len()
147 }
148
149 pub fn handles_path(&self, path: &Path) -> bool {
151 self.servers.iter().any(|s| s.matches(path))
152 }
153
154 pub fn workspace_root(&self) -> &Path {
156 &self.workspace_root
157 }
158
159 fn server_for_path(&self, path: &Path) -> Option<&LspServerConfig> {
161 self.servers.iter().find(|s| s.matches(path))
162 }
163
164 pub async fn client_for_path(&self, path: &Path) -> Result<Option<Arc<LspClient>>, LspError> {
170 let Some(config) = self.server_for_path(path) else {
171 return Ok(None);
172 };
173 let name = config.name.clone();
174
175 if let Some(client) = self.clients.read().get(&name).cloned() {
177 return Ok(Some(client));
178 }
179
180 let lsp_config = LspClientConfig::new(
182 config.name.clone(),
183 config.command.clone(),
184 config.args.clone(),
185 );
186 let client = LspClient::start(lsp_config, self.workspace_root.clone()).await?;
187 info!(server = %name, "LSP server started");
188 self.clients.write().insert(name, client.clone());
189 Ok(Some(client))
190 }
191
192 pub fn running_client(&self, name: &str) -> Option<Arc<LspClient>> {
194 self.clients.read().get(name).cloned()
195 }
196
197 pub fn live_clients(&self) -> Vec<(String, Arc<LspClient>)> {
199 self.clients
200 .read()
201 .iter()
202 .map(|(k, v)| (k.clone(), v.clone()))
203 .collect()
204 }
205
206 pub async fn shutdown_all(&self) {
208 let clients: Vec<(String, Arc<LspClient>)> = self.clients.write().drain().collect();
209 for (name, client) in clients {
210 if let Ok(mut exclusive) = Arc::try_unwrap(client) {
217 let _ = exclusive.shutdown().await;
218 tracing::debug!(server = %name, "LSP server shut down");
219 } else {
220 tracing::debug!(server = %name, "LSP server has external refs; skipping graceful shutdown");
221 }
222 }
223 }
224
225 pub const DEFAULT_DRAIN_TIMEOUT: Duration = Duration::from_millis(50);
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn server_config_matches_by_extension() {
237 let cfg = LspServerConfig::new("rust-analyzer", "rust-analyzer", vec![], "rs");
238 assert!(cfg.matches(Path::new("src/main.rs")));
239 assert!(!cfg.matches(Path::new("src/main.ts")));
240 assert!(!cfg.matches(Path::new("README")));
241 }
242
243 #[test]
244 fn manager_handles_path() {
245 let mgr = LspManager::new(
246 vec![LspServerConfig::new(
247 "rust-analyzer",
248 "rust-analyzer",
249 vec![],
250 "rs",
251 )],
252 PathBuf::from("."),
253 );
254 assert!(mgr.handles_path(Path::new("foo.rs")));
255 assert!(!mgr.handles_path(Path::new("foo.ts")));
256 }
257
258 #[test]
259 fn manager_no_servers_returns_zero() {
260 let mgr = LspManager::new(vec![], PathBuf::from("."));
261 assert_eq!(mgr.server_count(), 0);
262 assert!(!mgr.handles_path(Path::new("anything.rs")));
263 }
264
265 #[test]
266 fn manager_server_for_path_returns_owner() {
267 let mgr = LspManager::new(
268 vec![
269 LspServerConfig::new("rust-analyzer", "rust-analyzer", vec![], "rs"),
270 LspServerConfig::new("tsserver", "typescript-language-server", vec![], "ts"),
271 ],
272 PathBuf::from("."),
273 );
274 let p = Path::new("foo.ts");
275 let owner = mgr.server_for_path(p).unwrap();
276 assert_eq!(owner.name, "tsserver");
277 }
278}