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;
19
20use super::transport::LspTransport;
21use super::types::LspServerConfig;
22
23#[derive(Debug, Clone)]
25pub struct HoverResult {
26 pub signature: String,
28 pub documentation: Option<String>,
30}
31
32impl HoverResult {
33 pub fn new(signature: impl Into<String>) -> Self {
35 Self {
36 signature: signature.into(),
37 documentation: None,
38 }
39 }
40
41 pub fn with_documentation(mut self, doc: impl Into<String>) -> Self {
43 self.documentation = Some(doc.into());
44 self
45 }
46}
47
48pub struct LspClient {
53 transport: Arc<Mutex<Option<LspTransport>>>,
55 language: String,
57 server_name: String,
59 project_root: PathBuf,
61 open_files: Arc<Mutex<HashMap<Url, String>>>,
63 diagnostics_cache: Arc<Mutex<HashMap<Url, Vec<Diagnostic>>>>,
65 capabilities: Arc<Mutex<Option<lsp_types::ServerCapabilities>>>,
67}
68
69impl LspClient {
70 pub fn new(language: impl Into<String>, server_name: impl Into<String>, project_root: PathBuf) -> Self {
72 Self {
73 transport: Arc::new(Mutex::new(None)),
74 language: language.into(),
75 server_name: server_name.into(),
76 project_root,
77 open_files: Arc::new(Mutex::new(HashMap::new())),
78 diagnostics_cache: Arc::new(Mutex::new(HashMap::new())),
79 capabilities: Arc::new(Mutex::new(None)),
80 }
81 }
82
83 pub fn from_config(config: &LspServerConfig, project_root: PathBuf) -> Self {
85 Self::new(config.language.clone(), config.command.clone(), project_root)
86 }
87
88 pub async fn spawn(&self, config: &LspServerConfig) -> Result<()> {
90 log::info!("LSP spawn: starting '{}'...", self.server_name);
91 crate::debug::debug_log().log("lsp", &format!("spawn: starting '{}'...", self.server_name));
92
93 let transport = LspTransport::spawn(&config.command, &config.command, &config.args).await?;
95 log::info!("LSP spawn: '{}' process started", self.server_name);
96 crate::debug::debug_log().log("lsp", &format!("spawn: '{}' process started", self.server_name));
97
98 {
99 let mut transport_guard = self.transport.lock().await;
100 *transport_guard = Some(transport);
101 }
102
103 log::info!("LSP spawn: initializing '{}'...", self.server_name);
105 crate::debug::debug_log().log("lsp", &format!("spawn: initializing '{}'...", self.server_name));
106 self.initialize().await?;
107 log::info!("LSP spawn: '{}' initialized successfully", self.server_name);
108 crate::debug::debug_log().log("lsp", &format!("spawn: '{}' initialized OK", self.server_name));
109
110 log::info!("LSP client '{}' spawned and initialized successfully", self.server_name);
111 Ok(())
112 }
113
114 #[allow(deprecated)] pub async fn initialize(&self) -> Result<()> {
117 let transport = self.transport.lock().await;
118 let transport = transport.as_ref().ok_or_else(|| anyhow!("Transport not initialized"))?;
119
120 let root_uri = Url::from_file_path(&self.project_root)
121 .map_err(|_| anyhow!("Invalid project root path: {:?}", self.project_root))?;
122
123 let params = InitializeParams {
124 process_id: Some(std::process::id()),
125 root_path: None,
126 root_uri: Some(root_uri.clone()),
127 initialization_options: None,
128 capabilities: ClientCapabilities {
129 text_document: Some(TextDocumentClientCapabilities {
130 hover: Some(lsp_types::HoverClientCapabilities {
131 dynamic_registration: Some(false),
132 content_format: Some(vec![MarkupKind::Markdown, MarkupKind::PlainText]),
133 }),
134 definition: Some(lsp_types::GotoCapability {
135 dynamic_registration: Some(false),
136 link_support: Some(true),
137 }),
138 references: Some(lsp_types::ReferenceClientCapabilities {
139 dynamic_registration: Some(false),
140 }),
141 publish_diagnostics: Some(lsp_types::PublishDiagnosticsClientCapabilities {
142 related_information: Some(true),
143 tag_support: Some(lsp_types::TagSupport {
144 value_set: vec![lsp_types::DiagnosticTag::UNNECESSARY, lsp_types::DiagnosticTag::DEPRECATED],
145 }),
146 version_support: Some(false),
147 code_description_support: Some(true),
148 data_support: Some(false),
149 }),
150 ..Default::default()
151 }),
152 workspace: Some(WorkspaceClientCapabilities {
153 workspace_folders: Some(true),
154 ..Default::default()
155 }),
156 ..Default::default()
157 },
158 trace: Some(lsp_types::TraceValue::Off),
159 workspace_folders: Some(vec![WorkspaceFolder {
160 uri: root_uri,
161 name: self.project_root
162 .file_name()
163 .map(|n| n.to_string_lossy().to_string())
164 .unwrap_or_else(|| "workspace".to_string()),
165 }]),
166 client_info: Some(lsp_types::ClientInfo {
167 name: "matrixcode".to_string(),
168 version: Some(env!("CARGO_PKG_VERSION").to_string()),
169 }),
170 locale: None,
171 work_done_progress_params: Default::default(),
172 };
173
174 let result = transport
175 .send_request("initialize", serde_json::to_value(params)?)
176 .await?;
177
178 if let Some(capabilities) = result.get("capabilities") {
180 let caps: lsp_types::ServerCapabilities = serde_json::from_value(capabilities.clone())?;
181 let mut caps_guard = self.capabilities.lock().await;
182 *caps_guard = Some(caps);
183 }
184
185 self.initialized().await?;
187
188 Ok(())
189 }
190
191 pub async fn initialized(&self) -> Result<()> {
193 let transport = self.transport.lock().await;
194 let transport = transport.as_ref().ok_or_else(|| anyhow!("Transport not initialized"))?;
195
196 let params = InitializedParams {};
197 transport
198 .send_notification("initialized", serde_json::to_value(params)?)
199 .await?;
200
201 Ok(())
202 }
203
204 pub async fn open_file(&self, uri: &Url, content: &str) -> Result<()> {
206 let transport = self.transport.lock().await;
207 let transport = transport.as_ref().ok_or_else(|| anyhow!("Transport not initialized"))?;
208
209 let language_id = self.language.clone();
210 let version = 1;
211
212 let text_document = TextDocumentItem {
213 uri: uri.clone(),
214 language_id,
215 version,
216 text: content.to_string(),
217 };
218
219 let params = lsp_types::DidOpenTextDocumentParams { text_document };
220 transport
221 .send_notification("textDocument/didOpen", serde_json::to_value(params)?)
222 .await?;
223
224 let mut open_files = self.open_files.lock().await;
226 open_files.insert(uri.clone(), content.to_string());
227
228 log::debug!("Opened file in LSP: {}", uri);
229 Ok(())
230 }
231
232 pub async fn hover(&self, uri: &Url, position: Position) -> Result<Option<HoverResult>> {
234 let transport = self.transport.lock().await;
235 let transport = transport.as_ref().ok_or_else(|| anyhow!("Transport not initialized"))?;
236
237 let text_document = TextDocumentIdentifier { uri: uri.clone() };
238 let params = HoverParams {
239 text_document_position_params: TextDocumentPositionParams {
240 text_document,
241 position,
242 },
243 work_done_progress_params: Default::default(),
244 };
245
246 let result = transport
247 .send_request("textDocument/hover", serde_json::to_value(params)?)
248 .await?;
249
250 if result.is_null() {
251 return Ok(None);
252 }
253
254 let hover: Hover = serde_json::from_value(result)?;
255 Ok(Some(Self::parse_hover(hover)))
256 }
257
258 fn parse_hover(hover: Hover) -> HoverResult {
260 let (signature, documentation) = match hover.contents {
261 HoverContents::Scalar(scalar) => {
262 let content = match scalar {
263 lsp_types::MarkedString::String(s) => s,
264 lsp_types::MarkedString::LanguageString(ls) => {
265 format!("```{}\n{}\n```", ls.language, ls.value)
266 }
267 };
268 (content, None)
269 }
270 HoverContents::Array(arr) => {
271 let parts: Vec<String> = arr
272 .into_iter()
273 .map(|ms| match ms {
274 lsp_types::MarkedString::String(s) => s,
275 lsp_types::MarkedString::LanguageString(ls) => {
276 format!("```{}\n{}\n```", ls.language, ls.value)
277 }
278 })
279 .collect();
280 let signature = parts.first().cloned().unwrap_or_default();
281 let documentation = if parts.len() > 1 {
282 Some(parts[1..].join("\n\n"))
283 } else {
284 None
285 };
286 (signature, documentation)
287 }
288 HoverContents::Markup(markup) => {
289 let content = markup.value;
290 if content.contains("\n\n") {
292 let parts: Vec<&str> = content.splitn(2, "\n\n").collect();
293 (parts[0].to_string(), Some(parts[1].to_string()))
294 } else {
295 (content, None)
296 }
297 }
298 };
299
300 HoverResult { signature, documentation }
301 }
302
303 pub async fn definition(&self, uri: &Url, position: Position) -> Result<Vec<Location>> {
305 let transport = self.transport.lock().await;
306 let transport = transport.as_ref().ok_or_else(|| anyhow!("Transport not initialized"))?;
307
308 let text_document = TextDocumentIdentifier { uri: uri.clone() };
309 let params = GotoDefinitionParams {
310 text_document_position_params: TextDocumentPositionParams {
311 text_document,
312 position,
313 },
314 work_done_progress_params: Default::default(),
315 partial_result_params: Default::default(),
316 };
317
318 let result = transport
319 .send_request("textDocument/definition", serde_json::to_value(params)?)
320 .await?;
321
322 if result.is_null() {
323 return Ok(Vec::new());
324 }
325
326 let locations = Self::parse_definition_response(result)?;
327 Ok(locations)
328 }
329
330 fn parse_definition_response(result: serde_json::Value) -> Result<Vec<Location>> {
332 if let Ok(links) = serde_json::from_value::<Vec<lsp_types::LocationLink>>(result.clone()) {
334 return Ok(links
335 .into_iter()
336 .map(|link| Location {
337 uri: link.target_uri,
338 range: link.target_selection_range,
339 })
340 .collect());
341 }
342
343 if let Ok(locations) = serde_json::from_value::<Vec<Location>>(result.clone()) {
345 return Ok(locations);
346 }
347
348 if let Ok(location) = serde_json::from_value::<Location>(result.clone()) {
350 return Ok(vec![location]);
351 }
352
353 if let Ok(link) = serde_json::from_value::<lsp_types::LocationLink>(result) {
355 return Ok(vec![Location {
356 uri: link.target_uri,
357 range: link.target_selection_range,
358 }]);
359 }
360
361 Ok(Vec::new())
362 }
363
364 pub async fn references(&self, uri: &Url, position: Position, include_declaration: bool) -> Result<Vec<Location>> {
366 let transport = self.transport.lock().await;
367 let transport = transport.as_ref().ok_or_else(|| anyhow!("Transport not initialized"))?;
368
369 let text_document = TextDocumentIdentifier { uri: uri.clone() };
370 let params = ReferenceParams {
371 text_document_position: TextDocumentPositionParams {
372 text_document,
373 position,
374 },
375 work_done_progress_params: Default::default(),
376 partial_result_params: Default::default(),
377 context: ReferenceContext {
378 include_declaration,
379 },
380 };
381
382 let result = transport
383 .send_request("textDocument/references", serde_json::to_value(params)?)
384 .await?;
385
386 if result.is_null() {
387 return Ok(Vec::new());
388 }
389
390 let locations: Vec<Location> = serde_json::from_value(result)?;
391 Ok(locations)
392 }
393
394 pub async fn diagnostics(&self, uri: &Url) -> Result<Vec<Diagnostic>> {
396 let cache = self.diagnostics_cache.lock().await;
397 Ok(cache.get(uri).cloned().unwrap_or_default())
398 }
399
400 pub async fn update_diagnostics(&self, uri: Url, diagnostics: Vec<Diagnostic>) {
402 let mut cache = self.diagnostics_cache.lock().await;
403 cache.insert(uri, diagnostics);
404 }
405
406 pub async fn shutdown(&self) -> Result<()> {
408 let mut transport_guard = self.transport.lock().await;
409
410 if let Some(transport) = transport_guard.take() {
411 transport
413 .send_request("shutdown", serde_json::Value::Null)
414 .await?;
415
416 transport
418 .send_notification("exit", serde_json::Value::Null)
419 .await?;
420
421 transport.close().await?;
423
424 log::info!("LSP client '{}' shutdown successfully", self.server_name);
425 }
426
427 let mut open_files = self.open_files.lock().await;
429 open_files.clear();
430 let mut diagnostics_cache = self.diagnostics_cache.lock().await;
431 diagnostics_cache.clear();
432
433 Ok(())
434 }
435
436 pub async fn is_connected(&self) -> bool {
438 let transport = self.transport.lock().await;
439 transport.is_some()
440 }
441
442 pub fn language(&self) -> &str {
444 &self.language
445 }
446
447 pub fn server_name(&self) -> &str {
449 &self.server_name
450 }
451
452 pub fn project_root(&self) -> &PathBuf {
454 &self.project_root
455 }
456
457 pub async fn capabilities(&self) -> Option<lsp_types::ServerCapabilities> {
459 let caps = self.capabilities.lock().await;
460 caps.clone()
461 }
462}
463
464pub fn format_location(location: &Location) -> String {
470 let path = location.uri.to_file_path();
471 let path_str = path
472 .map(|p| p.to_string_lossy().to_string())
473 .unwrap_or_else(|_| location.uri.to_string());
474
475 let range = &location.range;
476 let start = &range.start;
477 format!(
478 "{}:{}:{}",
479 path_str,
480 start.line + 1, start.character + 1
482 )
483}
484
485pub fn format_diagnostic(diagnostic: &Diagnostic) -> String {
487 let severity = diagnostic.severity
488 .map(|s| format_severity(s))
489 .unwrap_or_else(|| "error".to_string());
490
491 let message = &diagnostic.message;
492
493 let location = diagnostic.related_information
494 .as_ref()
495 .and_then(|info| info.first())
496 .map(|info| format!(" at {}:{}", info.location.uri, info.location.range.start.line + 1))
497 .unwrap_or_default();
498
499 let code = diagnostic.code
500 .as_ref()
501 .map(|c| format!("[{}] ", match c {
502 lsp_types::NumberOrString::Number(n) => n.to_string(),
503 lsp_types::NumberOrString::String(s) => s.clone(),
504 }))
505 .unwrap_or_default();
506
507 format!("{}{}: {}{}", severity, code, message, location)
508}
509
510fn format_severity(severity: DiagnosticSeverity) -> String {
512 match severity {
513 DiagnosticSeverity::ERROR => "error".to_string(),
514 DiagnosticSeverity::WARNING => "warning".to_string(),
515 DiagnosticSeverity::INFORMATION => "info".to_string(),
516 DiagnosticSeverity::HINT => "hint".to_string(),
517 _ => "unknown".to_string(),
518 }
519}
520
521pub fn format_hover_result(hover: &HoverResult) -> String {
523 if let Some(doc) = &hover.documentation {
524 format!("{}\n\n{}", hover.signature, doc)
525 } else {
526 hover.signature.clone()
527 }
528}
529
530pub fn path_to_uri(path: &PathBuf) -> Result<Url> {
532 Url::from_file_path(path)
533 .map_err(|_| anyhow!("Invalid file path: {:?}", path))
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539
540 #[test]
541 fn test_hover_result_new() {
542 let result = HoverResult::new("fn foo() -> i32");
543 assert_eq!(result.signature, "fn foo() -> i32");
544 assert!(result.documentation.is_none());
545 }
546
547 #[test]
548 fn test_hover_result_with_documentation() {
549 let result = HoverResult::new("fn foo() -> i32")
550 .with_documentation("This is a test function");
551 assert_eq!(result.signature, "fn foo() -> i32");
552 assert_eq!(result.documentation, Some("This is a test function".to_string()));
553 }
554
555 #[test]
556 fn test_format_hover_result() {
557 let hover = HoverResult::new("fn foo() -> i32")
558 .with_documentation("Docs");
559 let formatted = format_hover_result(&hover);
560 assert!(formatted.contains("fn foo() -> i32"));
561 assert!(formatted.contains("Docs"));
562 }
563
564 #[test]
565 fn test_format_severity() {
566 assert_eq!(format_severity(DiagnosticSeverity::ERROR), "error");
567 assert_eq!(format_severity(DiagnosticSeverity::WARNING), "warning");
568 assert_eq!(format_severity(DiagnosticSeverity::INFORMATION), "info");
569 assert_eq!(format_severity(DiagnosticSeverity::HINT), "hint");
570 }
571
572 #[test]
573 fn test_path_to_uri() {
574 let path = if cfg!(target_os = "windows") {
575 PathBuf::from("C:\\temp\\test.rs")
576 } else {
577 PathBuf::from("/tmp/test.rs")
578 };
579 let uri = path_to_uri(&path).unwrap();
580 assert!(uri.to_string().ends_with("test.rs"));
581 }
582}