1use anyhow::{anyhow, Result};
2use log::info;
3use serde_json::{json, Value};
4use std::{
5 collections::{HashMap, HashSet},
6 path::PathBuf,
7 process::{ExitStatus, Stdio},
8 sync::Arc,
9 time::Duration,
10};
11use tokio::{
12 io::{AsyncWriteExt, BufWriter},
13 process::{Child, Command},
14 sync::{oneshot, watch, Mutex},
15 task::JoinHandle,
16};
17
18use crate::{
19 config::{
20 DOCUMENT_OPEN_DELAY_MILLIS, GRACEFUL_SHUTDOWN_TIMEOUT_SECS, LSP_REQUEST_TIMEOUT_SECS,
21 },
22 protocol::lsp::LSPRequest,
23};
24
25pub struct RustAnalyzerClient {
26 pub(super) process: Option<Child>,
27 pub(super) request_id: Arc<Mutex<u64>>,
28 pub(super) workspace_root: PathBuf,
29 pub(super) stdin: Option<BufWriter<tokio::process::ChildStdin>>,
30 pub(super) pending_requests: Arc<Mutex<HashMap<u64, oneshot::Sender<Value>>>>,
31 pub(super) initialized: bool,
32 pub(super) open_documents: Arc<Mutex<HashSet<String>>>,
33 pub(super) diagnostics: Arc<Mutex<HashMap<String, Vec<Value>>>>,
34 pub(super) quiescent: watch::Sender<bool>,
37 pub(super) saved_documents: HashSet<String>,
39 pub(super) reader: Option<JoinHandle<()>>,
41}
42
43impl RustAnalyzerClient {
44 pub fn new(workspace_root: PathBuf) -> Self {
45 let workspace_root = workspace_root.canonicalize().unwrap_or_else(|_| {
47 if workspace_root.is_absolute() {
48 workspace_root.clone()
49 } else {
50 std::env::current_dir()
51 .unwrap_or_else(|_| PathBuf::from("."))
52 .join(&workspace_root)
53 }
54 });
55
56 Self {
57 process: None,
58 request_id: Arc::new(Mutex::new(1)),
59 workspace_root,
60 stdin: None,
61 pending_requests: Arc::new(Mutex::new(HashMap::new())),
62 initialized: false,
63 open_documents: Arc::new(Mutex::new(HashSet::new())),
64 diagnostics: Arc::new(Mutex::new(HashMap::new())),
65 quiescent: watch::channel(false).0,
66 saved_documents: HashSet::new(),
67 reader: None,
68 }
69 }
70
71 pub async fn start(&mut self) -> Result<()> {
72 info!(
73 "Starting rust-analyzer process in workspace: {}",
74 self.workspace_root.display()
75 );
76
77 self.diagnostics.lock().await.clear();
79
80 let rust_analyzer_path = find_rust_analyzer()?;
82 info!("Using rust-analyzer at: {}", rust_analyzer_path.display());
83
84 let mut cmd = Command::new(rust_analyzer_path);
85 cmd.current_dir(&self.workspace_root)
86 .stdin(Stdio::piped())
87 .stdout(Stdio::piped())
88 .stderr(Stdio::piped())
89 .kill_on_drop(true);
91
92 if let Ok(cache_home) = std::env::var("XDG_CACHE_HOME") {
94 cmd.env("XDG_CACHE_HOME", cache_home);
95 }
96 if let Ok(target_dir) = std::env::var("CARGO_TARGET_DIR") {
97 cmd.env("CARGO_TARGET_DIR", target_dir);
98 }
99 if let Ok(tmpdir) = std::env::var("TMPDIR") {
100 cmd.env("TMPDIR", tmpdir);
101 }
102
103 let mut child = cmd
104 .spawn()
105 .map_err(|e| anyhow!("Failed to start rust-analyzer: {}", e))?;
106
107 let stdin = child
108 .stdin
109 .take()
110 .ok_or_else(|| anyhow!("Failed to get stdin"))?;
111 let stdout = child
112 .stdout
113 .take()
114 .ok_or_else(|| anyhow!("Failed to get stdout"))?;
115 let stderr = child
116 .stderr
117 .take()
118 .ok_or_else(|| anyhow!("Failed to get stderr"))?;
119
120 self.stdin = Some(BufWriter::new(stdin));
121
122 self.pending_requests = Arc::new(Mutex::new(HashMap::new()));
125 self.reader = Some(super::connection::start_handlers(
126 stdout,
127 stderr,
128 Arc::clone(&self.pending_requests),
129 Arc::clone(&self.diagnostics),
130 self.quiescent.clone(),
131 ));
132
133 self.process = Some(child);
134
135 self.initialize().await?;
137 self.initialized = true;
138
139 let config_params = json!({
141 "settings": {
142 "rust-analyzer": {
143 "checkOnSave": {
144 "enable": true,
145 "command": "check",
146 "allTargets": true
147 }
148 }
149 }
150 });
151 let _ = self
152 .send_notification("workspace/didChangeConfiguration", Some(config_params))
153 .await;
154
155 info!("rust-analyzer client started and initialized");
156 Ok(())
157 }
158
159 pub(super) async fn send_notification(
160 &mut self,
161 method: &str,
162 params: Option<Value>,
163 ) -> Result<()> {
164 let notification = json!({
165 "jsonrpc": "2.0",
166 "method": method,
167 "params": params.unwrap_or(json!({}))
168 });
169
170 let content = serde_json::to_string(¬ification)?;
171 let message = format!("Content-Length: {}\r\n\r\n{}", content.len(), content);
172
173 info!("Sending LSP notification: {}", method);
174
175 let Some(stdin) = &mut self.stdin else {
176 return Err(anyhow!("No stdin available"));
177 };
178
179 stdin.write_all(message.as_bytes()).await?;
180 stdin.flush().await?;
181 Ok(())
182 }
183
184 pub(super) async fn send_request(
185 &mut self,
186 method: &str,
187 params: Option<Value>,
188 ) -> Result<Value> {
189 let mut request_id_lock = self.request_id.lock().await;
190 let id = *request_id_lock;
191 *request_id_lock += 1;
192 drop(request_id_lock);
193
194 let request = LSPRequest {
195 jsonrpc: "2.0".to_string(),
196 id,
197 method: method.to_string(),
198 params: params.clone(),
199 };
200
201 let content = serde_json::to_string(&request)?;
202 let message = format!("Content-Length: {}\r\n\r\n{}", content.len(), content);
203
204 info!("Sending LSP request: {} with params: {:?}", method, params);
205
206 let (tx, rx) = oneshot::channel();
210 let pending_requests = self.pending_requests.clone();
211 pending_requests.lock().await.insert(id, tx);
212
213 let Some(stdin) = &mut self.stdin else {
214 pending_requests.lock().await.remove(&id);
215 return Err(anyhow!("No stdin available"));
216 };
217
218 let mut written = stdin.write_all(message.as_bytes()).await;
219 if written.is_ok() {
220 written = stdin.flush().await;
221 }
222 if let Err(e) = written {
223 pending_requests.lock().await.remove(&id);
224 return Err(e.into());
225 }
226
227 match tokio::time::timeout(Duration::from_secs(LSP_REQUEST_TIMEOUT_SECS), rx).await {
230 Ok(response) => response.map_err(|_| anyhow!("rust-analyzer exited before responding")),
231 Err(_) => {
232 pending_requests.lock().await.remove(&id);
234 Err(anyhow!("Request timeout"))
235 }
236 }
237 }
238
239 async fn initialize(&mut self) -> Result<()> {
240 let init_params = json!({
241 "processId": std::process::id(),
242 "rootUri": format!("file://{}", self.workspace_root.display()),
243 "initializationOptions": {
244 "cargo": {
245 "buildScripts": {
246 "enable": true
247 }
248 },
249 "checkOnSave": {
250 "enable": true,
251 "command": "check",
252 "allTargets": true
253 },
254 "diagnostics": {
255 "enable": true,
256 "experimental": {
257 "enable": true
258 }
259 },
260 "procMacro": {
261 "enable": true
262 }
263 },
264 "capabilities": {
265 "textDocument": {
266 "hover": {
267 "contentFormat": ["markdown", "plaintext"]
268 },
269 "completion": {
270 "completionItem": {
271 "snippetSupport": true
272 }
273 },
274 "definition": {
275 "linkSupport": true
276 },
277 "references": {},
278 "documentSymbol": {},
279 "codeAction": {
280 "codeActionLiteralSupport": {
281 "codeActionKind": {
282 "valueSet": [
283 "quickfix",
284 "refactor",
285 "refactor.extract",
286 "refactor.inline",
287 "refactor.rewrite",
288 "source",
289 "source.organizeImports"
290 ]
291 }
292 },
293 "resolveSupport": {
294 "properties": ["edit"]
295 }
296 },
297 "publishDiagnostics": {
298 "relatedInformation": true,
299 "tagSupport": {
300 "valueSet": [1, 2]
301 }
302 },
303 "formatting": {}
304 },
305 "workspace": {
306 "didChangeConfiguration": {
307 "dynamicRegistration": false
308 }
309 },
310 "experimental": {
313 "serverStatusNotification": true
314 }
315 }
316 });
317
318 self.send_request("initialize", Some(init_params)).await?;
319 self.send_notification("initialized", Some(json!({})))
320 .await?;
321
322 self.send_request("rust-analyzer/reloadWorkspace", None)
324 .await
325 .ok();
326
327 Ok(())
328 }
329
330 pub async fn open_document(&mut self, uri: &str, content: &str) -> Result<()> {
331 let already_open = self.open_documents.lock().await.contains(uri);
332 if already_open {
333 info!("Document already open: {}", uri);
334 } else {
335 info!("Opening document: {}", uri);
336 let params = json!({
337 "textDocument": {
338 "uri": uri,
339 "languageId": "rust",
340 "version": 1,
341 "text": content
342 }
343 });
344 self.send_notification("textDocument/didOpen", Some(params))
345 .await?;
346
347 self.open_documents.lock().await.insert(uri.to_string());
348 }
349
350 if self.saved_documents.contains(uri) {
358 return Ok(());
359 }
360 if !*self.quiescent.borrow() {
361 info!("rust-analyzer is busy, holding back didSave for {}", uri);
362 return Ok(());
363 }
364
365 self.diagnostics.lock().await.remove(uri);
368 let save_params = json!({
369 "textDocument": {
370 "uri": uri
371 }
372 });
373 self.send_notification("textDocument/didSave", Some(save_params))
374 .await?;
375 self.saved_documents.insert(uri.to_string());
376
377 tokio::time::sleep(Duration::from_millis(DOCUMENT_OPEN_DELAY_MILLIS)).await;
379
380 Ok(())
381 }
382
383 pub async fn shutdown(&mut self) -> Result<()> {
385 if self.initialized {
386 let handshake = async {
388 let _ = self.send_request("shutdown", None).await;
389 let _ = self.send_notification("exit", None).await;
390 };
391 let timeout = Duration::from_secs(GRACEFUL_SHUTDOWN_TIMEOUT_SECS);
392 if tokio::time::timeout(timeout, handshake).await.is_err() {
393 info!("Graceful shutdown timed out");
394 }
395 }
396
397 self.force_kill().await;
398 Ok(())
399 }
400
401 pub async fn force_kill(&mut self) {
406 if let Some(mut process) = self.process.take() {
407 let _ = process.kill().await;
409 let _ = process.wait().await;
410 }
411
412 self.open_documents.lock().await.clear();
414 self.saved_documents.clear();
415 self.diagnostics.lock().await.clear();
416 self.initialized = false;
417 }
418
419 pub fn is_gone(&self) -> bool {
421 self.reader.as_ref().is_some_and(JoinHandle::is_finished)
422 }
423
424 pub fn exit_status(&mut self) -> Option<ExitStatus> {
426 self.process.as_mut()?.try_wait().ok().flatten()
427 }
428}
429
430fn find_rust_analyzer() -> Result<PathBuf> {
431 which::which("rust-analyzer").or_else(|_| {
432 let home = std::env::var("HOME").unwrap_or_else(|_| String::from("~"));
434 let cargo_bin = PathBuf::from(home).join(".cargo/bin/rust-analyzer");
435 if cargo_bin.exists() {
436 Ok(cargo_bin)
437 } else {
438 which::which("rust-analyzer")
439 }
440 })
441 .map_err(|e| {
442 anyhow!(
443 "Failed to find rust-analyzer in PATH or ~/.cargo/bin: {}. Please ensure rust-analyzer is installed.",
444 e
445 )
446 })
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use tokio::io::AsyncReadExt;
453
454 const URI: &str = "file:///tmp/lib.rs";
455
456 #[tokio::test]
457 async fn did_save_is_held_back_while_rust_analyzer_is_busy() {
458 let (mut client, mut child) = client_with_fake_stdin();
459
460 open(&mut client).await;
461 open(&mut client).await;
462
463 let sent = written(&mut client, &mut child).await;
464 assert_eq!(sent.matches("textDocument/didOpen").count(), 1, "{sent}");
465 assert_eq!(sent.matches("textDocument/didSave").count(), 0, "{sent}");
466 }
467
468 #[tokio::test]
469 async fn held_back_did_save_is_sent_once_rust_analyzer_is_quiescent() {
470 let (mut client, mut child) = client_with_fake_stdin();
471
472 open(&mut client).await;
473 client.quiescent.send_replace(true);
474 open(&mut client).await;
475 open(&mut client).await;
476
477 let sent = written(&mut client, &mut child).await;
478 assert_eq!(sent.matches("textDocument/didOpen").count(), 1, "{sent}");
479 assert_eq!(sent.matches("textDocument/didSave").count(), 1, "{sent}");
480 }
481
482 #[tokio::test]
483 async fn did_save_follows_did_open_while_rust_analyzer_is_quiescent() {
484 let (mut client, mut child) = client_with_fake_stdin();
485 client.quiescent.send_replace(true);
486
487 open(&mut client).await;
488 open(&mut client).await;
489
490 let sent = written(&mut client, &mut child).await;
491 assert_eq!(sent.matches("textDocument/didOpen").count(), 1, "{sent}");
492 assert_eq!(sent.matches("textDocument/didSave").count(), 1, "{sent}");
493 }
494
495 #[tokio::test]
496 async fn exit_status_reflects_whether_rust_analyzer_is_alive() {
497 let mut client = RustAnalyzerClient::new(PathBuf::from("."));
498 let mut child = Command::new("sh")
500 .args(["-c", "read _; exit 3"])
501 .stdin(Stdio::piped())
502 .spawn()
503 .unwrap();
504 let stdin = child.stdin.take();
505 client.process = Some(child);
506
507 assert!(client.exit_status().is_none());
508
509 drop(stdin);
510 client.process.as_mut().unwrap().wait().await.unwrap();
511 assert_eq!(
512 client.exit_status().and_then(|status| status.code()),
513 Some(3)
514 );
515 }
516
517 #[tokio::test]
518 async fn is_gone_once_rust_analyzer_closes_its_stdout() {
519 let mut client = RustAnalyzerClient::new(PathBuf::from("."));
520 let (stdout, rust_analyzer) = tokio::io::duplex(64);
521 client.reader = Some(super::super::connection::start_handlers(
522 stdout,
523 tokio::io::empty(),
524 Arc::clone(&client.pending_requests),
525 Arc::clone(&client.diagnostics),
526 client.quiescent.clone(),
527 ));
528 tokio::task::yield_now().await;
529 assert!(!client.is_gone());
530
531 drop(rust_analyzer);
532 tokio::time::timeout(Duration::from_secs(5), client.reader.as_mut().unwrap())
533 .await
534 .expect("reader must finish once stdout closes")
535 .unwrap();
536 assert!(client.is_gone());
537 }
538
539 #[tokio::test]
540 async fn workspace_diagnostics_fails_once_rust_analyzer_is_gone() {
541 let mut client = RustAnalyzerClient::new(PathBuf::from("."));
542 let mut reader = tokio::spawn(async {});
543 (&mut reader).await.unwrap();
544 client.reader = Some(reader);
545
546 assert!(client.workspace_diagnostics().await.is_err());
548 }
549
550 fn client_with_fake_stdin() -> (RustAnalyzerClient, Child) {
554 let mut child = Command::new("cat")
555 .stdin(Stdio::piped())
556 .stdout(Stdio::piped())
557 .spawn()
558 .unwrap();
559 let mut client = RustAnalyzerClient::new(PathBuf::from("."));
560 client.stdin = Some(BufWriter::new(child.stdin.take().unwrap()));
561 (client, child)
562 }
563
564 async fn open(client: &mut RustAnalyzerClient) {
565 client.open_document(URI, "fn main() {}").await.unwrap();
566 }
567
568 async fn written(client: &mut RustAnalyzerClient, child: &mut Child) -> String {
570 client.stdin.take();
571 let mut output = String::new();
572 child
573 .stdout
574 .take()
575 .unwrap()
576 .read_to_string(&mut output)
577 .await
578 .unwrap();
579 child.wait().await.unwrap();
580 output
581 }
582}