1pub mod client;
17pub mod discovery;
18pub mod tools;
19pub mod types;
20
21use async_trait::async_trait;
22use client::{LspClient, LspError};
23use discovery::ServerRegistry;
24use std::collections::HashMap;
25use std::path::{Path, PathBuf};
26use std::sync::Arc;
27use tokio::sync::Mutex;
28use tracing::{info, warn};
29use types::{CompletionItem, Diagnostic, Location, TextEdit, WorkspaceEdit};
30
31#[async_trait]
37pub trait LspBackend: Send + Sync {
38 async fn hover(
40 &self,
41 file: &Path,
42 line: u32,
43 character: u32,
44 ) -> Result<Option<String>, LspError>;
45
46 async fn definition(
48 &self,
49 file: &Path,
50 line: u32,
51 character: u32,
52 ) -> Result<Vec<Location>, LspError>;
53
54 async fn references(
56 &self,
57 file: &Path,
58 line: u32,
59 character: u32,
60 ) -> Result<Vec<Location>, LspError>;
61
62 async fn diagnostics(&self, file: &Path) -> Result<Vec<Diagnostic>, LspError>;
64
65 async fn completions(
67 &self,
68 file: &Path,
69 line: u32,
70 character: u32,
71 ) -> Result<Vec<CompletionItem>, LspError>;
72
73 async fn rename(
75 &self,
76 file: &Path,
77 line: u32,
78 character: u32,
79 new_name: &str,
80 ) -> Result<WorkspaceEdit, LspError>;
81
82 async fn format(&self, file: &Path) -> Result<Vec<TextEdit>, LspError>;
84}
85
86pub struct LspManager {
92 workspace: PathBuf,
93 registry: ServerRegistry,
94 clients: Mutex<HashMap<String, LspClient>>,
95}
96
97impl LspManager {
98 pub fn new(workspace: PathBuf) -> Self {
100 Self {
101 workspace,
102 registry: ServerRegistry::with_defaults(),
103 clients: Mutex::new(HashMap::new()),
104 }
105 }
106
107 pub fn with_registry(workspace: PathBuf, registry: ServerRegistry) -> Self {
109 Self {
110 workspace,
111 registry,
112 clients: Mutex::new(HashMap::new()),
113 }
114 }
115
116 async fn get_client_for_file(&self, file: &Path) -> Result<String, LspError> {
121 let language = discovery::ServerRegistry::detect_language(file).ok_or_else(|| {
122 let ext = file
123 .extension()
124 .and_then(|e| e.to_str())
125 .unwrap_or("unknown");
126 LspError::UnsupportedLanguage {
127 language: ext.to_string(),
128 }
129 })?;
130
131 let mut clients = self.clients.lock().await;
132
133 if clients.contains_key(&language) {
134 return Ok(language);
135 }
136
137 let config = self
138 .registry
139 .get(&language)
140 .ok_or_else(|| LspError::UnsupportedLanguage {
141 language: language.clone(),
142 })?;
143
144 info!(
145 language = %language,
146 command = %config.command,
147 "Starting language server"
148 );
149
150 match LspClient::start(&config.command, &config.args, &self.workspace).await {
151 Ok(client) => {
152 clients.insert(language.clone(), client);
153 Ok(language)
154 }
155 Err(e) => {
156 warn!(
157 language = %language,
158 error = %e,
159 "Failed to start language server"
160 );
161 Err(e)
162 }
163 }
164 }
165
166 fn extract_hover_text(hover: &types::HoverResult) -> String {
168 match &hover.contents {
169 types::HoverContents::Scalar(marked) => match marked {
170 types::MarkedString::String(s) => s.clone(),
171 types::MarkedString::LanguageString { language, value } => {
172 format!("```{}\n{}\n```", language, value)
173 }
174 },
175 types::HoverContents::Markup(markup) => markup.value.clone(),
176 types::HoverContents::Array(items) => items
177 .iter()
178 .map(|m| match m {
179 types::MarkedString::String(s) => s.clone(),
180 types::MarkedString::LanguageString { language, value } => {
181 format!("```{}\n{}\n```", language, value)
182 }
183 })
184 .collect::<Vec<_>>()
185 .join("\n\n"),
186 }
187 }
188}
189
190#[async_trait]
191impl LspBackend for LspManager {
192 async fn hover(
193 &self,
194 file: &Path,
195 line: u32,
196 character: u32,
197 ) -> Result<Option<String>, LspError> {
198 let language = self.get_client_for_file(file).await?;
199 let mut clients = self.clients.lock().await;
200 let client = clients
201 .get_mut(&language)
202 .ok_or_else(|| LspError::ServerNotRunning {
203 language: language.clone(),
204 })?;
205
206 let result = client.hover(file, line, character).await?;
207 Ok(result.map(|h| Self::extract_hover_text(&h)))
208 }
209
210 async fn definition(
211 &self,
212 file: &Path,
213 line: u32,
214 character: u32,
215 ) -> Result<Vec<Location>, LspError> {
216 let language = self.get_client_for_file(file).await?;
217 let mut clients = self.clients.lock().await;
218 let client = clients
219 .get_mut(&language)
220 .ok_or_else(|| LspError::ServerNotRunning {
221 language: language.clone(),
222 })?;
223 client.definition(file, line, character).await
224 }
225
226 async fn references(
227 &self,
228 file: &Path,
229 line: u32,
230 character: u32,
231 ) -> Result<Vec<Location>, LspError> {
232 let language = self.get_client_for_file(file).await?;
233 let mut clients = self.clients.lock().await;
234 let client = clients
235 .get_mut(&language)
236 .ok_or_else(|| LspError::ServerNotRunning {
237 language: language.clone(),
238 })?;
239 client.references(file, line, character).await
240 }
241
242 async fn diagnostics(&self, file: &Path) -> Result<Vec<Diagnostic>, LspError> {
243 let language = self.get_client_for_file(file).await?;
244 let clients = self.clients.lock().await;
245 let client = clients
246 .get(&language)
247 .ok_or_else(|| LspError::ServerNotRunning {
248 language: language.clone(),
249 })?;
250 client.diagnostics(file)
251 }
252
253 async fn completions(
254 &self,
255 file: &Path,
256 line: u32,
257 character: u32,
258 ) -> Result<Vec<CompletionItem>, LspError> {
259 let language = self.get_client_for_file(file).await?;
260 let mut clients = self.clients.lock().await;
261 let client = clients
262 .get_mut(&language)
263 .ok_or_else(|| LspError::ServerNotRunning {
264 language: language.clone(),
265 })?;
266 client.completions(file, line, character).await
267 }
268
269 async fn rename(
270 &self,
271 file: &Path,
272 line: u32,
273 character: u32,
274 new_name: &str,
275 ) -> Result<WorkspaceEdit, LspError> {
276 let language = self.get_client_for_file(file).await?;
277 let mut clients = self.clients.lock().await;
278 let client = clients
279 .get_mut(&language)
280 .ok_or_else(|| LspError::ServerNotRunning {
281 language: language.clone(),
282 })?;
283 client.rename(file, line, character, new_name).await
284 }
285
286 async fn format(&self, file: &Path) -> Result<Vec<TextEdit>, LspError> {
287 let language = self.get_client_for_file(file).await?;
288 let mut clients = self.clients.lock().await;
289 let client = clients
290 .get_mut(&language)
291 .ok_or_else(|| LspError::ServerNotRunning {
292 language: language.clone(),
293 })?;
294 client.format(file).await
295 }
296}
297
298pub fn create_lsp_tools(backend: Arc<dyn LspBackend>) -> Vec<Arc<dyn crate::registry::Tool>> {
302 vec![
303 Arc::new(tools::LspHoverTool::new(Arc::clone(&backend))),
304 Arc::new(tools::LspDefinitionTool::new(Arc::clone(&backend))),
305 Arc::new(tools::LspReferencesTool::new(Arc::clone(&backend))),
306 Arc::new(tools::LspDiagnosticsTool::new(Arc::clone(&backend))),
307 Arc::new(tools::LspCompletionsTool::new(Arc::clone(&backend))),
308 Arc::new(tools::LspRenameTool::new(Arc::clone(&backend))),
309 Arc::new(tools::LspFormatTool::new(Arc::clone(&backend))),
310 ]
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn test_lsp_manager_creation() {
319 let manager = LspManager::new(PathBuf::from("/tmp/workspace"));
320 assert_eq!(manager.workspace, PathBuf::from("/tmp/workspace"));
321 }
322
323 #[test]
324 fn test_extract_hover_text_scalar_string() {
325 let hover = types::HoverResult {
326 contents: types::HoverContents::Scalar(types::MarkedString::String(
327 "fn main()".to_string(),
328 )),
329 range: None,
330 };
331 assert_eq!(LspManager::extract_hover_text(&hover), "fn main()");
332 }
333
334 #[test]
335 fn test_extract_hover_text_scalar_language_string() {
336 let hover = types::HoverResult {
337 contents: types::HoverContents::Scalar(types::MarkedString::LanguageString {
338 language: "rust".to_string(),
339 value: "fn main()".to_string(),
340 }),
341 range: None,
342 };
343 assert_eq!(
344 LspManager::extract_hover_text(&hover),
345 "```rust\nfn main()\n```"
346 );
347 }
348
349 #[test]
350 fn test_extract_hover_text_markup() {
351 let hover = types::HoverResult {
352 contents: types::HoverContents::Markup(types::MarkupContent {
353 kind: "markdown".to_string(),
354 value: "# Hello\nWorld".to_string(),
355 }),
356 range: None,
357 };
358 assert_eq!(LspManager::extract_hover_text(&hover), "# Hello\nWorld");
359 }
360
361 #[test]
362 fn test_extract_hover_text_array() {
363 let hover = types::HoverResult {
364 contents: types::HoverContents::Array(vec![
365 types::MarkedString::String("Type: i32".to_string()),
366 types::MarkedString::LanguageString {
367 language: "rust".to_string(),
368 value: "let x: i32 = 42;".to_string(),
369 },
370 ]),
371 range: None,
372 };
373 let text = LspManager::extract_hover_text(&hover);
374 assert!(text.contains("Type: i32"));
375 assert!(text.contains("```rust\nlet x: i32 = 42;\n```"));
376 }
377
378 #[test]
379 fn test_create_lsp_tools_returns_seven() {
380 use crate::lsp::types::*;
381
382 struct DummyBackend;
383
384 #[async_trait]
385 impl LspBackend for DummyBackend {
386 async fn hover(&self, _: &Path, _: u32, _: u32) -> Result<Option<String>, LspError> {
387 Ok(None)
388 }
389 async fn definition(
390 &self,
391 _: &Path,
392 _: u32,
393 _: u32,
394 ) -> Result<Vec<Location>, LspError> {
395 Ok(vec![])
396 }
397 async fn references(
398 &self,
399 _: &Path,
400 _: u32,
401 _: u32,
402 ) -> Result<Vec<Location>, LspError> {
403 Ok(vec![])
404 }
405 async fn diagnostics(&self, _: &Path) -> Result<Vec<Diagnostic>, LspError> {
406 Ok(vec![])
407 }
408 async fn completions(
409 &self,
410 _: &Path,
411 _: u32,
412 _: u32,
413 ) -> Result<Vec<CompletionItem>, LspError> {
414 Ok(vec![])
415 }
416 async fn rename(
417 &self,
418 _: &Path,
419 _: u32,
420 _: u32,
421 _: &str,
422 ) -> Result<WorkspaceEdit, LspError> {
423 Ok(WorkspaceEdit { changes: None })
424 }
425 async fn format(&self, _: &Path) -> Result<Vec<TextEdit>, LspError> {
426 Ok(vec![])
427 }
428 }
429
430 let backend: Arc<dyn LspBackend> = Arc::new(DummyBackend);
431 let tools = create_lsp_tools(backend);
432 assert_eq!(tools.len(), 7);
433
434 let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
435 assert!(names.contains(&"lsp_hover"));
436 assert!(names.contains(&"lsp_definition"));
437 assert!(names.contains(&"lsp_references"));
438 assert!(names.contains(&"lsp_diagnostics"));
439 assert!(names.contains(&"lsp_completions"));
440 assert!(names.contains(&"lsp_rename"));
441 assert!(names.contains(&"lsp_format"));
442 }
443
444 #[test]
445 fn test_lsp_manager_with_custom_registry() {
446 let registry = ServerRegistry::with_defaults();
447 let manager = LspManager::with_registry(PathBuf::from("/tmp"), registry);
448 assert_eq!(manager.workspace, PathBuf::from("/tmp"));
449 }
450}