1use super::transport::LspTransport;
9use super::types::*;
10use anyhow::Result;
11use lsp_types::{
12 ClientCapabilities, CompletionContext, CompletionParams, CompletionTriggerKind,
13 DocumentSymbolParams, HoverParams, Position, TextDocumentIdentifier, TextDocumentItem,
14 TextDocumentPositionParams,
15};
16use std::collections::HashMap;
17use std::path::Path;
18use std::sync::Arc;
19use tokio::sync::RwLock;
20use tracing::{debug, info, warn};
21
22pub struct LspClient {
24 transport: LspTransport,
25 config: LspConfig,
26 server_capabilities: RwLock<Option<lsp_types::ServerCapabilities>>,
27 open_documents: RwLock<HashMap<String, i32>>,
29}
30
31impl LspClient {
32 pub async fn new(config: LspConfig) -> Result<Self> {
34 super::types::ensure_server_installed(&config).await?;
35
36 let transport =
37 LspTransport::spawn(&config.command, &config.args, config.timeout_ms).await?;
38
39 Ok(Self {
40 transport,
41 config,
42 server_capabilities: RwLock::new(None),
43 open_documents: RwLock::new(HashMap::new()),
44 })
45 }
46
47 pub async fn for_language(language: &str, root_uri: Option<String>) -> Result<Self> {
49 let mut config = get_language_server_config(language)
50 .ok_or_else(|| anyhow::anyhow!("Unknown language: {}", language))?;
51 config.root_uri = root_uri;
52 Self::new(config).await
53 }
54
55 pub async fn initialize(&self) -> Result<()> {
57 let root_uri = self.config.root_uri.clone();
58
59 let params = InitializeParams {
60 process_id: Some(std::process::id() as i64),
61 client_info: ClientInfo {
62 name: "codetether".to_string(),
63 version: env!("CARGO_PKG_VERSION").to_string(),
64 },
65 locale: None,
66 root_path: None,
67 root_uri: root_uri.clone(),
68 initialization_options: self.config.initialization_options.clone(),
69 capabilities: ClientCapabilities::default(),
70 trace: None,
71 workspace_folders: None,
72 };
73
74 let response = self
75 .transport
76 .request("initialize", Some(serde_json::to_value(params)?))
77 .await?;
78
79 if let Some(error) = response.error {
80 return Err(anyhow::anyhow!("LSP initialize error: {}", error.message));
81 }
82
83 if let Some(result) = response.result {
84 let init_result: InitializeResult = serde_json::from_value(result)?;
85 *self.server_capabilities.write().await = Some(init_result.capabilities);
86 info!(server_info = ?init_result.server_info, "LSP server initialized");
87 }
88
89 self.transport.notify("initialized", None).await?;
90 self.transport.set_initialized(true);
91
92 Ok(())
93 }
94
95 #[allow(dead_code)]
97 pub async fn shutdown(&self) -> Result<()> {
98 let response = self.transport.request("shutdown", None).await?;
99
100 if let Some(error) = response.error {
101 warn!("LSP shutdown error: {}", error.message);
102 }
103
104 self.transport.notify("exit", None).await?;
105 info!("LSP server shutdown complete");
106
107 Ok(())
108 }
109
110 pub async fn open_document(&self, path: &Path, content: &str) -> Result<()> {
112 let uri = path_to_uri(path);
113 let language_id = detect_language_from_path(path.to_string_lossy().as_ref())
114 .unwrap_or("plaintext")
115 .to_string();
116
117 let text_document = TextDocumentItem {
118 uri: parse_uri(&uri)?,
119 language_id,
120 version: 1,
121 text: content.to_string(),
122 };
123
124 let params = DidOpenTextDocumentParams { text_document };
125 self.transport
126 .notify("textDocument/didOpen", Some(serde_json::to_value(params)?))
127 .await?;
128
129 self.open_documents.write().await.insert(uri, 1);
130 debug!(path = %path.display(), "Opened document");
131
132 Ok(())
133 }
134
135 #[allow(dead_code)]
137 pub async fn close_document(&self, path: &Path) -> Result<()> {
138 let uri = path_to_uri(path);
139
140 let text_document = TextDocumentIdentifier {
141 uri: parse_uri(&uri)?,
142 };
143
144 let params = DidCloseTextDocumentParams { text_document };
145 self.transport
146 .notify("textDocument/didClose", Some(serde_json::to_value(params)?))
147 .await?;
148
149 self.open_documents.write().await.remove(&uri);
150 debug!(path = %path.display(), "Closed document");
151
152 Ok(())
153 }
154
155 pub async fn change_document(&self, path: &Path, content: &str) -> Result<()> {
157 let uri = path_to_uri(path);
158 let mut open_docs = self.open_documents.write().await;
159
160 let version = open_docs.entry(uri.clone()).or_insert(0);
161 *version += 1;
162
163 let text_document = VersionedTextDocumentIdentifier {
164 uri,
165 version: *version,
166 };
167
168 let content_changes = vec![super::types::TextDocumentContentChangeEvent {
169 range: None,
170 range_length: None,
171 text: content.to_string(),
172 }];
173
174 let params = DidChangeTextDocumentParams {
175 text_document,
176 content_changes,
177 };
178
179 self.transport
180 .notify(
181 "textDocument/didChange",
182 Some(serde_json::to_value(params)?),
183 )
184 .await?;
185
186 debug!(path = %path.display(), version = *version, "Changed document");
187
188 Ok(())
189 }
190
191 pub async fn go_to_definition(
193 &self,
194 path: &Path,
195 line: u32,
196 character: u32,
197 ) -> Result<LspActionResult> {
198 let uri = path_to_uri(path);
199 self.ensure_document_open(path).await?;
200
201 let params = serde_json::json!({
202 "textDocument": { "uri": uri },
203 "position": { "line": line.saturating_sub(1), "character": character.saturating_sub(1) },
204 });
205
206 let response = self
207 .transport
208 .request("textDocument/definition", Some(params))
209 .await?;
210
211 parse_location_response(response, "definition")
212 }
213
214 pub async fn find_references(
216 &self,
217 path: &Path,
218 line: u32,
219 character: u32,
220 include_declaration: bool,
221 ) -> Result<LspActionResult> {
222 let uri = path_to_uri(path);
223 self.ensure_document_open(path).await?;
224
225 let params = ReferenceParams {
226 text_document: TextDocumentIdentifier {
227 uri: parse_uri(&uri)?,
228 },
229 position: Position {
230 line: line.saturating_sub(1),
231 character: character.saturating_sub(1),
232 },
233 context: ReferenceContext {
234 include_declaration,
235 },
236 };
237
238 let response = self
239 .transport
240 .request(
241 "textDocument/references",
242 Some(serde_json::to_value(params)?),
243 )
244 .await?;
245
246 parse_location_response(response, "references")
247 }
248
249 pub async fn hover(&self, path: &Path, line: u32, character: u32) -> Result<LspActionResult> {
251 let uri = path_to_uri(path);
252 self.ensure_document_open(path).await?;
253
254 let params = HoverParams {
255 text_document_position_params: TextDocumentPositionParams {
256 text_document: TextDocumentIdentifier {
257 uri: parse_uri(&uri)?,
258 },
259 position: Position {
260 line: line.saturating_sub(1),
261 character: character.saturating_sub(1),
262 },
263 },
264 work_done_progress_params: Default::default(),
265 };
266
267 let response = self
268 .transport
269 .request("textDocument/hover", Some(serde_json::to_value(params)?))
270 .await?;
271
272 parse_hover_response(response)
273 }
274
275 pub async fn document_symbols(&self, path: &Path) -> Result<LspActionResult> {
277 let uri = path_to_uri(path);
278 self.ensure_document_open(path).await?;
279
280 let params = DocumentSymbolParams {
281 text_document: TextDocumentIdentifier {
282 uri: parse_uri(&uri)?,
283 },
284 work_done_progress_params: Default::default(),
285 partial_result_params: Default::default(),
286 };
287
288 let response = self
289 .transport
290 .request(
291 "textDocument/documentSymbol",
292 Some(serde_json::to_value(params)?),
293 )
294 .await?;
295
296 parse_document_symbols_response(response)
297 }
298
299 pub async fn workspace_symbols(&self, query: &str) -> Result<LspActionResult> {
301 let params = WorkspaceSymbolParams {
302 query: query.to_string(),
303 };
304
305 let response = self
306 .transport
307 .request("workspace/symbol", Some(serde_json::to_value(params)?))
308 .await?;
309
310 parse_workspace_symbols_response(response)
311 }
312
313 pub async fn go_to_implementation(
315 &self,
316 path: &Path,
317 line: u32,
318 character: u32,
319 ) -> Result<LspActionResult> {
320 let uri = path_to_uri(path);
321 self.ensure_document_open(path).await?;
322
323 let params = serde_json::json!({
324 "textDocument": { "uri": uri },
325 "position": { "line": line.saturating_sub(1), "character": character.saturating_sub(1) },
326 });
327
328 let response = self
329 .transport
330 .request("textDocument/implementation", Some(params))
331 .await?;
332
333 parse_location_response(response, "implementation")
334 }
335
336 pub async fn completion(
338 &self,
339 path: &Path,
340 line: u32,
341 character: u32,
342 ) -> Result<LspActionResult> {
343 let uri = path_to_uri(path);
344 self.ensure_document_open(path).await?;
345
346 let params = CompletionParams {
347 text_document_position: TextDocumentPositionParams {
348 text_document: TextDocumentIdentifier {
349 uri: parse_uri(&uri)?,
350 },
351 position: Position {
352 line: line.saturating_sub(1),
353 character: character.saturating_sub(1),
354 },
355 },
356 work_done_progress_params: Default::default(),
357 partial_result_params: Default::default(),
358 context: Some(CompletionContext {
359 trigger_kind: CompletionTriggerKind::INVOKED,
360 trigger_character: None,
361 }),
362 };
363
364 let response = self
365 .transport
366 .request(
367 "textDocument/completion",
368 Some(serde_json::to_value(params)?),
369 )
370 .await?;
371
372 parse_completion_response(response)
373 }
374
375 pub async fn diagnostics(&self, path: &Path) -> Result<LspActionResult> {
383 let uri = path_to_uri(path);
384
385 let disk_content = tokio::fs::read_to_string(path).await.unwrap_or_default();
389 let already_open = self.open_documents.read().await.contains_key(&uri);
390
391 let baseline_seq = self.transport.diagnostics_publish_seq();
392 self.transport.invalidate_diagnostics(&uri).await;
393
394 if already_open {
395 if let Err(e) = self.change_document(path, &disk_content).await {
396 debug!(path = %path.display(), error = %e, "didChange failed; falling back to cached snapshot");
397 }
398 } else if let Err(e) = self.open_document(path, &disk_content).await {
399 debug!(path = %path.display(), error = %e, "didOpen failed; falling back to cached snapshot");
400 }
401
402 let _ = self
405 .transport
406 .wait_for_publish_after(baseline_seq, std::time::Duration::from_millis(1500))
407 .await;
408
409 let snapshot = self.transport.diagnostics_snapshot().await;
410 let diagnostics = snapshot
411 .get(&uri)
412 .cloned()
413 .unwrap_or_default()
414 .into_iter()
415 .map(|diagnostic| DiagnosticInfo::from((uri.clone(), diagnostic)))
416 .collect();
417
418 Ok(LspActionResult::Diagnostics { diagnostics })
419 }
420
421 async fn ensure_document_open(&self, path: &Path) -> Result<()> {
423 let uri = path_to_uri(path);
424 if !self.open_documents.read().await.contains_key(&uri) {
425 let content = tokio::fs::read_to_string(path).await?;
426 self.open_document(path, &content).await?;
427 }
428 Ok(())
429 }
430
431 #[allow(dead_code)]
433 pub async fn capabilities(&self) -> Option<lsp_types::ServerCapabilities> {
434 self.server_capabilities.read().await.clone()
435 }
436
437 #[allow(dead_code)]
439 pub fn handles_file(&self, path: &Path) -> bool {
440 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
441 self.config.file_extensions.iter().any(|fe| fe == ext)
442 }
443
444 #[allow(dead_code)]
446 pub fn handles_language(&self, language: &str) -> bool {
447 let extensions = match language {
448 "rust" => &["rs"][..],
449 "typescript" => &["ts", "tsx"],
450 "javascript" => &["js", "jsx"],
451 "python" => &["py"],
452 "go" => &["go"],
453 "c" => &["c", "h"],
454 "cpp" => &["cpp", "cc", "cxx", "hpp", "h"],
455 _ => &[],
456 };
457
458 extensions
459 .iter()
460 .any(|ext| self.config.file_extensions.iter().any(|fe| fe == *ext))
461 }
462}
463
464fn path_to_uri(path: &Path) -> String {
466 let absolute = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
467 format!("file://{}", absolute.display())
468}
469
470fn parse_uri(uri_str: &str) -> Result<lsp_types::Uri> {
472 uri_str
473 .parse()
474 .map_err(|e| anyhow::anyhow!("Invalid URI: {e}"))
475}
476
477fn parse_location_response(response: JsonRpcResponse, _operation: &str) -> Result<LspActionResult> {
479 if let Some(error) = response.error {
480 return Ok(LspActionResult::Error {
481 message: error.message,
482 });
483 }
484
485 let Some(result) = response.result else {
486 return Ok(LspActionResult::Definition { locations: vec![] });
487 };
488
489 if let Ok(loc) = serde_json::from_value::<lsp_types::Location>(result.clone()) {
490 return Ok(LspActionResult::Definition {
491 locations: vec![LocationInfo::from(loc)],
492 });
493 }
494
495 if let Ok(locs) = serde_json::from_value::<Vec<lsp_types::Location>>(result.clone()) {
496 return Ok(LspActionResult::Definition {
497 locations: locs.into_iter().map(LocationInfo::from).collect(),
498 });
499 }
500
501 if let Ok(links) = serde_json::from_value::<Vec<lsp_types::LocationLink>>(result) {
502 return Ok(LspActionResult::Definition {
503 locations: links
504 .into_iter()
505 .map(|link| LocationInfo {
506 uri: link.target_uri.to_string(),
507 range: RangeInfo::from(link.target_selection_range),
508 })
509 .collect(),
510 });
511 }
512
513 Ok(LspActionResult::Definition { locations: vec![] })
514}
515
516fn parse_hover_response(response: JsonRpcResponse) -> Result<LspActionResult> {
518 if let Some(error) = response.error {
519 return Ok(LspActionResult::Error {
520 message: error.message,
521 });
522 }
523
524 let Some(result) = response.result else {
525 return Ok(LspActionResult::Hover {
526 contents: String::new(),
527 range: None,
528 });
529 };
530
531 if result.is_null() {
532 return Ok(LspActionResult::Hover {
533 contents: "No hover information available".to_string(),
534 range: None,
535 });
536 }
537
538 let hover: lsp_types::Hover = serde_json::from_value(result)?;
539
540 let contents = match hover.contents {
541 lsp_types::HoverContents::Scalar(markup) => match markup {
542 lsp_types::MarkedString::String(s) => s,
543 lsp_types::MarkedString::LanguageString(ls) => ls.value,
544 },
545 lsp_types::HoverContents::Array(markups) => markups
546 .into_iter()
547 .map(|m| match m {
548 lsp_types::MarkedString::String(s) => s,
549 lsp_types::MarkedString::LanguageString(ls) => ls.value,
550 })
551 .collect::<Vec<_>>()
552 .join("\n\n"),
553 lsp_types::HoverContents::Markup(markup) => markup.value,
554 };
555
556 Ok(LspActionResult::Hover {
557 contents,
558 range: hover.range.map(RangeInfo::from),
559 })
560}
561
562fn parse_document_symbols_response(response: JsonRpcResponse) -> Result<LspActionResult> {
564 if let Some(error) = response.error {
565 return Ok(LspActionResult::Error {
566 message: error.message,
567 });
568 }
569
570 let Some(result) = response.result else {
571 return Ok(LspActionResult::DocumentSymbols { symbols: vec![] });
572 };
573
574 if result.is_null() {
575 return Ok(LspActionResult::DocumentSymbols { symbols: vec![] });
576 }
577
578 if let Ok(symbols) = serde_json::from_value::<Vec<lsp_types::DocumentSymbol>>(result.clone()) {
579 return Ok(LspActionResult::DocumentSymbols {
580 symbols: symbols.into_iter().map(SymbolInfo::from).collect(),
581 });
582 }
583
584 if let Ok(symbols) = serde_json::from_value::<Vec<lsp_types::SymbolInformation>>(result) {
585 return Ok(LspActionResult::DocumentSymbols {
586 symbols: symbols.into_iter().map(SymbolInfo::from).collect(),
587 });
588 }
589
590 Ok(LspActionResult::DocumentSymbols { symbols: vec![] })
591}
592
593fn parse_workspace_symbols_response(response: JsonRpcResponse) -> Result<LspActionResult> {
595 if let Some(error) = response.error {
596 return Ok(LspActionResult::Error {
597 message: error.message,
598 });
599 }
600
601 let Some(result) = response.result else {
602 return Ok(LspActionResult::WorkspaceSymbols { symbols: vec![] });
603 };
604
605 if result.is_null() {
606 return Ok(LspActionResult::WorkspaceSymbols { symbols: vec![] });
607 }
608
609 if let Ok(symbols) = serde_json::from_value::<Vec<lsp_types::SymbolInformation>>(result.clone())
610 {
611 return Ok(LspActionResult::WorkspaceSymbols {
612 symbols: symbols.into_iter().map(SymbolInfo::from).collect(),
613 });
614 }
615
616 if let Ok(symbols) = serde_json::from_value::<Vec<lsp_types::WorkspaceSymbol>>(result) {
617 return Ok(LspActionResult::WorkspaceSymbols {
618 symbols: symbols
619 .into_iter()
620 .map(|s| {
621 let (uri, range) = match s.location {
622 lsp_types::OneOf::Left(loc) => {
623 (loc.uri.to_string(), Some(RangeInfo::from(loc.range)))
624 }
625 lsp_types::OneOf::Right(wl) => (wl.uri.to_string(), None),
626 };
627 SymbolInfo {
628 name: s.name,
629 kind: format!("{:?}", s.kind),
630 detail: None,
631 uri: Some(uri),
632 range,
633 container_name: s.container_name,
634 }
635 })
636 .collect(),
637 });
638 }
639
640 Ok(LspActionResult::WorkspaceSymbols { symbols: vec![] })
641}
642
643fn parse_completion_response(response: JsonRpcResponse) -> Result<LspActionResult> {
645 if let Some(error) = response.error {
646 return Ok(LspActionResult::Error {
647 message: error.message,
648 });
649 }
650
651 let Some(result) = response.result else {
652 return Ok(LspActionResult::Completion { items: vec![] });
653 };
654
655 if result.is_null() {
656 return Ok(LspActionResult::Completion { items: vec![] });
657 }
658
659 if let Ok(list) = serde_json::from_value::<lsp_types::CompletionList>(result.clone()) {
660 return Ok(LspActionResult::Completion {
661 items: list
662 .items
663 .into_iter()
664 .map(CompletionItemInfo::from)
665 .collect(),
666 });
667 }
668
669 if let Ok(items) = serde_json::from_value::<Vec<lsp_types::CompletionItem>>(result) {
670 return Ok(LspActionResult::Completion {
671 items: items.into_iter().map(CompletionItemInfo::from).collect(),
672 });
673 }
674
675 Ok(LspActionResult::Completion { items: vec![] })
676}
677
678pub struct LspManager {
680 clients: RwLock<HashMap<String, Arc<LspClient>>>,
681 linter_clients: RwLock<HashMap<String, Arc<LspClient>>>,
684 root_uri: Option<String>,
685 lsp_settings: Option<crate::config::LspSettings>,
687}
688
689impl LspManager {
690 pub fn new(root_uri: Option<String>) -> Self {
692 Self {
693 clients: RwLock::new(HashMap::new()),
694 linter_clients: RwLock::new(HashMap::new()),
695 root_uri,
696 lsp_settings: None,
697 }
698 }
699
700 pub fn with_config(root_uri: Option<String>, settings: crate::config::LspSettings) -> Self {
702 Self {
703 clients: RwLock::new(HashMap::new()),
704 linter_clients: RwLock::new(HashMap::new()),
705 root_uri,
706 lsp_settings: Some(settings),
707 }
708 }
709
710 pub async fn get_client(&self, language: &str) -> Result<Arc<LspClient>> {
712 {
713 let clients = self.clients.read().await;
714 if let Some(client) = clients.get(language) {
715 return Ok(Arc::clone(client));
716 }
717 }
718
719 let client = if let Some(settings) = &self.lsp_settings {
720 if let Some(entry) = settings.servers.get(language) {
721 let config = LspConfig::from_server_entry(entry, self.root_uri.clone());
722 LspClient::new(config).await?
723 } else {
724 LspClient::for_language(language, self.root_uri.clone()).await?
725 }
726 } else {
727 LspClient::for_language(language, self.root_uri.clone()).await?
728 };
729 client.initialize().await?;
730
731 let client = Arc::new(client);
732 self.clients
733 .write()
734 .await
735 .insert(language.to_string(), Arc::clone(&client));
736
737 Ok(client)
738 }
739
740 pub async fn get_client_for_file(&self, path: &Path) -> Result<Arc<LspClient>> {
742 let language = detect_language_from_path(path.to_string_lossy().as_ref())
743 .ok_or_else(|| anyhow::anyhow!("Unknown language for file: {}", path.display()))?;
744 self.get_client(language).await
745 }
746
747 #[allow(dead_code)]
749 pub async fn handles_file(&self, path: &Path) -> bool {
750 let clients = self.clients.read().await;
751 clients.values().any(|c| c.handles_file(path))
752 }
753
754 #[allow(dead_code)]
756 pub async fn capabilities_for(&self, language: &str) -> Option<lsp_types::ServerCapabilities> {
757 let clients = self.clients.read().await;
758 if let Some(client) = clients.get(language) {
759 client.capabilities().await
760 } else {
761 None
762 }
763 }
764
765 #[allow(dead_code)]
767 pub async fn close_document(&self, path: &Path) -> Result<()> {
768 if let Ok(client) = self.get_client_for_file(path).await {
769 client.close_document(path).await?;
770 }
771 Ok(())
772 }
773
774 #[allow(dead_code)]
776 pub async fn change_document(&self, path: &Path, content: &str) -> Result<()> {
777 if let Ok(client) = self.get_client_for_file(path).await {
778 client.change_document(path, content).await?;
779 }
780 Ok(())
781 }
782
783 #[allow(dead_code)]
785 pub async fn shutdown_all(&self) {
786 let clients = self.clients.read().await;
787 for (lang, client) in clients.iter() {
788 if let Err(e) = client.shutdown().await {
789 warn!("Failed to shutdown {} language server: {}", lang, e);
790 }
791 }
792 let linters = self.linter_clients.read().await;
793 for (name, client) in linters.iter() {
794 if let Err(e) = client.shutdown().await {
795 warn!("Failed to shutdown {} linter server: {}", name, e);
796 }
797 }
798 }
799
800 pub async fn get_linter_client(&self, name: &str) -> Result<Option<Arc<LspClient>>> {
803 {
805 let linters = self.linter_clients.read().await;
806 if let Some(client) = linters.get(name) {
807 return Ok(Some(Arc::clone(client)));
808 }
809 }
810
811 let lsp_config = if let Some(settings) = &self.lsp_settings {
813 if let Some(entry) = settings.linters.get(name) {
814 if !entry.enabled {
815 return Ok(None);
816 }
817 LspConfig::from_linter_entry(name, entry, self.root_uri.clone())
818 } else if !settings.disable_builtin_linters {
819 if let Some(mut cfg) = get_linter_server_config(name) {
821 cfg.root_uri = self.root_uri.clone();
822 Some(cfg)
823 } else {
824 None
825 }
826 } else {
827 None
828 }
829 } else {
830 if let Some(mut cfg) = get_linter_server_config(name) {
832 cfg.root_uri = self.root_uri.clone();
833 Some(cfg)
834 } else {
835 None
836 }
837 };
838
839 let Some(config) = lsp_config else {
840 return Ok(None);
841 };
842
843 let client = match LspClient::new(config).await {
845 Ok(c) => c,
846 Err(e) => {
847 debug!(linter = name, error = %e, "Linter server not available");
848 return Ok(None);
849 }
850 };
851 if let Err(e) = client.initialize().await {
852 warn!(linter = name, error = %e, "Linter server failed to initialize");
853 return Ok(None);
854 }
855
856 let client = Arc::new(client);
857 self.linter_clients
858 .write()
859 .await
860 .insert(name.to_string(), Arc::clone(&client));
861 info!(linter = name, "Linter server started");
862 Ok(Some(client))
863 }
864
865 pub async fn linter_diagnostics(&self, path: &Path) -> Vec<DiagnosticInfo> {
868 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
869
870 let linter_names: Vec<String> = if let Some(settings) = &self.lsp_settings {
872 settings
873 .linters
874 .iter()
875 .filter(|(_, entry)| {
876 entry.enabled
877 && (entry.file_extensions.iter().any(|e| e == ext)
878 || entry.file_extensions.is_empty())
879 })
880 .map(|(name, _)| name.clone())
881 .collect()
882 } else {
883 let mut names = Vec::new();
885 for candidate in &["eslint", "biome", "ruff", "stylelint"] {
886 if linter_extensions(candidate).contains(&ext) {
887 names.push((*candidate).to_string());
888 }
889 }
890 names
891 };
892
893 let mut all_diagnostics = Vec::new();
894 for name in &linter_names {
895 match self.get_linter_client(name).await {
896 Ok(Some(client)) => match client.diagnostics(path).await {
897 Ok(LspActionResult::Diagnostics { diagnostics }) => {
898 all_diagnostics.extend(diagnostics);
899 }
900 Ok(_) => {}
901 Err(e) => {
902 debug!(linter = %name, error = %e, "Linter diagnostics failed");
903 }
904 },
905 Ok(None) => {}
906 Err(e) => {
907 debug!(linter = %name, error = %e, "Failed to get linter client");
908 }
909 }
910 }
911 all_diagnostics
912 }
913}
914
915impl Default for LspManager {
916 fn default() -> Self {
917 Self::new(None)
918 }
919}