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::{hash_map::DefaultHasher, HashMap, HashSet},
6    hash::{Hash, Hasher},
7    path::PathBuf,
8    process::{ExitStatus, Stdio},
9    sync::Arc,
10    time::Duration,
11};
12use tokio::{
13    io::BufWriter,
14    process::{Child, Command},
15    sync::{oneshot, watch, Mutex},
16    task::JoinHandle,
17};
18
19use crate::{
20    config::{
21        DOCUMENT_OPEN_DELAY_MILLIS, GRACEFUL_SHUTDOWN_TIMEOUT_SECS, LSP_REQUEST_TIMEOUT_SECS,
22    },
23    protocol::lsp::LSPRequest,
24    uri,
25};
26
27use super::connection::{send_message, Connection, Diagnostics, Flycheck, Outgoing, Pending};
28
29pub struct RustAnalyzerClient {
30    pub(super) process: Option<Child>,
31    pub(super) request_id: Arc<Mutex<u64>>,
32    pub(super) workspace_root: PathBuf,
33    pub(super) stdin: Option<Outgoing<tokio::process::ChildStdin>>,
34    pub(super) pending_requests: Pending,
35    pub(super) initialized: bool,
36    /// What rust-analyzer was last told about each open document, keyed by normalized URI.
37    pub(super) open_documents: Arc<Mutex<HashMap<String, OpenDocument>>>,
38    pub(super) diagnostics: Diagnostics,
39    /// Whether rust-analyzer last reported itself quiescent, i.e. with no background work such
40    /// as loading the workspace in flight. Fed by its `experimental/serverStatus` notifications.
41    pub(super) quiescent: watch::Sender<bool>,
42    /// The cargo checks rust-analyzer has run, fed by its `$/progress` notifications.
43    pub(super) flycheck: watch::Sender<Flycheck>,
44    /// Whether waiting for a cargo check has already been given up on once. Ones older than
45    /// the reports never report a check, and waiting on a report that is not coming would cost
46    /// every diagnostics call the whole timeout -- but this only holds while no check has been
47    /// reported at all, so one that turns up later puts the waiting back.
48    pub(super) gave_up_on_checks: bool,
49    /// Open documents whose `didSave` has been sent, see [`Self::open_document`].
50    pub(super) saved_documents: HashSet<String>,
51    /// The task reading rust-analyzer's stdout; it finishing means rust-analyzer is gone.
52    pub(super) reader: Option<JoinHandle<()>>,
53    /// What rust-analyzer was started with, and is handed again whenever it asks for its
54    /// configuration.
55    pub(super) settings: Value,
56}
57
58impl RustAnalyzerClient {
59    pub fn new(workspace_root: PathBuf, settings: Value) -> Self {
60        let workspace_root = uri::absolute(&workspace_root);
61
62        Self {
63            process: None,
64            request_id: Arc::new(Mutex::new(1)),
65            workspace_root,
66            stdin: None,
67            pending_requests: Arc::new(Mutex::new(HashMap::new())),
68            initialized: false,
69            open_documents: Arc::new(Mutex::new(HashMap::new())),
70            diagnostics: Arc::new(Mutex::new(HashMap::new())),
71            quiescent: watch::channel(false).0,
72            flycheck: watch::channel(Flycheck::default()).0,
73            gave_up_on_checks: false,
74            saved_documents: HashSet::new(),
75            reader: None,
76            settings,
77        }
78    }
79
80    pub async fn start(&mut self) -> Result<()> {
81        info!(
82            "Starting rust-analyzer process in workspace: {}",
83            self.workspace_root.display()
84        );
85        // rust-analyzer says nothing about settings it does not recognise, so a mistyped one is
86        // only ever findable here.
87        info!("rust-analyzer settings: {}", self.settings);
88
89        // Clear any existing diagnostics from previous sessions.
90        self.diagnostics.lock().await.clear();
91
92        // Find rust-analyzer executable.
93        let rust_analyzer_path = find_rust_analyzer()?;
94        info!("Using rust-analyzer at: {}", rust_analyzer_path.display());
95
96        let mut cmd = Command::new(rust_analyzer_path);
97        cmd.current_dir(&self.workspace_root)
98            .stdin(Stdio::piped())
99            .stdout(Stdio::piped())
100            .stderr(Stdio::piped())
101            // So that a start failing halfway cannot leave an orphaned rust-analyzer behind.
102            .kill_on_drop(true);
103
104        // Pass through isolation environment variables if they're set.
105        if let Ok(cache_home) = std::env::var("XDG_CACHE_HOME") {
106            cmd.env("XDG_CACHE_HOME", cache_home);
107        }
108        if let Ok(target_dir) = std::env::var("CARGO_TARGET_DIR") {
109            cmd.env("CARGO_TARGET_DIR", target_dir);
110        }
111        if let Ok(tmpdir) = std::env::var("TMPDIR") {
112            cmd.env("TMPDIR", tmpdir);
113        }
114
115        let mut child = cmd
116            .spawn()
117            .map_err(|e| anyhow!("Failed to start rust-analyzer: {}", e))?;
118
119        let stdin = child
120            .stdin
121            .take()
122            .ok_or_else(|| anyhow!("Failed to get stdin"))?;
123        let stdout = child
124            .stdout
125            .take()
126            .ok_or_else(|| anyhow!("Failed to get stdout"))?;
127        let stderr = child
128            .stderr
129            .take()
130            .ok_or_else(|| anyhow!("Failed to get stderr"))?;
131
132        let stdin = Arc::new(Mutex::new(BufWriter::new(stdin)));
133        self.stdin = Some(Arc::clone(&stdin));
134
135        // Start connection handlers, with a pending-request map of their own: the reader of an
136        // earlier process fails whatever is left in its map when it finishes. It writes to
137        // rust-analyzer as well as reading from it, since rust-analyzer's own requests are its
138        // to answer.
139        self.pending_requests = Arc::new(Mutex::new(HashMap::new()));
140        self.reader = Some(super::connection::start_handlers(
141            stdout,
142            stderr,
143            Connection {
144                pending_requests: Arc::clone(&self.pending_requests),
145                diagnostics: Arc::clone(&self.diagnostics),
146                quiescent: self.quiescent.clone(),
147                flycheck: self.flycheck.clone(),
148                outgoing: stdin,
149                settings: self.settings.clone(),
150            },
151        ));
152
153        self.process = Some(child);
154
155        // Initialize LSP.
156        self.initialize().await?;
157        self.initialized = true;
158
159        // Tell rust-analyzer its configuration changed. It ignores what comes with the
160        // notification and asks for the settings itself, which the reader task answers.
161        let config_params = json!({ "settings": { "rust-analyzer": self.settings } });
162        let _ = self
163            .send_notification("workspace/didChangeConfiguration", Some(config_params))
164            .await;
165
166        info!("rust-analyzer client started and initialized");
167        Ok(())
168    }
169
170    pub(super) async fn send_notification(
171        &mut self,
172        method: &str,
173        params: Option<Value>,
174    ) -> Result<()> {
175        let notification = json!({
176            "jsonrpc": "2.0",
177            "method": method,
178            "params": params.unwrap_or(json!({}))
179        });
180
181        info!("Sending LSP notification: {}", method);
182
183        let Some(stdin) = &self.stdin else {
184            return Err(anyhow!("No stdin available"));
185        };
186
187        send_message(stdin, &notification).await
188    }
189
190    /// Asks rust-analyzer something and waits for its answer.
191    ///
192    /// A request whose analysis is superseded while rust-analyzer is working on it comes back
193    /// refused rather than answered -- that is what "content modified" means -- and every
194    /// notification this server sends can do that to a request in flight. It means ask again.
195    pub(super) async fn send_request(
196        &mut self,
197        method: &str,
198        params: Option<Value>,
199    ) -> Result<Value> {
200        for attempt in 1..REQUEST_ATTEMPTS {
201            let answer = self.send_request_once(method, params.clone()).await;
202            let Err(e) = &answer else {
203                return answer;
204            };
205            if !superseded(e) {
206                return answer;
207            }
208
209            info!(
210                "Asking rust-analyzer for {} again ({}): {}",
211                method, attempt, e
212            );
213        }
214
215        self.send_request_once(method, params).await
216    }
217
218    async fn send_request_once(&mut self, method: &str, params: Option<Value>) -> Result<Value> {
219        let mut request_id_lock = self.request_id.lock().await;
220        let id = *request_id_lock;
221        *request_id_lock += 1;
222        drop(request_id_lock);
223
224        let request = LSPRequest {
225            jsonrpc: "2.0".to_string(),
226            id,
227            method: method.to_string(),
228            params: params.clone(),
229        };
230
231        let request = serde_json::to_value(request)?;
232
233        info!("Sending LSP request: {} with params: {:?}", method, params);
234
235        // Register the response channel before writing the request: a response arriving
236        // between the write and the registration would be dropped by the reader task,
237        // turning into a spurious request timeout.
238        let (tx, rx) = oneshot::channel();
239        let pending_requests = self.pending_requests.clone();
240        pending_requests.lock().await.insert(id, tx);
241
242        let Some(stdin) = &self.stdin else {
243            pending_requests.lock().await.remove(&id);
244            return Err(anyhow!("No stdin available"));
245        };
246
247        if let Err(e) = send_message(stdin, &request).await {
248            pending_requests.lock().await.remove(&id);
249            return Err(e);
250        }
251
252        // Wait for response with timeout. The channel only closes unanswered when the reader
253        // task gave up on rust-analyzer's stdout, i.e. rust-analyzer is gone.
254        match tokio::time::timeout(Duration::from_secs(LSP_REQUEST_TIMEOUT_SECS), rx).await {
255            Ok(Ok(answer)) => answer.map_err(|message| anyhow!("{message}")),
256            Ok(Err(_)) => Err(anyhow!("rust-analyzer exited before responding")),
257            Err(_) => {
258                // Unregister so an abandoned request cannot leak its pending entry.
259                pending_requests.lock().await.remove(&id);
260                Err(anyhow!("Request timeout"))
261            }
262        }
263    }
264
265    async fn initialize(&mut self) -> Result<()> {
266        let init_params = json!({
267            "processId": std::process::id(),
268            "rootUri": uri::path_to_uri(&self.workspace_root)?,
269            "initializationOptions": self.settings,
270            "capabilities": {
271                "textDocument": {
272                    "hover": {
273                        "contentFormat": ["markdown", "plaintext"]
274                    },
275                    "completion": {
276                        "completionItem": {
277                            "snippetSupport": true
278                        }
279                    },
280                    "definition": {
281                        "linkSupport": true
282                    },
283                    "references": {},
284                    "documentSymbol": {},
285                    "codeAction": {
286                        "codeActionLiteralSupport": {
287                            "codeActionKind": {
288                                "valueSet": [
289                                    "quickfix",
290                                    "refactor",
291                                    "refactor.extract",
292                                    "refactor.inline",
293                                    "refactor.rewrite",
294                                    "source",
295                                    "source.organizeImports"
296                                ]
297                            }
298                        },
299                        "resolveSupport": {
300                            "properties": ["edit"]
301                        }
302                    },
303                    "publishDiagnostics": {
304                        "relatedInformation": true,
305                        "tagSupport": {
306                            "valueSet": [1, 2]
307                        }
308                    },
309                    "formatting": {},
310                    "rename": {
311                        "dynamicRegistration": false,
312                        "prepareSupport": true
313                    }
314                },
315                "workspace": {
316                    "didChangeConfiguration": {
317                        "dynamicRegistration": false
318                    },
319                    // Renaming a module renames its file, which rust-analyzer refuses to work
320                    // out at all for a client that has not said it understands file operations.
321                    "workspaceEdit": {
322                        "documentChanges": true,
323                        "resourceOperations": ["create", "rename", "delete"],
324                        "failureHandling": "abort"
325                    }
326                },
327                // The default, stated outright because every position in every result is counted
328                // this way and the tools say so.
329                "general": {
330                    "positionEncodings": ["utf-16"]
331                },
332                // Opt into the progress reports rust-analyzer gives on its background work,
333                // the cargo checks above all. It stays silent about all of it otherwise.
334                "window": {
335                    "workDoneProgress": true
336                },
337                // Opt into `experimental/serverStatus` notifications, which report whether
338                // rust-analyzer is quiescent.
339                "experimental": {
340                    "serverStatusNotification": true
341                }
342            }
343        });
344
345        self.send_request("initialize", Some(init_params)).await?;
346        self.send_notification("initialized", Some(json!({})))
347            .await?;
348
349        // Request workspace reload to trigger cargo check.
350        self.send_request("rust-analyzer/reloadWorkspace", None)
351            .await
352            .ok();
353
354        Ok(())
355    }
356
357    /// Tells rust-analyzer about `content`, opening the document or updating it as needed.
358    ///
359    /// An open document's content belongs to us for as long as it stays open: rust-analyzer
360    /// refuses to re-read one from disk, so an edit anyone else makes is invisible to it until
361    /// this sends the new content along. Every request for a document that has been edited since
362    /// it was opened -- which, with an agent at the other end, is most of them -- was answered
363    /// from the content it had when it was first looked at.
364    pub async fn open_document(&mut self, uri: &str, content: &str) -> Result<()> {
365        let key = uri::normalize(uri);
366        let content_hash = hash(content);
367        let known = self
368            .open_documents
369            .lock()
370            .await
371            .get(&key)
372            .map(|document| (document.version, document.content_hash));
373
374        match known {
375            None => {
376                info!("Opening document: {}", uri);
377                let params = json!({
378                    "textDocument": {
379                        "uri": uri,
380                        "languageId": "rust",
381                        "version": FIRST_DOCUMENT_VERSION,
382                        "text": content
383                    }
384                });
385                self.send_notification("textDocument/didOpen", Some(params))
386                    .await?;
387                self.open_documents.lock().await.insert(
388                    key.clone(),
389                    OpenDocument {
390                        version: FIRST_DOCUMENT_VERSION,
391                        content_hash,
392                    },
393                );
394            }
395            Some((_, known_hash)) if known_hash == content_hash => {
396                info!("Document already open and unchanged: {}", uri);
397            }
398            Some((version, _)) => {
399                // Whole-document changes are what the LSP calls a content change with no range,
400                // and what rust-analyzer's handler looks for first. Sending the file as one is
401                // both simpler and safer than working out a diff nobody asked us for.
402                let version = version + 1;
403                info!("Document changed, sending version {} of {}", version, uri);
404                let params = json!({
405                    "textDocument": {
406                        "uri": uri,
407                        "version": version
408                    },
409                    "contentChanges": [{ "text": content }]
410                });
411                self.send_notification("textDocument/didChange", Some(params))
412                    .await?;
413                self.open_documents.lock().await.insert(
414                    key.clone(),
415                    OpenDocument {
416                        version,
417                        content_hash,
418                    },
419                );
420
421                // Whatever was reported about the content just replaced is no longer about
422                // anything: drop it, and let the didSave below ask for a check of what is there
423                // now.
424                self.diagnostics.lock().await.remove(&key);
425                self.saved_documents.remove(&key);
426            }
427        }
428
429        // A didSave makes rust-analyzer run cargo check for the document's package. It has to
430        // wait until rust-analyzer is quiescent, though: during a workspace load the freshly
431        // opened document has no source root yet, and rust-analyzer's didSave handler then panics
432        // and takes the whole process down (seen with 1.97 and 1.98). So hold it back while busy
433        // and send it on the document's next use instead; in the meantime the workspace-wide
434        // cargo check rust-analyzer runs on its own once quiescent covers the document anyway.
435        // The flag is only a snapshot, so this narrows the window rather than closing it.
436        if self.saved_documents.contains(&key) {
437            return Ok(());
438        }
439        if !*self.quiescent.borrow() {
440            info!("rust-analyzer is busy, holding back didSave for {}", uri);
441            return Ok(());
442        }
443
444        // Drop the diagnostics stored so far, so that what gets reported next comes from the cargo
445        // check this didSave triggers rather than from before it.
446        self.diagnostics.lock().await.remove(&key);
447        let save_params = json!({
448            "textDocument": {
449                "uri": uri
450            }
451        });
452        self.send_notification("textDocument/didSave", Some(save_params))
453            .await?;
454        self.saved_documents.insert(key);
455
456        // Give rust-analyzer time to get cargo check going.
457        tokio::time::sleep(Duration::from_millis(DOCUMENT_OPEN_DELAY_MILLIS)).await;
458
459        Ok(())
460    }
461
462    /// Tells rust-analyzer to stop taking this document's content from us.
463    ///
464    /// What is on disk becomes the truth about it again, which for a file that is no longer
465    /// there means it stops existing rather than lingering in rust-analyzer as it last was.
466    pub async fn close_document(&mut self, uri: &str) -> Result<()> {
467        let key = uri::normalize(uri);
468        if self.open_documents.lock().await.remove(&key).is_none() {
469            return Ok(());
470        }
471
472        info!("Closing document: {}", uri);
473        let params = json!({ "textDocument": { "uri": uri } });
474        self.send_notification("textDocument/didClose", Some(params))
475            .await?;
476
477        self.saved_documents.remove(&key);
478        self.diagnostics.lock().await.remove(&key);
479
480        Ok(())
481    }
482
483    /// The documents rust-analyzer has been told about, by URI.
484    pub async fn open_document_uris(&self) -> Vec<String> {
485        self.open_documents.lock().await.keys().cloned().collect()
486    }
487
488    /// Shuts rust-analyzer down, attempting the graceful LSP handshake first.
489    pub async fn shutdown(&mut self) -> Result<()> {
490        if self.initialized {
491            // Bound the handshake so a wedged rust-analyzer cannot stall the shutdown.
492            let handshake = async {
493                let _ = self.send_request("shutdown", None).await;
494                let _ = self.send_notification("exit", None).await;
495            };
496            let timeout = Duration::from_secs(GRACEFUL_SHUTDOWN_TIMEOUT_SECS);
497            if tokio::time::timeout(timeout, handshake).await.is_err() {
498                info!("Graceful shutdown timed out");
499            }
500        }
501
502        self.force_kill().await;
503        Ok(())
504    }
505
506    /// Kills rust-analyzer immediately, without the LSP shutdown handshake.
507    ///
508    /// Meant for when a graceful [`Self::shutdown`] was aborted, so the process must not be left
509    /// behind.
510    pub async fn force_kill(&mut self) {
511        if let Some(mut process) = self.process.take() {
512            // Kill the process and wait for it to actually exit.
513            let _ = process.kill().await;
514            let _ = process.wait().await;
515        }
516
517        // Clear open documents and diagnostics.
518        self.open_documents.lock().await.clear();
519        self.saved_documents.clear();
520        self.diagnostics.lock().await.clear();
521        self.flycheck.send_replace(Flycheck::default());
522        self.gave_up_on_checks = false;
523        self.initialized = false;
524    }
525
526    /// Whether rust-analyzer is gone, i.e. its stdout has closed because it exited or is about to.
527    pub fn is_gone(&self) -> bool {
528        self.reader.as_ref().is_some_and(JoinHandle::is_finished)
529    }
530
531    /// The exit status of the rust-analyzer process, if it has exited.
532    pub fn exit_status(&mut self) -> Option<ExitStatus> {
533        self.process.as_mut()?.try_wait().ok().flatten()
534    }
535}
536
537/// What rust-analyzer was last told about a document, so that the next thing it is told about
538/// it can follow on.
539pub(super) struct OpenDocument {
540    /// The version last sent. rust-analyzer wants these to climb, and echoes the current one
541    /// back with every diagnostic it publishes.
542    version: u64,
543    /// Fingerprint of the content last sent, which is how an edit is told from a re-read. A
544    /// hash rather than the content itself: an agent works its way through a lot of files, and
545    /// nothing here needs the old text back.
546    content_hash: u64,
547}
548
549/// How many times to ask for something rust-analyzer abandoned mid-request.
550const REQUEST_ATTEMPTS: u32 = 3;
551
552/// Whether rust-analyzer abandoned a request because what it was working from changed under it.
553fn superseded(error: &anyhow::Error) -> bool {
554    error
555        .to_string()
556        .to_lowercase()
557        .contains("content modified")
558}
559
560/// The version a document is opened at, which every later change counts up from.
561const FIRST_DOCUMENT_VERSION: u64 = 1;
562
563fn hash(content: &str) -> u64 {
564    let mut hasher = DefaultHasher::new();
565    content.hash(&mut hasher);
566    hasher.finish()
567}
568
569fn find_rust_analyzer() -> Result<PathBuf> {
570    which::which("rust-analyzer").or_else(|_| {
571        // Try common installation locations if not in PATH.
572        let home = std::env::var("HOME").unwrap_or_else(|_| String::from("~"));
573        let cargo_bin = PathBuf::from(home).join(".cargo/bin/rust-analyzer");
574        if cargo_bin.exists() {
575            Ok(cargo_bin)
576        } else {
577            which::which("rust-analyzer")
578        }
579    })
580    .map_err(|e| {
581        anyhow!(
582            "Failed to find rust-analyzer in PATH or ~/.cargo/bin: {}. Please ensure rust-analyzer is installed.",
583            e
584        )
585    })
586}
587
588#[cfg(test)]
589mod tests {
590    use super::*;
591
592    // Some of these stand a real process up in rust-analyzer's place, which takes shell tooling
593    // this repository only assumes on Unix.
594    #[cfg(unix)]
595    use tokio::io::AsyncReadExt;
596
597    #[cfg(unix)]
598    const URI: &str = "file:///tmp/lib.rs";
599
600    /// The progress token rust-analyzer reports a workspace's cargo checks under.
601    #[cfg(unix)]
602    const FLYCHECK: &str = "rust-analyzer/flycheck/0";
603
604    #[cfg(unix)]
605    #[tokio::test]
606    async fn did_save_is_held_back_while_rust_analyzer_is_busy() {
607        let (mut client, mut child) = client_with_fake_stdin();
608
609        open(&mut client).await;
610        open(&mut client).await;
611
612        let sent = written(&mut client, &mut child).await;
613        assert_eq!(sent.matches("textDocument/didOpen").count(), 1, "{sent}");
614        assert_eq!(sent.matches("textDocument/didSave").count(), 0, "{sent}");
615    }
616
617    #[cfg(unix)]
618    #[tokio::test]
619    async fn held_back_did_save_is_sent_once_rust_analyzer_is_quiescent() {
620        let (mut client, mut child) = client_with_fake_stdin();
621
622        open(&mut client).await;
623        client.quiescent.send_replace(true);
624        open(&mut client).await;
625        open(&mut client).await;
626
627        let sent = written(&mut client, &mut child).await;
628        assert_eq!(sent.matches("textDocument/didOpen").count(), 1, "{sent}");
629        assert_eq!(sent.matches("textDocument/didSave").count(), 1, "{sent}");
630    }
631
632    #[cfg(unix)]
633    #[tokio::test]
634    async fn did_save_follows_did_open_while_rust_analyzer_is_quiescent() {
635        let (mut client, mut child) = client_with_fake_stdin();
636        client.quiescent.send_replace(true);
637
638        open(&mut client).await;
639        open(&mut client).await;
640
641        let sent = written(&mut client, &mut child).await;
642        assert_eq!(sent.matches("textDocument/didOpen").count(), 1, "{sent}");
643        assert_eq!(sent.matches("textDocument/didSave").count(), 1, "{sent}");
644    }
645
646    #[cfg(unix)]
647    #[tokio::test]
648    async fn an_edited_document_is_sent_as_a_change() {
649        let (mut client, mut child) = client_with_fake_stdin();
650        client.quiescent.send_replace(true);
651
652        client.open_document(URI, "fn main() {}").await.unwrap();
653        client
654            .open_document(URI, "fn main() { let x = 1; }")
655            .await
656            .unwrap();
657
658        let sent = written(&mut client, &mut child).await;
659        assert_eq!(sent.matches("textDocument/didOpen").count(), 1, "{sent}");
660        assert_eq!(sent.matches("textDocument/didChange").count(), 1, "{sent}");
661        assert!(sent.contains(r#"let x = 1;"#), "{sent}");
662        // The version climbs, which is what rust-analyzer stamps its diagnostics with.
663        assert!(sent.contains(r#""version":2"#), "{sent}");
664    }
665
666    #[cfg(unix)]
667    #[tokio::test]
668    async fn a_document_that_did_not_change_is_not_sent_again() {
669        // rust-analyzer drops a change whose text it already has, so the only thing sending one
670        // achieves is a check that never reports anything back.
671        let (mut client, mut child) = client_with_fake_stdin();
672        client.quiescent.send_replace(true);
673
674        open(&mut client).await;
675        open(&mut client).await;
676
677        let sent = written(&mut client, &mut child).await;
678        assert_eq!(sent.matches("textDocument/didOpen").count(), 1, "{sent}");
679        assert_eq!(sent.matches("textDocument/didChange").count(), 0, "{sent}");
680    }
681
682    #[cfg(unix)]
683    #[tokio::test]
684    async fn every_edit_gets_the_next_version() {
685        let (mut client, mut child) = client_with_fake_stdin();
686        client.quiescent.send_replace(true);
687
688        for content in ["fn main() {}", "fn main() { 1; }", "fn main() { 2; }"] {
689            client.open_document(URI, content).await.unwrap();
690        }
691
692        let sent = written(&mut client, &mut child).await;
693        assert!(sent.contains(r#""version":1"#), "{sent}");
694        assert!(sent.contains(r#""version":2"#), "{sent}");
695        assert!(sent.contains(r#""version":3"#), "{sent}");
696    }
697
698    #[cfg(unix)]
699    #[tokio::test]
700    async fn an_edit_drops_what_was_reported_about_the_old_content() {
701        let (mut client, mut child) = client_with_fake_stdin();
702        client.quiescent.send_replace(true);
703
704        client.open_document(URI, "fn main() {}").await.unwrap();
705        client
706            .diagnostics
707            .lock()
708            .await
709            .insert(URI.to_string(), vec![json!({ "message": "stale" })]);
710        client
711            .open_document(URI, "fn main() { let x = 1; }")
712            .await
713            .unwrap();
714
715        assert!(!client.diagnostics.lock().await.contains_key(URI));
716        // And the check that reports on the new content is asked for again.
717        let sent = written(&mut client, &mut child).await;
718        assert_eq!(sent.matches("textDocument/didSave").count(), 2, "{sent}");
719    }
720
721    #[cfg(unix)]
722    #[tokio::test(start_paused = true)]
723    async fn diagnostics_are_asked_for_and_waited_on() {
724        let (mut client, mut child) = client_with_fake_stdin();
725        client.quiescent.send_replace(true);
726
727        // Stand in for rust-analyzer: report a check starting and finishing, with something to
728        // say about the file, once the client is waiting for one.
729        let flycheck = client.flycheck.clone();
730        let diagnostics = Arc::clone(&client.diagnostics);
731        tokio::spawn(async move {
732            tokio::time::sleep(Duration::from_millis(50)).await;
733            flycheck.send_modify(|flycheck| flycheck.begin(FLYCHECK));
734            diagnostics.lock().await.insert(
735                URI.to_string(),
736                vec![json!({ "message": "mismatched types" })],
737            );
738            flycheck.send_modify(|flycheck| flycheck.end(FLYCHECK));
739        });
740
741        let fresh = client.fresh_diagnostics(URI).await.unwrap();
742
743        assert!(fresh.complete);
744        assert_eq!(fresh.items[0]["message"], "mismatched types");
745        let sent = written(&mut client, &mut child).await;
746        assert!(sent.contains("rust-analyzer/runFlycheck"), "{sent}");
747        // And rust-analyzer's own analysis is asked for rather than waited for.
748        assert!(sent.contains("textDocument/diagnostic"), "{sent}");
749    }
750
751    #[cfg(unix)]
752    #[tokio::test(start_paused = true)]
753    async fn diagnostics_come_back_marked_when_no_check_runs() {
754        // What an older rust-analyzer, or one with checking switched off, leaves us with: no
755        // check to wait for, so what is already known is the best there is.
756        let (mut client, _child) = client_with_fake_stdin();
757        client.quiescent.send_replace(true);
758
759        let fresh = client.fresh_diagnostics(URI).await.unwrap();
760
761        assert!(!fresh.complete);
762        assert_eq!(fresh.items, json!([]));
763        // And having learnt that, it does not wait the same wait out again.
764        assert!(client.gave_up_on_checks);
765    }
766
767    #[cfg(unix)]
768    #[tokio::test(start_paused = true)]
769    async fn a_check_reported_late_puts_the_waiting_back() {
770        // Giving up on a rust-analyzer that reports no checks must not outlast a check turning
771        // up: on a workspace big enough, the first one begins after the wait has been given up.
772        let (mut client, _child) = client_with_fake_stdin();
773        client.quiescent.send_replace(true);
774        client.fresh_diagnostics(URI).await.unwrap();
775        assert!(client.gave_up_on_checks);
776
777        client.flycheck.send_modify(|flycheck| {
778            flycheck.begin(FLYCHECK);
779            flycheck.end(FLYCHECK);
780        });
781
782        // So this call waits again, and the check it asks for is waited out.
783        let flycheck = client.flycheck.clone();
784        tokio::spawn(async move {
785            tokio::time::sleep(Duration::from_millis(50)).await;
786            flycheck.send_modify(|flycheck| flycheck.begin(FLYCHECK));
787            flycheck.send_modify(|flycheck| flycheck.end(FLYCHECK));
788        });
789
790        let fresh = client.fresh_diagnostics(URI).await.unwrap();
791
792        assert!(fresh.complete);
793    }
794
795    #[cfg(unix)]
796    #[tokio::test(start_paused = true)]
797    async fn diagnostics_from_a_workspace_still_loading_are_marked() {
798        // A file rust-analyzer has not reached yet looks exactly like a file with nothing wrong
799        // with it, so an unqualified "no errors" here would be a lie.
800        let (mut client, _child) = client_with_fake_stdin();
801        let flycheck = client.flycheck.clone();
802        tokio::spawn(async move {
803            tokio::time::sleep(Duration::from_millis(50)).await;
804            flycheck.send_modify(|flycheck| flycheck.begin(FLYCHECK));
805            flycheck.send_modify(|flycheck| flycheck.end(FLYCHECK));
806        });
807
808        let fresh = client.fresh_diagnostics(URI).await.unwrap();
809
810        assert!(!fresh.complete);
811    }
812
813    #[cfg(unix)]
814    #[tokio::test]
815    async fn exit_status_reflects_whether_rust_analyzer_is_alive() {
816        let mut client = RustAnalyzerClient::new(PathBuf::from("."), json!({}));
817        // The shell lives until its stdin closes, then exits with 3.
818        let mut child = Command::new("sh")
819            .args(["-c", "read _; exit 3"])
820            .stdin(Stdio::piped())
821            .spawn()
822            .unwrap();
823        let stdin = child.stdin.take();
824        client.process = Some(child);
825
826        assert!(client.exit_status().is_none());
827
828        drop(stdin);
829        client.process.as_mut().unwrap().wait().await.unwrap();
830        assert_eq!(
831            client.exit_status().and_then(|status| status.code()),
832            Some(3)
833        );
834    }
835
836    #[tokio::test]
837    async fn is_gone_once_rust_analyzer_closes_its_stdout() {
838        let mut client = RustAnalyzerClient::new(PathBuf::from("."), json!({}));
839        let (stdout, rust_analyzer) = tokio::io::duplex(64);
840        client.reader = Some(super::super::connection::start_handlers(
841            stdout,
842            tokio::io::empty(),
843            Connection {
844                pending_requests: Arc::clone(&client.pending_requests),
845                diagnostics: Arc::clone(&client.diagnostics),
846                quiescent: client.quiescent.clone(),
847                flycheck: client.flycheck.clone(),
848                outgoing: Arc::new(Mutex::new(BufWriter::new(tokio::io::sink()))),
849                settings: json!({}),
850            },
851        ));
852        tokio::task::yield_now().await;
853        assert!(!client.is_gone());
854
855        drop(rust_analyzer);
856        tokio::time::timeout(Duration::from_secs(5), client.reader.as_mut().unwrap())
857            .await
858            .expect("reader must finish once stdout closes")
859            .unwrap();
860        assert!(client.is_gone());
861    }
862
863    #[tokio::test]
864    async fn workspace_diagnostics_fails_once_rust_analyzer_is_gone() {
865        let mut client = RustAnalyzerClient::new(PathBuf::from("."), json!({}));
866        let mut reader = tokio::spawn(async {});
867        (&mut reader).await.unwrap();
868        client.reader = Some(reader);
869
870        // Must not fall back to an empty, i.e. clean-looking, report.
871        assert!(client.workspace_diagnostics().await.is_err());
872    }
873
874    /// A client whose "rust-analyzer" is a `cat` process, so that everything the client writes
875    /// to its stdin can be read back from the child's stdout. Starts out non-quiescent, like a
876    /// freshly started rust-analyzer.
877    #[cfg(unix)]
878    fn client_with_fake_stdin() -> (RustAnalyzerClient, Child) {
879        let mut child = Command::new("cat")
880            .stdin(Stdio::piped())
881            .stdout(Stdio::piped())
882            .spawn()
883            .unwrap();
884        let mut client = RustAnalyzerClient::new(PathBuf::from("."), json!({}));
885        client.stdin = Some(Arc::new(Mutex::new(BufWriter::new(
886            child.stdin.take().unwrap(),
887        ))));
888        (client, child)
889    }
890
891    #[cfg(unix)]
892    async fn open(client: &mut RustAnalyzerClient) {
893        client.open_document(URI, "fn main() {}").await.unwrap();
894    }
895
896    /// Closes the client's stdin and returns everything it wrote.
897    #[cfg(unix)]
898    async fn written(client: &mut RustAnalyzerClient, child: &mut Child) -> String {
899        client.stdin.take();
900        let mut output = String::new();
901        child
902            .stdout
903            .take()
904            .unwrap()
905            .read_to_string(&mut output)
906            .await
907            .unwrap();
908        child.wait().await.unwrap();
909        output
910    }
911}