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