client.rs
1 use serde::{Deserialize, Serialize}; 2 use serde_json::{json, Value}; 3 use std::collections::HashMap; 4 use std::io::{BufRead, BufReader, Write}; 5 use std::path::PathBuf; 6 use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio}; 7 use std::sync::atomic::{AtomicI64, Ordering}; 8 use std::sync::{Arc, Mutex, RwLock}; 9 use std::thread; 10 use std::time::{Duration, Instant}; 11 12 static REQUEST_ID: AtomicI64 = AtomicI64::new(1); 13 14 pub struct LspTestClient { 15 _child: Child, 16 17 stdin: Arc<Mutex<ChildStdin>>, 18 19 pending_responses: Arc<RwLock<HashMap<i64, Value>>>, 20 21 notifications: Arc<RwLock<Vec<JsonRpcNotification>>>, 22 23 open_documents: Arc<Mutex<HashMap<String, String>>>, 24 25 _reader_handle: thread::JoinHandle<()>, 26 27 pub workspace_root: PathBuf, 28 } 29 30 #[derive(Debug, Clone, Serialize, Deserialize)] 31 pub struct JsonRpcRequest { 32 pub jsonrpc: String, 33 pub id: i64, 34 pub method: String, 35 #[serde(skip_serializing_if = "Option::is_none")] 36 pub params: Option<Value>, 37 } 38 39 #[allow(dead_code)] 40 #[derive(Debug, Clone, Serialize, Deserialize)] 41 pub struct JsonRpcResponse { 42 pub jsonrpc: String, 43 #[serde(skip_serializing_if = "Option::is_none")] 44 pub id: Option<i64>, 45 #[serde(skip_serializing_if = "Option::is_none")] 46 pub result: Option<Value>, 47 #[serde(skip_serializing_if = "Option::is_none")] 48 pub error: Option<JsonRpcError>, 49 } 50 51 #[allow(dead_code)] 52 #[derive(Debug, Clone, Serialize, Deserialize)] 53 pub struct JsonRpcError { 54 pub code: i64, 55 pub message: String, 56 #[serde(skip_serializing_if = "Option::is_none")] 57 pub data: Option<Value>, 58 } 59 60 #[derive(Debug, Clone, Serialize, Deserialize)] 61 pub struct JsonRpcNotification { 62 pub jsonrpc: String, 63 pub method: String, 64 #[serde(skip_serializing_if = "Option::is_none")] 65 pub params: Option<Value>, 66 } 67 68 impl LspTestClient { 69 pub fn spawn(workspace_root: PathBuf) -> Result<Self, Box<dyn std::error::Error>> { 70 let lsp_binary = std::env::var("ECOLOG_LSP_BINARY") 71 .map(PathBuf::from) 72 .unwrap_or_else(|_| { 73 let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 74 75 let local_binary = manifest_dir.join("target").join("debug").join("ecolog-lsp"); 76 if local_binary.exists() { 77 return local_binary; 78 } 79 80 manifest_dir 81 .parent() 82 .map(|p| p.join("target").join("debug").join("ecolog-lsp")) 83 .unwrap_or(local_binary) 84 }); 85 86 if !lsp_binary.exists() { 87 return Err(format!( 88 "LSP binary not found at {:?}. Run 'cargo build' first.", 89 lsp_binary 90 ) 91 .into()); 92 } 93 94 let mut child = Command::new(&lsp_binary) 95 .current_dir(&workspace_root) 96 .stdin(Stdio::piped()) 97 .stdout(Stdio::piped()) 98 .stderr(Stdio::inherit()) 99 .spawn()?; 100 101 let stdin = child.stdin.take().expect("Failed to capture stdin"); 102 let stdout = child.stdout.take().expect("Failed to capture stdout"); 103 104 let stdin = Arc::new(Mutex::new(stdin)); 105 let pending_responses = Arc::new(RwLock::new(HashMap::new())); 106 let notifications = Arc::new(RwLock::new(Vec::new())); 107 let open_documents = Arc::new(Mutex::new(HashMap::new())); 108 109 let pending_clone = Arc::clone(&pending_responses); 110 let notifications_clone = Arc::clone(¬ifications); 111 let stdin_clone = Arc::clone(&stdin); 112 113 let reader_handle = thread::spawn(move || { 114 Self::read_messages(stdout, pending_clone, notifications_clone, stdin_clone); 115 }); 116 117 Ok(Self { 118 _child: child, 119 stdin, 120 pending_responses, 121 notifications, 122 open_documents, 123 _reader_handle: reader_handle, 124 workspace_root, 125 }) 126 } 127 128 fn read_messages( 129 stdout: ChildStdout, 130 pending: Arc<RwLock<HashMap<i64, Value>>>, 131 notifications: Arc<RwLock<Vec<JsonRpcNotification>>>, 132 stdin: Arc<Mutex<ChildStdin>>, 133 ) { 134 let mut reader = BufReader::new(stdout); 135 136 loop { 137 let mut content_length: Option<usize> = None; 138 loop { 139 let mut line = String::new(); 140 if reader.read_line(&mut line).unwrap_or(0) == 0 { 141 return; 142 } 143 let line = line.trim(); 144 if line.is_empty() { 145 break; 146 } 147 if let Some(len_str) = line.strip_prefix("Content-Length:") { 148 content_length = len_str.trim().parse().ok(); 149 } 150 } 151 152 let Some(len) = content_length else { 153 continue; 154 }; 155 156 let mut content = vec![0u8; len]; 157 if std::io::Read::read_exact(&mut reader, &mut content).is_err() { 158 return; 159 } 160 161 let Ok(message): Result<Value, _> = serde_json::from_slice(&content) else { 162 continue; 163 }; 164 165 if let Some(id) = message.get("id").and_then(|v| v.as_i64()) { 166 if message.get("result").is_some() || message.get("error").is_some() { 167 pending.write().unwrap().insert(id, message); 168 } else if message.get("method").is_some() { 169 // Server-to-client request: respond with empty success 170 let response = json!({ 171 "jsonrpc": "2.0", 172 "id": id, 173 "result": null 174 }); 175 let content = serde_json::to_string(&response).unwrap(); 176 let header = format!("Content-Length: {}\r\n\r\n", content.len()); 177 if let Ok(mut stdin_guard) = stdin.lock() { 178 let _ = stdin_guard.write_all(header.as_bytes()); 179 let _ = stdin_guard.write_all(content.as_bytes()); 180 let _ = stdin_guard.flush(); 181 } 182 } 183 } else if message.get("method").is_some() { 184 if let Ok(notif) = serde_json::from_value::<JsonRpcNotification>(message) { 185 notifications.write().unwrap().push(notif); 186 } 187 } 188 } 189 } 190 191 pub fn request( 192 &self, 193 method: &str, 194 params: Option<Value>, 195 ) -> Result<Value, Box<dyn std::error::Error>> { 196 self.request_with_timeout(method, params, Duration::from_secs(30)) 197 } 198 199 pub fn request_with_timeout( 200 &self, 201 method: &str, 202 params: Option<Value>, 203 duration: Duration, 204 ) -> Result<Value, Box<dyn std::error::Error>> { 205 let id = REQUEST_ID.fetch_add(1, Ordering::SeqCst); 206 207 let request = JsonRpcRequest { 208 jsonrpc: "2.0".to_string(), 209 id, 210 method: method.to_string(), 211 params, 212 }; 213 214 self.send_message(&serde_json::to_value(&request)?)?; 215 216 let start = Instant::now(); 217 loop { 218 { 219 let mut responses = self.pending_responses.write().unwrap(); 220 if let Some(response) = responses.remove(&id) { 221 if let Some(error) = response.get("error") { 222 return Err(format!("LSP Error: {:?}", error).into()); 223 } 224 return Ok(response.get("result").cloned().unwrap_or(Value::Null)); 225 } 226 } 227 228 if start.elapsed() > duration { 229 return Err(format!("Request '{}' timed out after {:?}", method, duration).into()); 230 } 231 232 thread::sleep(Duration::from_millis(10)); 233 } 234 } 235 236 pub fn notify( 237 &self, 238 method: &str, 239 params: Option<Value>, 240 ) -> Result<(), Box<dyn std::error::Error>> { 241 let notification = json!({ 242 "jsonrpc": "2.0", 243 "method": method, 244 "params": params 245 }); 246 self.send_message(¬ification) 247 } 248 249 fn send_message(&self, message: &Value) -> Result<(), Box<dyn std::error::Error>> { 250 let content = serde_json::to_string(message)?; 251 let header = format!("Content-Length: {}\r\n\r\n", content.len()); 252 253 let mut stdin = self.stdin.lock().unwrap(); 254 stdin.write_all(header.as_bytes())?; 255 stdin.write_all(content.as_bytes())?; 256 stdin.flush()?; 257 258 Ok(()) 259 } 260 261 #[allow(dead_code)] 262 pub fn get_notifications(&self) -> Vec<JsonRpcNotification> { 263 self.notifications.read().unwrap().clone() 264 } 265 266 pub fn get_notifications_by_method(&self, method: &str) -> Vec<JsonRpcNotification> { 267 self.notifications 268 .read() 269 .unwrap() 270 .iter() 271 .filter(|n| n.method == method) 272 .cloned() 273 .collect() 274 } 275 276 pub fn clear_notifications(&self) { 277 self.notifications.write().unwrap().clear(); 278 } 279 280 pub fn wait_for_notification( 281 &self, 282 method: &str, 283 timeout_duration: Duration, 284 ) -> Option<JsonRpcNotification> { 285 let start = Instant::now(); 286 loop { 287 let notifications = self.get_notifications_by_method(method); 288 if let Some(n) = notifications.last() { 289 return Some(n.clone()); 290 } 291 if start.elapsed() > timeout_duration { 292 return None; 293 } 294 thread::sleep(Duration::from_millis(50)); 295 } 296 } 297 298 pub fn initialize(&self) -> Result<Value, Box<dyn std::error::Error>> { 299 let init_params = json!({ 300 "processId": std::process::id(), 301 "rootUri": format!("file://{}", self.workspace_root.display()), 302 "rootPath": self.workspace_root.display().to_string(), 303 "capabilities": { 304 "textDocument": { 305 "hover": { "contentFormat": ["markdown", "plaintext"] }, 306 "completion": { 307 "completionItem": { 308 "snippetSupport": true, 309 "documentationFormat": ["markdown", "plaintext"] 310 } 311 }, 312 "definition": {}, 313 "references": {}, 314 "rename": { "prepareSupport": true }, 315 "publishDiagnostics": {} 316 }, 317 "workspace": { 318 "didChangeWatchedFiles": { "dynamicRegistration": true } 319 } 320 } 321 }); 322 323 let result = self.request("initialize", Some(init_params))?; 324 325 self.notify("initialized", Some(json!({})))?; 326 327 thread::sleep(Duration::from_millis(500)); 328 329 Ok(result) 330 } 331 332 pub fn shutdown(&self) -> Result<(), Box<dyn std::error::Error>> { 333 self.request("shutdown", None)?; 334 self.notify("exit", None)?; 335 Ok(()) 336 } 337 338 pub fn open_document( 339 &self, 340 uri: &str, 341 language_id: &str, 342 text: &str, 343 ) -> Result<(), Box<dyn std::error::Error>> { 344 self.notify( 345 "textDocument/didOpen", 346 Some(json!({ 347 "textDocument": { 348 "uri": uri, 349 "languageId": language_id, 350 "version": 1, 351 "text": text 352 } 353 })), 354 )?; 355 self.open_documents 356 .lock() 357 .unwrap() 358 .insert(uri.to_string(), text.to_string()); 359 Ok(()) 360 } 361 362 pub fn change_document( 363 &self, 364 uri: &str, 365 version: i32, 366 text: &str, 367 ) -> Result<(), Box<dyn std::error::Error>> { 368 let mut docs = self.open_documents.lock().unwrap(); 369 let previous = docs.get(uri).cloned().unwrap_or_default(); 370 let end = full_document_end_position_utf16(&previous); 371 372 self.notify( 373 "textDocument/didChange", 374 Some(json!({ 375 "textDocument": { 376 "uri": uri, 377 "version": version 378 }, 379 "contentChanges": [{ 380 "range": { 381 "start": { "line": 0, "character": 0 }, 382 "end": { "line": end.0, "character": end.1 } 383 }, 384 "text": text 385 }] 386 })), 387 )?; 388 389 docs.insert(uri.to_string(), text.to_string()); 390 Ok(()) 391 } 392 393 pub fn close_document(&self, uri: &str) -> Result<(), Box<dyn std::error::Error>> { 394 self.notify( 395 "textDocument/didClose", 396 Some(json!({ 397 "textDocument": { "uri": uri } 398 })), 399 )?; 400 self.open_documents.lock().unwrap().remove(uri); 401 Ok(()) 402 } 403 404 pub fn hover( 405 &self, 406 uri: &str, 407 line: u32, 408 character: u32, 409 ) -> Result<Value, Box<dyn std::error::Error>> { 410 self.request( 411 "textDocument/hover", 412 Some(json!({ 413 "textDocument": { "uri": uri }, 414 "position": { "line": line, "character": character } 415 })), 416 ) 417 } 418 419 pub fn completion( 420 &self, 421 uri: &str, 422 line: u32, 423 character: u32, 424 ) -> Result<Value, Box<dyn std::error::Error>> { 425 self.request( 426 "textDocument/completion", 427 Some(json!({ 428 "textDocument": { "uri": uri }, 429 "position": { "line": line, "character": character } 430 })), 431 ) 432 } 433 434 pub fn definition( 435 &self, 436 uri: &str, 437 line: u32, 438 character: u32, 439 ) -> Result<Value, Box<dyn std::error::Error>> { 440 self.request( 441 "textDocument/definition", 442 Some(json!({ 443 "textDocument": { "uri": uri }, 444 "position": { "line": line, "character": character } 445 })), 446 ) 447 } 448 449 pub fn references( 450 &self, 451 uri: &str, 452 line: u32, 453 character: u32, 454 include_declaration: bool, 455 ) -> Result<Value, Box<dyn std::error::Error>> { 456 self.request( 457 "textDocument/references", 458 Some(json!({ 459 "textDocument": { "uri": uri }, 460 "position": { "line": line, "character": character }, 461 "context": { "includeDeclaration": include_declaration } 462 })), 463 ) 464 } 465 466 pub fn prepare_rename( 467 &self, 468 uri: &str, 469 line: u32, 470 character: u32, 471 ) -> Result<Value, Box<dyn std::error::Error>> { 472 self.request( 473 "textDocument/prepareRename", 474 Some(json!({ 475 "textDocument": { "uri": uri }, 476 "position": { "line": line, "character": character } 477 })), 478 ) 479 } 480 481 pub fn rename( 482 &self, 483 uri: &str, 484 line: u32, 485 character: u32, 486 new_name: &str, 487 ) -> Result<Value, Box<dyn std::error::Error>> { 488 self.request( 489 "textDocument/rename", 490 Some(json!({ 491 "textDocument": { "uri": uri }, 492 "position": { "line": line, "character": character }, 493 "newName": new_name 494 })), 495 ) 496 } 497 498 pub fn execute_command( 499 &self, 500 command: &str, 501 arguments: Vec<Value>, 502 ) -> Result<Value, Box<dyn std::error::Error>> { 503 self.request( 504 "workspace/executeCommand", 505 Some(json!({ 506 "command": command, 507 "arguments": arguments 508 })), 509 ) 510 } 511 512 pub fn workspace_symbol(&self, query: &str) -> Result<Value, Box<dyn std::error::Error>> { 513 self.request( 514 "workspace/symbol", 515 Some(json!({ 516 "query": query 517 })), 518 ) 519 } 520 521 /// Request inlay hints for a range within a document 522 pub fn inlay_hint( 523 &self, 524 uri: &str, 525 start_line: u32, 526 start_character: u32, 527 end_line: u32, 528 end_character: u32, 529 ) -> Result<Value, Box<dyn std::error::Error>> { 530 self.request( 531 "textDocument/inlayHint", 532 Some(json!({ 533 "textDocument": { "uri": uri }, 534 "range": { 535 "start": { "line": start_line, "character": start_character }, 536 "end": { "line": end_line, "character": end_character } 537 } 538 })), 539 ) 540 } 541 } 542 543 fn full_document_end_position_utf16(text: &str) -> (u32, u32) { 544 let mut line = 0u32; 545 let mut col_utf16 = 0u32; 546 for ch in text.chars() { 547 if ch == '\n' { 548 line += 1; 549 col_utf16 = 0; 550 } else { 551 col_utf16 += ch.len_utf16() as u32; 552 } 553 } 554 (line, col_utf16) 555 }