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