Skip to main content

rust_analyzer_mcp/lsp/
client.rs

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    /// Whether rust-analyzer last reported itself quiescent, i.e. with no background work such
35    /// as loading the workspace in flight. Fed by its `experimental/serverStatus` notifications.
36    pub(super) quiescent: watch::Sender<bool>,
37    /// Open documents whose `didSave` has been sent, see [`Self::open_document`].
38    pub(super) saved_documents: HashSet<String>,
39    /// The task reading rust-analyzer's stdout; it finishing means rust-analyzer is gone.
40    pub(super) reader: Option<JoinHandle<()>>,
41}
42
43impl RustAnalyzerClient {
44    pub fn new(workspace_root: PathBuf) -> Self {
45        // Ensure the workspace root is absolute.
46        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        // Clear any existing diagnostics from previous sessions.
78        self.diagnostics.lock().await.clear();
79
80        // Find rust-analyzer executable.
81        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            // So that a start failing halfway cannot leave an orphaned rust-analyzer behind.
90            .kill_on_drop(true);
91
92        // Pass through isolation environment variables if they're set.
93        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        // Start connection handlers, with a pending-request map of their own: the reader of an
123        // earlier process fails whatever is left in its map when it finishes.
124        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        // Initialize LSP.
136        self.initialize().await?;
137        self.initialized = true;
138
139        // Send workspace/didChangeConfiguration to ensure settings are applied.
140        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(&notification)?;
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        // Register the response channel before writing the request: a response arriving
207        // between the write and the registration would be dropped by the reader task,
208        // turning into a spurious request timeout.
209        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        // Wait for response with timeout. The channel only closes unanswered when the reader
228        // task gave up on rust-analyzer's stdout, i.e. rust-analyzer is gone.
229        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                // Unregister so an abandoned request cannot leak its pending entry.
233                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                // Opt into `experimental/serverStatus` notifications, which report whether
311                // rust-analyzer is quiescent.
312                "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        // Request workspace reload to trigger cargo check.
323        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        // A didSave makes rust-analyzer run cargo check for the document's package. It has to
351        // wait until rust-analyzer is quiescent, though: during a workspace load the freshly
352        // opened document has no source root yet, and rust-analyzer's didSave handler then panics
353        // and takes the whole process down (seen with 1.97 and 1.98). So hold it back while busy
354        // and send it on the document's next use instead; in the meantime the workspace-wide
355        // cargo check rust-analyzer runs on its own once quiescent covers the document anyway.
356        // The flag is only a snapshot, so this narrows the window rather than closing it.
357        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        // Drop the diagnostics stored so far, so that what gets reported next comes from the cargo
366        // check this didSave triggers rather than from before it.
367        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        // Give rust-analyzer time to get cargo check going.
378        tokio::time::sleep(Duration::from_millis(DOCUMENT_OPEN_DELAY_MILLIS)).await;
379
380        Ok(())
381    }
382
383    /// Shuts rust-analyzer down, attempting the graceful LSP handshake first.
384    pub async fn shutdown(&mut self) -> Result<()> {
385        if self.initialized {
386            // Bound the handshake so a wedged rust-analyzer cannot stall the shutdown.
387            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    /// Kills rust-analyzer immediately, without the LSP shutdown handshake.
402    ///
403    /// Meant for when a graceful [`Self::shutdown`] was aborted, so the process must not be left
404    /// behind.
405    pub async fn force_kill(&mut self) {
406        if let Some(mut process) = self.process.take() {
407            // Kill the process and wait for it to actually exit.
408            let _ = process.kill().await;
409            let _ = process.wait().await;
410        }
411
412        // Clear open documents and diagnostics.
413        self.open_documents.lock().await.clear();
414        self.saved_documents.clear();
415        self.diagnostics.lock().await.clear();
416        self.initialized = false;
417    }
418
419    /// Whether rust-analyzer is gone, i.e. its stdout has closed because it exited or is about to.
420    pub fn is_gone(&self) -> bool {
421        self.reader.as_ref().is_some_and(JoinHandle::is_finished)
422    }
423
424    /// The exit status of the rust-analyzer process, if it has exited.
425    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        // Try common installation locations if not in PATH.
433        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        // The shell lives until its stdin closes, then exits with 3.
499        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        // Must not fall back to an empty, i.e. clean-looking, report.
547        assert!(client.workspace_diagnostics().await.is_err());
548    }
549
550    /// A client whose "rust-analyzer" is a `cat` process, so that everything the client writes
551    /// to its stdin can be read back from the child's stdout. Starts out non-quiescent, like a
552    /// freshly started rust-analyzer.
553    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    /// Closes the client's stdin and returns everything it wrote.
569    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}