1use std::collections::{HashMap, HashSet};
9use std::path::Path;
10
11use serde_json::json;
12use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, BufWriter};
13use tokio::process::{Child, ChildStdin, ChildStdout, Command};
14use tracing;
15
16use super::types::{
17 CompletionItem, CompletionResponse, Diagnostic, DocumentFormattingParams, FormattingOptions,
18 HoverResult, Location, Position, ReferenceContext, ReferenceParams, RenameParams,
19 TextDocumentIdentifier, TextDocumentItem, TextEdit, WorkspaceEdit,
20};
21
22#[derive(Debug, thiserror::Error)]
28pub enum LspError {
29 #[error("Server not running: {language}")]
30 ServerNotRunning { language: String },
31
32 #[error("Server failed to start: {message}")]
33 ServerStartFailed { message: String },
34
35 #[error("Request timed out after {timeout_secs}s")]
36 Timeout { timeout_secs: u64 },
37
38 #[error("Protocol error: {message}")]
39 ProtocolError { message: String },
40
41 #[error("IO error: {0}")]
42 Io(#[from] std::io::Error),
43
44 #[error("JSON error: {0}")]
45 Json(#[from] serde_json::Error),
46
47 #[error("Server returned error: code={code}, message={message}")]
48 ServerError { code: i64, message: String },
49
50 #[error("File not found: {path}")]
51 FileNotFound { path: String },
52
53 #[error("Language not supported: {language}")]
54 UnsupportedLanguage { language: String },
55}
56
57pub struct LspClient {
67 process: Child,
68 stdin: BufWriter<ChildStdin>,
69 stdout: BufReader<ChildStdout>,
70 next_id: i64,
71 initialized: bool,
72 open_documents: HashSet<String>,
74 cached_diagnostics: HashMap<String, Vec<Diagnostic>>,
77 root_uri: String,
78}
79
80impl LspClient {
81 pub async fn start(command: &str, args: &[String], workspace: &Path) -> Result<Self, LspError> {
91 let mut child = Command::new(command)
92 .args(args)
93 .stdin(std::process::Stdio::piped())
94 .stdout(std::process::Stdio::piped())
95 .stderr(std::process::Stdio::inherit())
96 .current_dir(workspace)
97 .spawn()
98 .map_err(|e| LspError::ServerStartFailed {
99 message: format!("Failed to spawn `{command}`: {e}"),
100 })?;
101
102 let child_stdin = child
103 .stdin
104 .take()
105 .ok_or_else(|| LspError::ServerStartFailed {
106 message: "Could not capture stdin of child process".into(),
107 })?;
108 let child_stdout = child
109 .stdout
110 .take()
111 .ok_or_else(|| LspError::ServerStartFailed {
112 message: "Could not capture stdout of child process".into(),
113 })?;
114
115 let canonical =
116 std::fs::canonicalize(workspace).unwrap_or_else(|_| workspace.to_path_buf());
117 let root_uri = format!("file://{}", canonical.display());
118
119 let mut client = Self {
120 process: child,
121 stdin: BufWriter::new(child_stdin),
122 stdout: BufReader::new(child_stdout),
123 next_id: 0,
124 initialized: false,
125 open_documents: HashSet::new(),
126 cached_diagnostics: HashMap::new(),
127 root_uri,
128 };
129
130 client.initialize().await?;
131
132 Ok(client)
133 }
134
135 async fn initialize(&mut self) -> Result<(), LspError> {
137 let params = json!({
138 "processId": std::process::id(),
139 "rootUri": self.root_uri,
140 "capabilities": {
141 "textDocument": {
142 "hover": {
143 "contentFormat": ["plaintext", "markdown"]
144 },
145 "completion": {
146 "completionItem": {
147 "snippetSupport": false
148 }
149 },
150 "definition": {},
151 "references": {},
152 "rename": {
153 "prepareSupport": false
154 },
155 "formatting": {},
156 "publishDiagnostics": {
157 "relatedInformation": true
158 }
159 },
160 "workspace": {
161 "applyEdit": true,
162 "workspaceFolders": false
163 }
164 }
165 });
166
167 let _response = self.send_request("initialize", params).await?;
168 self.send_notification("initialized", json!({})).await?;
169 self.initialized = true;
170 tracing::info!(root_uri = %self.root_uri, "LSP server initialized");
171 Ok(())
172 }
173
174 pub async fn send_request(
183 &mut self,
184 method: &str,
185 params: serde_json::Value,
186 ) -> Result<serde_json::Value, LspError> {
187 self.next_id += 1;
188 let id = self.next_id;
189
190 let request = json!({
191 "jsonrpc": "2.0",
192 "id": id,
193 "method": method,
194 "params": params,
195 });
196
197 self.write_message(&request).await?;
198 tracing::debug!(id, method, "Sent LSP request");
199
200 loop {
202 let msg = self.read_message().await?;
203
204 if msg.get("id").is_none() {
206 self.handle_notification(&msg);
207 continue;
208 }
209
210 let resp_id = msg["id"].as_i64().unwrap_or(-1);
212 if resp_id != id {
213 tracing::warn!(
216 expected_id = id,
217 received_id = resp_id,
218 "Received response with unexpected id, skipping"
219 );
220 continue;
221 }
222
223 if let Some(err) = msg.get("error") {
225 let code = err["code"].as_i64().unwrap_or(0);
226 let message = err["message"]
227 .as_str()
228 .unwrap_or("unknown error")
229 .to_string();
230 return Err(LspError::ServerError { code, message });
231 }
232
233 return Ok(msg
235 .get("result")
236 .cloned()
237 .unwrap_or(serde_json::Value::Null));
238 }
239 }
240
241 pub async fn send_notification(
243 &mut self,
244 method: &str,
245 params: serde_json::Value,
246 ) -> Result<(), LspError> {
247 let notification = json!({
248 "jsonrpc": "2.0",
249 "method": method,
250 "params": params,
251 });
252
253 self.write_message(¬ification).await?;
254 tracing::debug!(method, "Sent LSP notification");
255 Ok(())
256 }
257
258 pub async fn read_message(&mut self) -> Result<serde_json::Value, LspError> {
260 let mut content_length: usize = 0;
261
262 loop {
264 let mut line = String::new();
265 let bytes_read = self.stdout.read_line(&mut line).await?;
266 if bytes_read == 0 {
267 return Err(LspError::ProtocolError {
268 message: "Unexpected EOF while reading headers".into(),
269 });
270 }
271 let trimmed = line.trim();
272 if trimmed.is_empty() {
273 break;
274 }
275 if let Some(len_str) = trimmed.strip_prefix("Content-Length: ") {
276 content_length =
277 len_str
278 .trim()
279 .parse::<usize>()
280 .map_err(|_| LspError::ProtocolError {
281 message: format!("Invalid Content-Length value: {len_str}"),
282 })?;
283 }
284 }
286
287 if content_length == 0 {
288 return Err(LspError::ProtocolError {
289 message: "Missing or zero Content-Length header".into(),
290 });
291 }
292
293 let mut body = vec![0u8; content_length];
294 self.stdout.read_exact(&mut body).await?;
295
296 let message: serde_json::Value = serde_json::from_slice(&body)?;
297 Ok(message)
298 }
299
300 async fn write_message(&mut self, message: &serde_json::Value) -> Result<(), LspError> {
302 let body = serde_json::to_string(message)?;
303 let header = format!("Content-Length: {}\r\n\r\n", body.len());
304 self.stdin.write_all(header.as_bytes()).await?;
305 self.stdin.write_all(body.as_bytes()).await?;
306 self.stdin.flush().await?;
307 Ok(())
308 }
309
310 fn handle_notification(&mut self, msg: &serde_json::Value) {
312 let method = match msg.get("method").and_then(|m| m.as_str()) {
313 Some(m) => m,
314 None => return,
315 };
316
317 match method {
318 "textDocument/publishDiagnostics" => {
319 if let Some(params) = msg.get("params")
320 && let Ok(diag_params) =
321 serde_json::from_value::<PublishDiagnosticsNotification>(params.clone())
322 {
323 tracing::debug!(
324 uri = %diag_params.uri,
325 count = diag_params.diagnostics.len(),
326 "Received diagnostics"
327 );
328 self.cached_diagnostics
329 .insert(diag_params.uri, diag_params.diagnostics);
330 }
331 }
332 other => {
333 tracing::debug!(method = other, "Received unhandled notification");
334 }
335 }
336 }
337
338 pub async fn ensure_document_open(&mut self, file_path: &Path) -> Result<String, LspError> {
348 let uri = file_path_to_uri(file_path)?;
349
350 if self.open_documents.contains(&uri) {
351 return Ok(uri);
352 }
353
354 let content =
355 tokio::fs::read_to_string(file_path)
356 .await
357 .map_err(|_| LspError::FileNotFound {
358 path: file_path.display().to_string(),
359 })?;
360
361 let language_id = detect_language_id(file_path);
362
363 let text_doc = TextDocumentItem {
364 uri: uri.clone(),
365 language_id,
366 version: 1,
367 text: content,
368 };
369
370 self.send_notification(
371 "textDocument/didOpen",
372 serde_json::to_value(json!({
373 "textDocument": text_doc
374 }))?,
375 )
376 .await?;
377
378 self.open_documents.insert(uri.clone());
379 Ok(uri)
380 }
381
382 pub async fn hover(
388 &mut self,
389 file: &Path,
390 line: u32,
391 character: u32,
392 ) -> Result<Option<HoverResult>, LspError> {
393 let uri = self.ensure_document_open(file).await?;
394
395 let params = make_text_document_position_params(&uri, line, character);
396 let result = self.send_request("textDocument/hover", params).await?;
397
398 if result.is_null() {
399 return Ok(None);
400 }
401
402 let hover: HoverResult = serde_json::from_value(result)?;
403 Ok(Some(hover))
404 }
405
406 pub async fn definition(
411 &mut self,
412 file: &Path,
413 line: u32,
414 character: u32,
415 ) -> Result<Vec<Location>, LspError> {
416 let uri = self.ensure_document_open(file).await?;
417
418 let params = make_text_document_position_params(&uri, line, character);
419 let result = self.send_request("textDocument/definition", params).await?;
420
421 parse_location_response(result)
422 }
423
424 pub async fn references(
426 &mut self,
427 file: &Path,
428 line: u32,
429 character: u32,
430 ) -> Result<Vec<Location>, LspError> {
431 let uri = self.ensure_document_open(file).await?;
432
433 let params = serde_json::to_value(ReferenceParams {
434 text_document: TextDocumentIdentifier { uri },
435 position: Position { line, character },
436 context: ReferenceContext {
437 include_declaration: true,
438 },
439 })?;
440
441 let result = self.send_request("textDocument/references", params).await?;
442
443 if result.is_null() {
444 return Ok(Vec::new());
445 }
446
447 let locations: Vec<Location> = serde_json::from_value(result)?;
448 Ok(locations)
449 }
450
451 pub async fn completions(
455 &mut self,
456 file: &Path,
457 line: u32,
458 character: u32,
459 ) -> Result<Vec<CompletionItem>, LspError> {
460 let uri = self.ensure_document_open(file).await?;
461
462 let params = make_text_document_position_params(&uri, line, character);
463 let result = self.send_request("textDocument/completion", params).await?;
464
465 if result.is_null() {
466 return Ok(Vec::new());
467 }
468
469 let response: CompletionResponse = serde_json::from_value(result)?;
472 match response {
473 CompletionResponse::Array(items) => Ok(items),
474 CompletionResponse::List(list) => Ok(list.items),
475 }
476 }
477
478 pub fn diagnostics(&self, file: &Path) -> Result<Vec<Diagnostic>, LspError> {
485 let uri = file_path_to_uri(file)?;
486 Ok(self
487 .cached_diagnostics
488 .get(&uri)
489 .cloned()
490 .unwrap_or_default())
491 }
492
493 pub async fn rename(
495 &mut self,
496 file: &Path,
497 line: u32,
498 character: u32,
499 new_name: &str,
500 ) -> Result<WorkspaceEdit, LspError> {
501 let uri = self.ensure_document_open(file).await?;
502
503 let params = serde_json::to_value(RenameParams {
504 text_document: TextDocumentIdentifier { uri },
505 position: Position { line, character },
506 new_name: new_name.to_string(),
507 })?;
508
509 let result = self.send_request("textDocument/rename", params).await?;
510
511 if result.is_null() {
512 return Ok(WorkspaceEdit { changes: None });
513 }
514
515 let edit: WorkspaceEdit = serde_json::from_value(result)?;
516 Ok(edit)
517 }
518
519 pub async fn format(&mut self, file: &Path) -> Result<Vec<TextEdit>, LspError> {
521 let uri = self.ensure_document_open(file).await?;
522
523 let params = serde_json::to_value(DocumentFormattingParams {
524 text_document: TextDocumentIdentifier { uri },
525 options: FormattingOptions {
526 tab_size: 4,
527 insert_spaces: true,
528 },
529 })?;
530
531 let result = self.send_request("textDocument/formatting", params).await?;
532
533 if result.is_null() {
534 return Ok(Vec::new());
535 }
536
537 let edits: Vec<TextEdit> = serde_json::from_value(result)?;
538 Ok(edits)
539 }
540
541 pub async fn shutdown(&mut self) -> Result<(), LspError> {
546 tracing::info!("Shutting down LSP server");
547
548 let _ = self.send_request("shutdown", json!(null)).await;
550
551 let _ = self.send_notification("exit", json!(null)).await;
553
554 let _ = self.process.wait().await;
556
557 self.initialized = false;
558 Ok(())
559 }
560
561 pub fn current_id(&self) -> i64 {
567 self.next_id
568 }
569
570 pub fn is_initialized(&self) -> bool {
572 self.initialized
573 }
574
575 pub fn open_documents(&self) -> &HashSet<String> {
577 &self.open_documents
578 }
579
580 pub fn cached_diagnostics(&self) -> &HashMap<String, Vec<Diagnostic>> {
582 &self.cached_diagnostics
583 }
584
585 pub fn root_uri(&self) -> &str {
587 &self.root_uri
588 }
589}
590
591#[derive(serde::Deserialize)]
597struct PublishDiagnosticsNotification {
598 uri: String,
599 diagnostics: Vec<Diagnostic>,
600}
601
602pub fn file_path_to_uri(path: &Path) -> Result<String, LspError> {
611 let canonical = std::fs::canonicalize(path).map_err(|_| LspError::FileNotFound {
612 path: path.display().to_string(),
613 })?;
614 Ok(format!("file://{}", canonical.display()))
615}
616
617fn make_text_document_position_params(uri: &str, line: u32, character: u32) -> serde_json::Value {
619 json!({
620 "textDocument": { "uri": uri },
621 "position": { "line": line, "character": character }
622 })
623}
624
625fn parse_location_response(value: serde_json::Value) -> Result<Vec<Location>, LspError> {
628 if value.is_null() {
629 return Ok(Vec::new());
630 }
631
632 if value.is_array() {
634 let locations: Vec<Location> = serde_json::from_value(value)?;
635 return Ok(locations);
636 }
637
638 let location: Location = serde_json::from_value(value)?;
640 Ok(vec![location])
641}
642
643fn detect_language_id(path: &Path) -> String {
645 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
646
647 match ext {
648 "rs" => "rust",
649 "py" | "pyi" => "python",
650 "js" | "mjs" | "cjs" => "javascript",
651 "ts" | "mts" | "cts" => "typescript",
652 "tsx" => "typescriptreact",
653 "jsx" => "javascriptreact",
654 "c" | "h" => "c",
655 "cpp" | "cc" | "cxx" | "hpp" | "hxx" => "cpp",
656 "java" => "java",
657 "go" => "go",
658 "rb" => "ruby",
659 "php" => "php",
660 "cs" => "csharp",
661 "swift" => "swift",
662 "kt" | "kts" => "kotlin",
663 "lua" => "lua",
664 "sh" | "bash" | "zsh" => "shellscript",
665 "json" => "json",
666 "yaml" | "yml" => "yaml",
667 "toml" => "toml",
668 "xml" => "xml",
669 "html" | "htm" => "html",
670 "css" => "css",
671 "scss" => "scss",
672 "md" | "markdown" => "markdown",
673 "sql" => "sql",
674 "zig" => "zig",
675 "ex" | "exs" => "elixir",
676 "erl" | "hrl" => "erlang",
677 "hs" => "haskell",
678 "ml" | "mli" => "ocaml",
679 _ => "plaintext",
680 }
681 .to_string()
682}
683
684#[cfg(test)]
689mod tests {
690 use super::*;
691 use std::io::Cursor;
692
693 fn frame_message(value: &serde_json::Value) -> Vec<u8> {
699 let body = serde_json::to_string(value).unwrap();
700 let header = format!("Content-Length: {}\r\n\r\n", body.len());
701 let mut buf = Vec::new();
702 buf.extend_from_slice(header.as_bytes());
703 buf.extend_from_slice(body.as_bytes());
704 buf
705 }
706
707 async fn read_framed_message(data: &[u8]) -> Result<serde_json::Value, LspError> {
710 let mut reader = tokio::io::BufReader::new(Cursor::new(data));
711 let mut content_length: usize = 0;
712
713 loop {
714 let mut line = String::new();
715 let bytes_read = tokio::io::AsyncBufReadExt::read_line(&mut reader, &mut line).await?;
716 if bytes_read == 0 {
717 return Err(LspError::ProtocolError {
718 message: "Unexpected EOF while reading headers".into(),
719 });
720 }
721 let trimmed = line.trim();
722 if trimmed.is_empty() {
723 break;
724 }
725 if let Some(len_str) = trimmed.strip_prefix("Content-Length: ") {
726 content_length =
727 len_str
728 .trim()
729 .parse::<usize>()
730 .map_err(|_| LspError::ProtocolError {
731 message: format!("Invalid Content-Length value: {len_str}"),
732 })?;
733 }
734 }
735
736 if content_length == 0 {
737 return Err(LspError::ProtocolError {
738 message: "Missing or zero Content-Length header".into(),
739 });
740 }
741
742 let mut body = vec![0u8; content_length];
743 tokio::io::AsyncReadExt::read_exact(&mut reader, &mut body).await?;
744 let msg: serde_json::Value = serde_json::from_slice(&body)?;
745 Ok(msg)
746 }
747
748 #[tokio::test]
753 async fn test_content_length_framing() {
754 let original = json!({"jsonrpc": "2.0", "id": 1, "method": "test", "params": {}});
755 let framed = frame_message(&original);
756
757 let header_end = "Content-Length: ".len();
759 assert!(
760 std::str::from_utf8(&framed[..header_end])
761 .unwrap()
762 .starts_with("Content-Length: "),
763 "Frame should start with Content-Length header"
764 );
765
766 let decoded = read_framed_message(&framed).await.unwrap();
768 assert_eq!(decoded, original);
769 }
770
771 #[tokio::test]
772 async fn test_content_length_framing_with_unicode() {
773 let original = json!({"jsonrpc": "2.0", "id": 2, "method": "test", "params": {"text": "\u{1f600} hello"}});
774 let framed = frame_message(&original);
775 let decoded = read_framed_message(&framed).await.unwrap();
776 assert_eq!(decoded, original);
777 }
778
779 #[tokio::test]
780 async fn test_content_length_framing_multiple_messages() {
781 let msg1 = json!({"jsonrpc": "2.0", "id": 1, "result": "first"});
782 let msg2 = json!({"jsonrpc": "2.0", "id": 2, "result": "second"});
783
784 let mut data = frame_message(&msg1);
785 data.extend(frame_message(&msg2));
786
787 let mut reader = tokio::io::BufReader::new(Cursor::new(data.clone()));
789 let mut content_length: usize = 0;
790
791 loop {
793 let mut line = String::new();
794 tokio::io::AsyncBufReadExt::read_line(&mut reader, &mut line)
795 .await
796 .unwrap();
797 let trimmed = line.trim();
798 if trimmed.is_empty() {
799 break;
800 }
801 if let Some(len_str) = trimmed.strip_prefix("Content-Length: ") {
802 content_length = len_str.trim().parse().unwrap();
803 }
804 }
805 let mut body = vec![0u8; content_length];
806 tokio::io::AsyncReadExt::read_exact(&mut reader, &mut body)
807 .await
808 .unwrap();
809 let first: serde_json::Value = serde_json::from_slice(&body).unwrap();
810 assert_eq!(first["id"], 1);
811 assert_eq!(first["result"], "first");
812
813 content_length = 0;
815 loop {
816 let mut line = String::new();
817 tokio::io::AsyncBufReadExt::read_line(&mut reader, &mut line)
818 .await
819 .unwrap();
820 let trimmed = line.trim();
821 if trimmed.is_empty() {
822 break;
823 }
824 if let Some(len_str) = trimmed.strip_prefix("Content-Length: ") {
825 content_length = len_str.trim().parse().unwrap();
826 }
827 }
828 let mut body2 = vec![0u8; content_length];
829 tokio::io::AsyncReadExt::read_exact(&mut reader, &mut body2)
830 .await
831 .unwrap();
832 let second: serde_json::Value = serde_json::from_slice(&body2).unwrap();
833 assert_eq!(second["id"], 2);
834 assert_eq!(second["result"], "second");
835 }
836
837 #[tokio::test]
842 async fn test_request_id_incrementing() {
843 let mut next_id: i64 = 0;
847
848 let mut ids = Vec::new();
849 for _ in 0..5 {
850 next_id += 1;
851 ids.push(next_id);
852 }
853
854 assert_eq!(ids, vec![1, 2, 3, 4, 5]);
855
856 for (i, id) in ids.iter().enumerate() {
858 let request = json!({
859 "jsonrpc": "2.0",
860 "id": id,
861 "method": "test",
862 "params": {}
863 });
864 assert_eq!(request["id"], (i as i64) + 1);
865 }
866 }
867
868 #[tokio::test]
873 async fn test_file_to_uri_conversion() {
874 let tmp = tempfile::NamedTempFile::new().unwrap();
875 let path = tmp.path();
876
877 let uri = file_path_to_uri(path).unwrap();
878
879 assert!(uri.starts_with("file://"), "URI should start with file://");
880 assert!(
882 !uri.contains(".."),
883 "URI should not contain relative components"
884 );
885 let canonical = std::fs::canonicalize(path).unwrap();
887 assert_eq!(uri, format!("file://{}", canonical.display()));
888 }
889
890 #[tokio::test]
891 async fn test_file_to_uri_nonexistent() {
892 let result = file_path_to_uri(Path::new("/tmp/absolutely_nonexistent_file_xyz.rs"));
893 assert!(result.is_err());
894 match result.unwrap_err() {
895 LspError::FileNotFound { path } => {
896 assert!(path.contains("absolutely_nonexistent_file_xyz.rs"));
897 }
898 other => panic!("Expected FileNotFound, got: {other:?}"),
899 }
900 }
901
902 #[tokio::test]
907 async fn test_hover_response_parsing_with_contents() {
908 let response = json!({
910 "contents": "fn main()",
911 "range": {
912 "start": {"line": 0, "character": 3},
913 "end": {"line": 0, "character": 7}
914 }
915 });
916
917 let hover: Result<HoverResult, _> = serde_json::from_value(response);
918 assert!(hover.is_ok(), "Should parse hover with string contents");
919 }
920
921 #[tokio::test]
922 async fn test_hover_response_parsing_null() {
923 let value = serde_json::Value::Null;
924 assert!(value.is_null(), "Null response should indicate no hover");
925 }
926
927 #[tokio::test]
932 async fn test_definition_response_parsing_single() {
933 let response = json!({
934 "uri": "file:///src/main.rs",
935 "range": {
936 "start": {"line": 10, "character": 0},
937 "end": {"line": 10, "character": 5}
938 }
939 });
940
941 let locations = parse_location_response(response).unwrap();
942 assert_eq!(locations.len(), 1);
943 assert_eq!(locations[0].uri, "file:///src/main.rs");
944 assert_eq!(locations[0].range.start.line, 10);
945 }
946
947 #[tokio::test]
948 async fn test_definition_response_parsing_array() {
949 let response = json!([
950 {
951 "uri": "file:///src/lib.rs",
952 "range": {
953 "start": {"line": 5, "character": 0},
954 "end": {"line": 5, "character": 10}
955 }
956 },
957 {
958 "uri": "file:///src/util.rs",
959 "range": {
960 "start": {"line": 20, "character": 4},
961 "end": {"line": 20, "character": 15}
962 }
963 }
964 ]);
965
966 let locations = parse_location_response(response).unwrap();
967 assert_eq!(locations.len(), 2);
968 assert_eq!(locations[0].uri, "file:///src/lib.rs");
969 assert_eq!(locations[1].uri, "file:///src/util.rs");
970 }
971
972 #[tokio::test]
973 async fn test_definition_response_parsing_null() {
974 let locations = parse_location_response(serde_json::Value::Null).unwrap();
975 assert!(locations.is_empty());
976 }
977
978 #[tokio::test]
983 async fn test_completion_response_parsing_array() {
984 let response = json!([
985 {"label": "foo", "kind": 6},
986 {"label": "bar", "kind": 3}
987 ]);
988
989 let items: Vec<CompletionItem> = serde_json::from_value(response).unwrap();
990 assert_eq!(items.len(), 2);
991 assert_eq!(items[0].label, "foo");
992 assert_eq!(items[1].label, "bar");
993 }
994
995 #[tokio::test]
996 async fn test_completion_response_parsing_completion_list() {
997 let response = json!({
998 "isIncomplete": false,
999 "items": [
1000 {"label": "println!", "kind": 3},
1001 {"label": "print!", "kind": 3}
1002 ]
1003 });
1004
1005 let cr: CompletionResponse = serde_json::from_value(response).unwrap();
1007 match cr {
1008 CompletionResponse::List(list) => {
1009 assert_eq!(list.items.len(), 2);
1010 assert_eq!(list.items[0].label, "println!");
1011 }
1012 CompletionResponse::Array(items) => {
1013 assert_eq!(items.len(), 2);
1015 }
1016 }
1017 }
1018
1019 #[tokio::test]
1020 async fn test_completion_response_parsing_null() {
1021 let value = serde_json::Value::Null;
1022 assert!(value.is_null());
1023 }
1025
1026 #[tokio::test]
1031 async fn test_diagnostic_caching() {
1032 let mut diagnostics: HashMap<String, Vec<Diagnostic>> = HashMap::new();
1033 let uri = "file:///src/main.rs".to_string();
1034
1035 assert!(!diagnostics.contains_key(&uri));
1037
1038 let notification_params = json!({
1040 "uri": "file:///src/main.rs",
1041 "diagnostics": [
1042 {
1043 "range": {
1044 "start": {"line": 3, "character": 0},
1045 "end": {"line": 3, "character": 10}
1046 },
1047 "severity": 1,
1048 "message": "unused variable"
1049 }
1050 ]
1051 });
1052
1053 let parsed: PublishDiagnosticsNotification =
1054 serde_json::from_value(notification_params).unwrap();
1055
1056 diagnostics.insert(parsed.uri.clone(), parsed.diagnostics);
1057
1058 let cached = diagnostics.get(&uri).unwrap();
1059 assert_eq!(cached.len(), 1);
1060 assert_eq!(cached[0].message, "unused variable");
1061
1062 let updated_params = json!({
1064 "uri": "file:///src/main.rs",
1065 "diagnostics": [
1066 {
1067 "range": {
1068 "start": {"line": 3, "character": 0},
1069 "end": {"line": 3, "character": 10}
1070 },
1071 "severity": 1,
1072 "message": "unused variable"
1073 },
1074 {
1075 "range": {
1076 "start": {"line": 10, "character": 0},
1077 "end": {"line": 10, "character": 5}
1078 },
1079 "severity": 2,
1080 "message": "dead code"
1081 }
1082 ]
1083 });
1084
1085 let parsed2: PublishDiagnosticsNotification =
1086 serde_json::from_value(updated_params).unwrap();
1087 diagnostics.insert(parsed2.uri.clone(), parsed2.diagnostics);
1088
1089 let cached2 = diagnostics.get(&uri).unwrap();
1090 assert_eq!(cached2.len(), 2);
1091 assert_eq!(cached2[1].message, "dead code");
1092 }
1093
1094 #[test]
1099 fn test_lsp_error_display() {
1100 let err = LspError::ServerNotRunning {
1101 language: "rust".into(),
1102 };
1103 assert_eq!(err.to_string(), "Server not running: rust");
1104
1105 let err = LspError::ServerStartFailed {
1106 message: "binary not found".into(),
1107 };
1108 assert_eq!(err.to_string(), "Server failed to start: binary not found");
1109
1110 let err = LspError::Timeout { timeout_secs: 30 };
1111 assert_eq!(err.to_string(), "Request timed out after 30s");
1112
1113 let err = LspError::ProtocolError {
1114 message: "bad header".into(),
1115 };
1116 assert_eq!(err.to_string(), "Protocol error: bad header");
1117
1118 let err = LspError::ServerError {
1119 code: -32600,
1120 message: "Invalid request".into(),
1121 };
1122 assert_eq!(
1123 err.to_string(),
1124 "Server returned error: code=-32600, message=Invalid request"
1125 );
1126
1127 let err = LspError::FileNotFound {
1128 path: "/tmp/foo.rs".into(),
1129 };
1130 assert_eq!(err.to_string(), "File not found: /tmp/foo.rs");
1131
1132 let err = LspError::UnsupportedLanguage {
1133 language: "brainfuck".into(),
1134 };
1135 assert_eq!(err.to_string(), "Language not supported: brainfuck");
1136 }
1137
1138 #[test]
1143 fn test_detect_language_id() {
1144 assert_eq!(detect_language_id(Path::new("main.rs")), "rust");
1145 assert_eq!(detect_language_id(Path::new("app.py")), "python");
1146 assert_eq!(detect_language_id(Path::new("index.ts")), "typescript");
1147 assert_eq!(detect_language_id(Path::new("App.tsx")), "typescriptreact");
1148 assert_eq!(detect_language_id(Path::new("script.js")), "javascript");
1149 assert_eq!(detect_language_id(Path::new("main.go")), "go");
1150 assert_eq!(detect_language_id(Path::new("Main.java")), "java");
1151 assert_eq!(detect_language_id(Path::new("prog.cpp")), "cpp");
1152 assert_eq!(detect_language_id(Path::new("header.h")), "c");
1153 assert_eq!(detect_language_id(Path::new("unknown.xyz")), "plaintext");
1154 assert_eq!(detect_language_id(Path::new("no_extension")), "plaintext");
1155 }
1156
1157 #[test]
1162 fn test_handle_notification_diagnostics() {
1163 let notification = json!({
1167 "jsonrpc": "2.0",
1168 "method": "textDocument/publishDiagnostics",
1169 "params": {
1170 "uri": "file:///project/src/lib.rs",
1171 "diagnostics": [
1172 {
1173 "range": {
1174 "start": {"line": 1, "character": 0},
1175 "end": {"line": 1, "character": 5}
1176 },
1177 "severity": 1,
1178 "message": "syntax error"
1179 }
1180 ]
1181 }
1182 });
1183
1184 let params = notification.get("params").unwrap();
1186 let parsed: PublishDiagnosticsNotification =
1187 serde_json::from_value(params.clone()).unwrap();
1188
1189 assert_eq!(parsed.uri, "file:///project/src/lib.rs");
1190 assert_eq!(parsed.diagnostics.len(), 1);
1191 assert_eq!(parsed.diagnostics[0].message, "syntax error");
1192 }
1193
1194 #[tokio::test]
1199 async fn test_parse_location_response_empty_array() {
1200 let response = json!([]);
1201 let locations = parse_location_response(response).unwrap();
1202 assert!(locations.is_empty());
1203 }
1204
1205 #[tokio::test]
1206 async fn test_parse_location_response_invalid() {
1207 let response = json!("not a location");
1208 let result = parse_location_response(response);
1209 assert!(result.is_err());
1210 }
1211}