1use anyhow::{Result, anyhow};
7use lsp_types::{
8 ClientCapabilities, Diagnostic, DiagnosticSeverity, GotoDefinitionParams,
9 Hover, HoverContents, HoverParams, InitializeParams,
10 InitializedParams, Location, MarkupKind, Position, ReferenceContext,
11 ReferenceParams, TextDocumentClientCapabilities, TextDocumentIdentifier,
12 TextDocumentItem, TextDocumentPositionParams, Url, WorkspaceClientCapabilities,
13 WorkspaceFolder,
14};
15use std::collections::HashMap;
16use std::path::PathBuf;
17use std::sync::Arc;
18use tokio::sync::Mutex;
19use tokio::time::{timeout, Duration};
20
21use super::constants::{PROCESS_STARTUP_TIMEOUT, SERVER_INIT_TIMEOUT};
22use super::progress::LspProgressCallback;
23use super::transport::LspTransport;
24use super::types::LspServerConfig;
25
26#[derive(Debug, Clone)]
28pub struct HoverResult {
29 pub signature: String,
31 pub documentation: Option<String>,
33}
34
35impl HoverResult {
36 pub fn new(signature: impl Into<String>) -> Self {
38 Self {
39 signature: signature.into(),
40 documentation: None,
41 }
42 }
43
44 pub fn with_documentation(mut self, doc: impl Into<String>) -> Self {
46 self.documentation = Some(doc.into());
47 self
48 }
49}
50
51pub struct LspClient {
56 transport: Arc<Mutex<Option<LspTransport>>>,
58 language: String,
60 server_name: String,
62 project_root: PathBuf,
64 open_files: Arc<Mutex<HashMap<Url, String>>>,
66 diagnostics_cache: Arc<Mutex<HashMap<Url, Vec<Diagnostic>>>>,
68 capabilities: Arc<Mutex<Option<lsp_types::ServerCapabilities>>>,
70}
71
72impl LspClient {
73 pub fn new(language: impl Into<String>, server_name: impl Into<String>, project_root: PathBuf) -> Self {
75 Self {
76 transport: Arc::new(Mutex::new(None)),
77 language: language.into(),
78 server_name: server_name.into(),
79 project_root,
80 open_files: Arc::new(Mutex::new(HashMap::new())),
81 diagnostics_cache: Arc::new(Mutex::new(HashMap::new())),
82 capabilities: Arc::new(Mutex::new(None)),
83 }
84 }
85
86 pub fn from_config(config: &LspServerConfig, project_root: PathBuf) -> Self {
88 Self::new(config.language.clone(), config.command.clone(), project_root)
89 }
90
91 pub async fn spawn(&self, config: &LspServerConfig) -> Result<()> {
93 log::info!("LSP spawn: starting '{}'...", self.server_name);
94 crate::debug::debug_log().log("lsp", &format!("spawn: starting '{}'...", self.server_name));
95
96 let transport = LspTransport::spawn(&config.command, &config.command, &config.args).await?;
98 log::info!("LSP spawn: '{}' process started", self.server_name);
99 crate::debug::debug_log().log("lsp", &format!("spawn: '{}' process started", self.server_name));
100
101 {
102 let mut transport_guard = self.transport.lock().await;
103 *transport_guard = Some(transport);
104 }
105
106 log::info!("LSP spawn: initializing '{}'...", self.server_name);
108 crate::debug::debug_log().log("lsp", &format!("spawn: initializing '{}'...", self.server_name));
109 self.initialize().await?;
110 log::info!("LSP spawn: '{}' initialized successfully", self.server_name);
111 crate::debug::debug_log().log("lsp", &format!("spawn: '{}' initialized OK", self.server_name));
112
113 log::info!("LSP client '{}' spawned and initialized successfully", self.server_name);
114 Ok(())
115 }
116
117 pub async fn spawn_async(
138 &self,
139 config: &LspServerConfig,
140 progress_callback: Arc<dyn LspProgressCallback>,
141 ) -> Result<()> {
142 log::info!("LSP spawn_async: starting '{}'...", self.server_name);
143
144 progress_callback.on_progress(0.1, "Starting process...");
146
147 let transport_result = timeout(PROCESS_STARTUP_TIMEOUT, async {
148 LspTransport::spawn(&config.command, &config.command, &config.args).await
149 })
150 .await;
151
152 let transport = transport_result.map_err(|_| {
153 let error_msg = format!(
154 "LSP process startup timeout ({}s).\n\
155 Possible causes:\n\
156 - Binary '{}' not found in PATH\n\
157 - Permission denied\n\
158 - Process hangs immediately",
159 PROCESS_STARTUP_TIMEOUT.as_secs(),
160 config.command
161 );
162 progress_callback.on_error(&error_msg);
163 anyhow!(error_msg)
164 })?;
165
166 let transport = transport.map_err(|e| {
167 let error_msg = format!("Failed to start LSP process '{}': {}", config.command, e);
168 progress_callback.on_error(&error_msg);
169 anyhow!(error_msg)
170 })?;
171
172 log::info!("LSP spawn_async: '{}' process started", self.server_name);
173 progress_callback.on_progress(0.3, "Process started, initializing server...");
174
175 {
177 let mut transport_guard = self.transport.lock().await;
178 *transport_guard = Some(transport);
179 }
180
181 progress_callback.on_progress(0.5, "Loading workspace...");
183
184 let init_result = timeout(SERVER_INIT_TIMEOUT, async {
185 self.initialize().await
186 })
187 .await;
188
189 match init_result {
190 Ok(result) => {
191 result?; log::info!("LSP spawn_async: '{}' initialized successfully", self.server_name);
193 progress_callback.on_progress(1.0, "Ready");
194 progress_callback.on_complete();
195 Ok(())
196 }
197 Err(_) => {
198 log::info!(
201 "LSP '{}' initialization timeout after {}s, continuing in background...",
202 self.server_name,
203 SERVER_INIT_TIMEOUT.as_secs()
204 );
205
206 progress_callback.on_progress(0.7, "Background init...");
209
210 let server_name = self.server_name.clone();
212 let transport = self.transport.clone();
213 let callback = progress_callback.clone();
214
215 tokio::spawn(async move {
216 tokio::time::sleep(Duration::from_secs(30)).await;
218
219 let transport_guard = transport.lock().await;
221 if transport_guard.is_some() {
222 log::info!("LSP '{}' background initialization completed", server_name);
223 callback.on_progress(1.0, "Ready");
224 callback.on_complete();
225 } else {
226 log::warn!("LSP '{}' process died during background init", server_name);
227 callback.on_error("Process died");
228 }
229 });
230
231 Ok(())
234 }
235 }
236 }
237
238 #[allow(deprecated)] pub async fn initialize(&self) -> Result<()> {
241 let transport = self.transport.lock().await;
242 let transport = transport.as_ref().ok_or_else(|| anyhow!("Transport not initialized"))?;
243
244 let root_uri = Url::from_file_path(&self.project_root)
245 .map_err(|_| anyhow!("Invalid project root path: {:?}", self.project_root))?;
246
247 let params = InitializeParams {
248 process_id: Some(std::process::id()),
249 root_path: None,
250 root_uri: Some(root_uri.clone()),
251 initialization_options: None,
252 capabilities: ClientCapabilities {
253 text_document: Some(TextDocumentClientCapabilities {
254 hover: Some(lsp_types::HoverClientCapabilities {
255 dynamic_registration: Some(false),
256 content_format: Some(vec![MarkupKind::Markdown, MarkupKind::PlainText]),
257 }),
258 definition: Some(lsp_types::GotoCapability {
259 dynamic_registration: Some(false),
260 link_support: Some(true),
261 }),
262 references: Some(lsp_types::ReferenceClientCapabilities {
263 dynamic_registration: Some(false),
264 }),
265 publish_diagnostics: Some(lsp_types::PublishDiagnosticsClientCapabilities {
266 related_information: Some(true),
267 tag_support: Some(lsp_types::TagSupport {
268 value_set: vec![lsp_types::DiagnosticTag::UNNECESSARY, lsp_types::DiagnosticTag::DEPRECATED],
269 }),
270 version_support: Some(false),
271 code_description_support: Some(true),
272 data_support: Some(false),
273 }),
274 ..Default::default()
275 }),
276 workspace: Some(WorkspaceClientCapabilities {
277 workspace_folders: Some(true),
278 ..Default::default()
279 }),
280 ..Default::default()
281 },
282 trace: Some(lsp_types::TraceValue::Off),
283 workspace_folders: Some(vec![WorkspaceFolder {
284 uri: root_uri,
285 name: self.project_root
286 .file_name()
287 .map(|n| n.to_string_lossy().to_string())
288 .unwrap_or_else(|| "workspace".to_string()),
289 }]),
290 client_info: Some(lsp_types::ClientInfo {
291 name: "matrixcode".to_string(),
292 version: Some(env!("CARGO_PKG_VERSION").to_string()),
293 }),
294 locale: None,
295 work_done_progress_params: Default::default(),
296 };
297
298 let result = transport
299 .send_request("initialize", serde_json::to_value(params)?)
300 .await?;
301
302 if let Some(capabilities) = result.get("capabilities") {
304 let caps: lsp_types::ServerCapabilities = serde_json::from_value(capabilities.clone())?;
305 let mut caps_guard = self.capabilities.lock().await;
306 *caps_guard = Some(caps);
307 }
308
309 self.initialized().await?;
311
312 Ok(())
313 }
314
315 pub async fn initialized(&self) -> Result<()> {
317 let transport = self.transport.lock().await;
318 let transport = transport.as_ref().ok_or_else(|| anyhow!("Transport not initialized"))?;
319
320 let params = InitializedParams {};
321 transport
322 .send_notification("initialized", serde_json::to_value(params)?)
323 .await?;
324
325 Ok(())
326 }
327
328 pub async fn open_file(&self, uri: &Url, content: &str) -> Result<()> {
330 let transport = self.transport.lock().await;
331 let transport = transport.as_ref().ok_or_else(|| anyhow!("Transport not initialized"))?;
332
333 let language_id = self.language.clone();
334 let version = 1;
335
336 let text_document = TextDocumentItem {
337 uri: uri.clone(),
338 language_id,
339 version,
340 text: content.to_string(),
341 };
342
343 let params = lsp_types::DidOpenTextDocumentParams { text_document };
344 transport
345 .send_notification("textDocument/didOpen", serde_json::to_value(params)?)
346 .await?;
347
348 let mut open_files = self.open_files.lock().await;
350 open_files.insert(uri.clone(), content.to_string());
351
352 log::debug!("Opened file in LSP: {}", uri);
353 Ok(())
354 }
355
356 pub async fn hover(&self, uri: &Url, position: Position) -> Result<Option<HoverResult>> {
358 let transport = self.transport.lock().await;
359 let transport = transport.as_ref().ok_or_else(|| anyhow!("Transport not initialized"))?;
360
361 let text_document = TextDocumentIdentifier { uri: uri.clone() };
362 let params = HoverParams {
363 text_document_position_params: TextDocumentPositionParams {
364 text_document,
365 position,
366 },
367 work_done_progress_params: Default::default(),
368 };
369
370 let result = transport
371 .send_request("textDocument/hover", serde_json::to_value(params)?)
372 .await?;
373
374 if result.is_null() {
375 return Ok(None);
376 }
377
378 let hover: Hover = serde_json::from_value(result)?;
379 Ok(Some(Self::parse_hover(hover)))
380 }
381
382 fn parse_hover(hover: Hover) -> HoverResult {
384 let (signature, documentation) = match hover.contents {
385 HoverContents::Scalar(scalar) => {
386 let content = match scalar {
387 lsp_types::MarkedString::String(s) => s,
388 lsp_types::MarkedString::LanguageString(ls) => {
389 format!("```{}\n{}\n```", ls.language, ls.value)
390 }
391 };
392 (content, None)
393 }
394 HoverContents::Array(arr) => {
395 let parts: Vec<String> = arr
396 .into_iter()
397 .map(|ms| match ms {
398 lsp_types::MarkedString::String(s) => s,
399 lsp_types::MarkedString::LanguageString(ls) => {
400 format!("```{}\n{}\n```", ls.language, ls.value)
401 }
402 })
403 .collect();
404 let signature = parts.first().cloned().unwrap_or_default();
405 let documentation = if parts.len() > 1 {
406 Some(parts[1..].join("\n\n"))
407 } else {
408 None
409 };
410 (signature, documentation)
411 }
412 HoverContents::Markup(markup) => {
413 let content = markup.value;
414 if content.contains("\n\n") {
416 let parts: Vec<&str> = content.splitn(2, "\n\n").collect();
417 (parts[0].to_string(), Some(parts[1].to_string()))
418 } else {
419 (content, None)
420 }
421 }
422 };
423
424 HoverResult { signature, documentation }
425 }
426
427 pub async fn definition(&self, uri: &Url, position: Position) -> Result<Vec<Location>> {
429 let transport = self.transport.lock().await;
430 let transport = transport.as_ref().ok_or_else(|| anyhow!("Transport not initialized"))?;
431
432 let text_document = TextDocumentIdentifier { uri: uri.clone() };
433 let params = GotoDefinitionParams {
434 text_document_position_params: TextDocumentPositionParams {
435 text_document,
436 position,
437 },
438 work_done_progress_params: Default::default(),
439 partial_result_params: Default::default(),
440 };
441
442 let result = transport
443 .send_request("textDocument/definition", serde_json::to_value(params)?)
444 .await?;
445
446 if result.is_null() {
447 return Ok(Vec::new());
448 }
449
450 let locations = Self::parse_definition_response(result)?;
451 Ok(locations)
452 }
453
454 fn parse_definition_response(result: serde_json::Value) -> Result<Vec<Location>> {
456 if let Ok(links) = serde_json::from_value::<Vec<lsp_types::LocationLink>>(result.clone()) {
458 return Ok(links
459 .into_iter()
460 .map(|link| Location {
461 uri: link.target_uri,
462 range: link.target_selection_range,
463 })
464 .collect());
465 }
466
467 if let Ok(locations) = serde_json::from_value::<Vec<Location>>(result.clone()) {
469 return Ok(locations);
470 }
471
472 if let Ok(location) = serde_json::from_value::<Location>(result.clone()) {
474 return Ok(vec![location]);
475 }
476
477 if let Ok(link) = serde_json::from_value::<lsp_types::LocationLink>(result) {
479 return Ok(vec![Location {
480 uri: link.target_uri,
481 range: link.target_selection_range,
482 }]);
483 }
484
485 Ok(Vec::new())
486 }
487
488 pub async fn references(&self, uri: &Url, position: Position, include_declaration: bool) -> Result<Vec<Location>> {
490 let transport = self.transport.lock().await;
491 let transport = transport.as_ref().ok_or_else(|| anyhow!("Transport not initialized"))?;
492
493 let text_document = TextDocumentIdentifier { uri: uri.clone() };
494 let params = ReferenceParams {
495 text_document_position: TextDocumentPositionParams {
496 text_document,
497 position,
498 },
499 work_done_progress_params: Default::default(),
500 partial_result_params: Default::default(),
501 context: ReferenceContext {
502 include_declaration,
503 },
504 };
505
506 let result = transport
507 .send_request("textDocument/references", serde_json::to_value(params)?)
508 .await?;
509
510 if result.is_null() {
511 return Ok(Vec::new());
512 }
513
514 let locations: Vec<Location> = serde_json::from_value(result)?;
515 Ok(locations)
516 }
517
518 pub async fn diagnostics(&self, uri: &Url) -> Result<Vec<Diagnostic>> {
520 let cache = self.diagnostics_cache.lock().await;
521 Ok(cache.get(uri).cloned().unwrap_or_default())
522 }
523
524 pub async fn update_diagnostics(&self, uri: Url, diagnostics: Vec<Diagnostic>) {
526 let mut cache = self.diagnostics_cache.lock().await;
527 cache.insert(uri, diagnostics);
528 }
529
530 pub async fn shutdown(&self) -> Result<()> {
532 let mut transport_guard = self.transport.lock().await;
533
534 if let Some(transport) = transport_guard.take() {
535 transport
537 .send_request("shutdown", serde_json::Value::Null)
538 .await?;
539
540 transport
542 .send_notification("exit", serde_json::Value::Null)
543 .await?;
544
545 transport.close().await?;
547
548 log::info!("LSP client '{}' shutdown successfully", self.server_name);
549 }
550
551 let mut open_files = self.open_files.lock().await;
553 open_files.clear();
554 let mut diagnostics_cache = self.diagnostics_cache.lock().await;
555 diagnostics_cache.clear();
556
557 Ok(())
558 }
559
560 pub async fn is_connected(&self) -> bool {
562 let transport = self.transport.lock().await;
563 transport.is_some()
564 }
565
566 pub fn language(&self) -> &str {
568 &self.language
569 }
570
571 pub fn server_name(&self) -> &str {
573 &self.server_name
574 }
575
576 pub fn project_root(&self) -> &PathBuf {
578 &self.project_root
579 }
580
581 pub async fn capabilities(&self) -> Option<lsp_types::ServerCapabilities> {
583 let caps = self.capabilities.lock().await;
584 caps.clone()
585 }
586}
587
588pub fn format_location(location: &Location) -> String {
594 let path = location.uri.to_file_path();
595 let path_str = path
596 .map(|p| p.to_string_lossy().to_string())
597 .unwrap_or_else(|_| location.uri.to_string());
598
599 let range = &location.range;
600 let start = &range.start;
601 format!(
602 "{}:{}:{}",
603 path_str,
604 start.line + 1, start.character + 1
606 )
607}
608
609pub fn format_diagnostic(diagnostic: &Diagnostic) -> String {
611 let severity = diagnostic.severity
612 .map(|s| format_severity(s))
613 .unwrap_or_else(|| "error".to_string());
614
615 let message = &diagnostic.message;
616
617 let location = diagnostic.related_information
618 .as_ref()
619 .and_then(|info| info.first())
620 .map(|info| format!(" at {}:{}", info.location.uri, info.location.range.start.line + 1))
621 .unwrap_or_default();
622
623 let code = diagnostic.code
624 .as_ref()
625 .map(|c| format!("[{}] ", match c {
626 lsp_types::NumberOrString::Number(n) => n.to_string(),
627 lsp_types::NumberOrString::String(s) => s.clone(),
628 }))
629 .unwrap_or_default();
630
631 format!("{}{}: {}{}", severity, code, message, location)
632}
633
634fn format_severity(severity: DiagnosticSeverity) -> String {
636 match severity {
637 DiagnosticSeverity::ERROR => "error".to_string(),
638 DiagnosticSeverity::WARNING => "warning".to_string(),
639 DiagnosticSeverity::INFORMATION => "info".to_string(),
640 DiagnosticSeverity::HINT => "hint".to_string(),
641 _ => "unknown".to_string(),
642 }
643}
644
645pub fn format_hover_result(hover: &HoverResult) -> String {
647 if let Some(doc) = &hover.documentation {
648 format!("{}\n\n{}", hover.signature, doc)
649 } else {
650 hover.signature.clone()
651 }
652}
653
654pub fn path_to_uri(path: &PathBuf) -> Result<Url> {
656 #[cfg(target_os = "windows")]
658 {
659 let path_str = path.to_string_lossy();
660
661 if path_str.starts_with('/') && path_str.chars().nth(1).map(|c| c.is_ascii_lowercase()).unwrap_or(false) {
663 let drive_letter = path_str.chars().nth(1).unwrap();
665 let rest = &path_str[2..]; let windows_path = format!("{}:{}", drive_letter.to_ascii_uppercase(), rest);
667 let windows_pathbuf = PathBuf::from(windows_path);
668
669 Url::from_file_path(&windows_pathbuf)
670 .map_err(|_| anyhow!("Invalid file path: {:?}", path))
671 } else {
672 Url::from_file_path(path)
673 .map_err(|_| anyhow!("Invalid file path: {:?}", path))
674 }
675 }
676
677 #[cfg(not(target_os = "windows"))]
678 {
679 Url::from_file_path(path)
680 .map_err(|_| anyhow!("Invalid file path: {:?}", path))
681 }
682}
683
684#[cfg(test)]
685mod tests {
686 use super::*;
687
688 #[test]
689 fn test_hover_result_new() {
690 let result = HoverResult::new("fn foo() -> i32");
691 assert_eq!(result.signature, "fn foo() -> i32");
692 assert!(result.documentation.is_none());
693 }
694
695 #[test]
696 fn test_hover_result_with_documentation() {
697 let result = HoverResult::new("fn foo() -> i32")
698 .with_documentation("This is a test function");
699 assert_eq!(result.signature, "fn foo() -> i32");
700 assert_eq!(result.documentation, Some("This is a test function".to_string()));
701 }
702
703 #[test]
704 fn test_format_hover_result() {
705 let hover = HoverResult::new("fn foo() -> i32")
706 .with_documentation("Docs");
707 let formatted = format_hover_result(&hover);
708 assert!(formatted.contains("fn foo() -> i32"));
709 assert!(formatted.contains("Docs"));
710 }
711
712 #[test]
713 fn test_format_severity() {
714 assert_eq!(format_severity(DiagnosticSeverity::ERROR), "error");
715 assert_eq!(format_severity(DiagnosticSeverity::WARNING), "warning");
716 assert_eq!(format_severity(DiagnosticSeverity::INFORMATION), "info");
717 assert_eq!(format_severity(DiagnosticSeverity::HINT), "hint");
718 }
719
720 #[test]
721 fn test_path_to_uri() {
722 let path = if cfg!(target_os = "windows") {
723 PathBuf::from("C:\\temp\\test.rs")
724 } else {
725 PathBuf::from("/tmp/test.rs")
726 };
727 let uri = path_to_uri(&path).unwrap();
728 assert!(uri.to_string().ends_with("test.rs"));
729 }
730}