Skip to main content

rust_analyzer_mcp/lsp/
handlers.rs

1use anyhow::Result;
2use log::{info, warn};
3use serde_json::{json, Value};
4use std::time::Duration;
5use tokio::sync::watch;
6
7use super::{client::RustAnalyzerClient, connection::Flycheck};
8use crate::{
9    config::{
10        DOCUMENT_OPEN_DELAY_MILLIS, FLYCHECK_REQUEST_ATTEMPTS, FLYCHECK_START_TIMEOUT_SECS,
11        FLYCHECK_TIMEOUT_SECS, WORKSPACE_LOAD_TIMEOUT_SECS,
12    },
13    uri,
14};
15
16/// What rust-analyzer marks the diagnostics it worked out itself with, as opposed to the ones it
17/// read out of a cargo check.
18const ANALYSIS_SOURCE: &str = "rust-analyzer";
19
20/// Diagnostics, and whether anything was still going on that could add to them.
21pub struct FreshDiagnostics {
22    pub items: Value,
23    /// False when the workspace was still loading, or the cargo check did not finish or never
24    /// started -- any of which makes these the best available rather than the last word.
25    pub complete: bool,
26}
27
28/// Waits for rust-analyzer's checks to reach `condition`, giving up after `timeout`.
29async fn wait_for(
30    progress: &mut watch::Receiver<Flycheck>,
31    timeout: Duration,
32    condition: impl Fn(&Flycheck) -> bool,
33) -> bool {
34    let wait = async {
35        loop {
36            if condition(&progress.borrow_and_update()) {
37                return true;
38            }
39            // Only fails once the client is gone, and then nothing is coming.
40            if progress.changed().await.is_err() {
41                return false;
42            }
43        }
44    };
45
46    tokio::time::timeout(timeout, wait).await.unwrap_or(false)
47}
48
49impl RustAnalyzerClient {
50    pub async fn hover(&mut self, uri: &str, line: u32, character: u32) -> Result<Value> {
51        let params = json!({
52            "textDocument": { "uri": uri },
53            "position": { "line": line, "character": character }
54        });
55
56        self.send_request("textDocument/hover", Some(params)).await
57    }
58
59    pub async fn definition(&mut self, uri: &str, line: u32, character: u32) -> Result<Value> {
60        let params = json!({
61            "textDocument": { "uri": uri },
62            "position": { "line": line, "character": character }
63        });
64
65        self.send_request("textDocument/definition", Some(params))
66            .await
67    }
68
69    pub async fn references(&mut self, uri: &str, line: u32, character: u32) -> Result<Value> {
70        let params = json!({
71            "textDocument": { "uri": uri },
72            "position": { "line": line, "character": character },
73            "context": { "includeDeclaration": true }
74        });
75
76        self.send_request("textDocument/references", Some(params))
77            .await
78    }
79
80    pub async fn completion(&mut self, uri: &str, line: u32, character: u32) -> Result<Value> {
81        let params = json!({
82            "textDocument": { "uri": uri },
83            "position": { "line": line, "character": character }
84        });
85
86        self.send_request("textDocument/completion", Some(params))
87            .await
88    }
89
90    /// Asks what renaming the symbol at a position would take, without doing any of it.
91    pub async fn rename(
92        &mut self,
93        uri: &str,
94        line: u32,
95        character: u32,
96        new_name: &str,
97    ) -> Result<Value> {
98        let params = json!({
99            "textDocument": { "uri": uri },
100            "position": { "line": line, "character": character },
101            "newName": new_name
102        });
103
104        self.send_request("textDocument/rename", Some(params)).await
105    }
106
107    /// The range of the symbol a rename at this position would be about.
108    ///
109    /// Asked before a rename for two reasons: it says what is about to be renamed, which is worth
110    /// reporting back, and when there is nothing renameable there it says so more precisely than
111    /// the rename itself does.
112    pub async fn prepare_rename(&mut self, uri: &str, line: u32, character: u32) -> Result<Value> {
113        let params = json!({
114            "textDocument": { "uri": uri },
115            "position": { "line": line, "character": character }
116        });
117
118        self.send_request("textDocument/prepareRename", Some(params))
119            .await
120    }
121
122    pub async fn document_symbols(&mut self, uri: &str) -> Result<Value> {
123        let params = json!({
124            "textDocument": { "uri": uri }
125        });
126
127        self.send_request("textDocument/documentSymbol", Some(params))
128            .await
129    }
130
131    pub async fn formatting(&mut self, uri: &str) -> Result<Value> {
132        let params = json!({
133            "textDocument": { "uri": uri },
134            "options": {
135                "tabSize": 4,
136                "insertSpaces": true
137            }
138        });
139
140        self.send_request("textDocument/formatting", Some(params))
141            .await
142    }
143
144    pub async fn diagnostics(&mut self, uri: &str) -> Result<Value> {
145        // First check if we have stored diagnostics from publishDiagnostics.
146        let key = uri::normalize(uri);
147        let diag_lock = self.diagnostics.lock().await;
148        info!("Looking for diagnostics for URI: {}", key);
149        info!(
150            "Available URIs with diagnostics: {:?}",
151            diag_lock.keys().collect::<Vec<_>>()
152        );
153        if let Some(diagnostics) = diag_lock.get(&key) {
154            info!("Found {} stored diagnostics for {}", diagnostics.len(), uri);
155            return Ok(json!(diagnostics));
156        }
157        drop(diag_lock);
158
159        info!("No stored diagnostics for {}, trying pull model", uri);
160        // If no stored diagnostics, try the pull model as fallback.
161        let params = json!({
162            "textDocument": { "uri": uri }
163        });
164
165        let response = self
166            .send_request("textDocument/diagnostic", Some(params))
167            .await?;
168
169        // Extract diagnostics from the response.
170        if let Some(items) = response.get("items") {
171            Ok(items.clone())
172        } else {
173            Ok(json!([]))
174        }
175    }
176
177    /// Diagnostics for `uri` from a cargo check that has seen the file as it is now.
178    ///
179    /// The diagnostics rustc gives -- the ones anyone actually wants -- only ever arrive as the
180    /// result of a check, and a check takes as long as it takes. Asking rust-analyzer for one and
181    /// waiting for it to finish is the only way to answer with the code in front of us rather
182    /// than whatever was last reported about it.
183    pub async fn fresh_diagnostics(&mut self, uri: &str) -> Result<FreshDiagnostics> {
184        // A report on a workspace rust-analyzer is still loading covers the part of it that has
185        // been reached, and a file it has not reached looks exactly like a file with nothing
186        // wrong with it.
187        let loaded = self
188            .wait_until_loaded(Duration::from_secs(WORKSPACE_LOAD_TIMEOUT_SECS))
189            .await;
190        if !loaded {
191            warn!("rust-analyzer is still loading the workspace; reporting on what it has");
192        }
193
194        let before = self.flycheck.borrow().clone();
195        let mut progress = self.flycheck.subscribe();
196
197        // A check covers the whole workspace and republishes everything it has to say about it,
198        // so everything said before it is superseded.
199        self.diagnostics.lock().await.clear();
200
201        let params = json!({ "textDocument": { "uri": uri } });
202        self.send_notification("rust-analyzer/runFlycheck", Some(params.clone()))
203            .await?;
204
205        // Waiting was given up on before, and nothing has reported a check since: a
206        // rust-analyzer too old to report them, or told not to check at all. One that turns up
207        // after all -- a workspace big enough for the first check to start late -- makes this
208        // false again, and the waiting resumes.
209        if self.gave_up_on_checks && before.never_ran_one() {
210            return Ok(FreshDiagnostics {
211                items: self.current_diagnostics(uri).await?,
212                complete: false,
213            });
214        }
215
216        // Waiting for the check to start is a step of its own, because it may never do: the
217        // request can be dropped along with the analysis it was working from, and rust-analyzer
218        // may be old enough not to know it or be configured not to check at all. Ask again a
219        // couple of times for the first case; the second gives up quickly rather than sitting
220        // out the whole timeout every call.
221        let mut started = false;
222        for attempt in 1..=FLYCHECK_REQUEST_ATTEMPTS {
223            started = wait_for(
224                &mut progress,
225                Duration::from_secs(FLYCHECK_START_TIMEOUT_SECS),
226                |flycheck| flycheck.started_since(&before),
227            )
228            .await;
229            if started || attempt == FLYCHECK_REQUEST_ATTEMPTS {
230                break;
231            }
232
233            info!("Asking for a cargo check of {} again", uri);
234            self.send_notification("rust-analyzer/runFlycheck", Some(params.clone()))
235                .await?;
236        }
237        if !started {
238            info!("No cargo check started for {}, reporting what we have", uri);
239            // Nothing has ever reported a check here, so take it that nothing will and stop
240            // making every later call wait the same wait out. Only until one does: the check
241            // this call asked for may yet begin, and the next call will see that it did.
242            if self.flycheck.borrow().never_ran_one() {
243                warn!("rust-analyzer has yet to report a cargo check; not waiting for one");
244                self.gave_up_on_checks = true;
245            }
246
247            return Ok(FreshDiagnostics {
248                items: self.current_diagnostics(uri).await?,
249                complete: false,
250            });
251        }
252
253        let finished = wait_for(
254            &mut progress,
255            Duration::from_secs(FLYCHECK_TIMEOUT_SECS),
256            |flycheck| flycheck.caught_up_with(&before),
257        )
258        .await;
259        if !finished {
260            warn!("cargo check for {} is still running, reporting early", uri);
261        }
262
263        // The results are published just after the check reports itself done, from a turn of
264        // rust-analyzer's loop we cannot see the end of.
265        tokio::time::sleep(Duration::from_millis(DOCUMENT_OPEN_DELAY_MILLIS)).await;
266
267        Ok(FreshDiagnostics {
268            items: self.current_diagnostics(uri).await?,
269            complete: loaded && finished,
270        })
271    }
272
273    /// Waits until rust-analyzer has no loading left to do, giving up after `timeout`.
274    ///
275    /// Worth doing before anything whose answer is only right once rust-analyzer has seen the
276    /// whole workspace.
277    pub async fn wait_until_loaded(&self, timeout: Duration) -> bool {
278        let mut quiescent = self.quiescent.subscribe();
279        let wait = async {
280            loop {
281                if *quiescent.borrow_and_update() {
282                    return true;
283                }
284                if quiescent.changed().await.is_err() {
285                    return false;
286                }
287            }
288        };
289
290        tokio::time::timeout(timeout, wait).await.unwrap_or(false)
291    }
292
293    /// Everything known about `uri` as it stands: what rust-analyzer works out itself, asked for
294    /// afresh, and what the last cargo check said.
295    ///
296    /// The two halves have to be come by differently. rust-analyzer publishes its own analysis
297    /// when it gets round to it, and one worked out before a change can arrive after it -- but
298    /// the same analysis can simply be asked for, and the answer then describes the content
299    /// rust-analyzer holds rather than the content it held. Cargo's half cannot be asked for at
300    /// all: it exists only as the published results of a check, which is what the wait above is
301    /// for.
302    async fn current_diagnostics(&mut self, uri: &str) -> Result<Value> {
303        let published = self.published_diagnostics(uri).await;
304
305        let params = json!({ "textDocument": { "uri": uri } });
306        let pulled = match self
307            .send_request("textDocument/diagnostic", Some(params))
308            .await
309        {
310            Ok(pulled) => pulled,
311            // Asking is an improvement on waiting to be told, not a requirement: an older
312            // rust-analyzer, or one that has gone away, leaves the published reports as all
313            // there is.
314            Err(e) => {
315                warn!("Could not ask rust-analyzer about {}: {}", uri, e);
316                return Ok(json!(published));
317            }
318        };
319        let Some(analysed) = pulled.get("items").and_then(|items| items.as_array()) else {
320            return Ok(json!(published));
321        };
322
323        let from_cargo = published
324            .iter()
325            .filter(|diagnostic| diagnostic["source"] != ANALYSIS_SOURCE)
326            .cloned();
327
328        Ok(json!(analysed
329            .iter()
330            .cloned()
331            .chain(from_cargo)
332            .collect::<Vec<_>>()))
333    }
334
335    /// What rust-analyzer last published about `uri`, and nothing else.
336    ///
337    /// Unlike [`Self::diagnostics`] this does not fall back to asking rust-analyzer directly:
338    /// having waited for a check, no entry means the check had nothing to say about the file,
339    /// and the answer to that is an empty list rather than a request whose reply cannot contain
340    /// a cargo diagnostic anyway.
341    async fn published_diagnostics(&self, uri: &str) -> Vec<Value> {
342        self.diagnostics
343            .lock()
344            .await
345            .get(&uri::normalize(uri))
346            .cloned()
347            .unwrap_or_default()
348    }
349
350    pub async fn workspace_diagnostics(&mut self) -> Result<Value> {
351        // Try workspace/diagnostic if available, otherwise collect from all open documents.
352        let params = json!({
353            "identifier": "rust-analyzer",
354            "previousResultId": null
355        });
356
357        match self
358            .send_request("workspace/diagnostic", Some(params))
359            .await
360        {
361            Ok(response) => Ok(response),
362            // A dead rust-analyzer must not pass for a clean workspace.
363            Err(e) if self.is_gone() => Err(e),
364            Err(_) => {
365                // Fallback: return diagnostics for all open documents.
366                let mut all_diagnostics = json!({});
367                let open_docs: Vec<String> =
368                    self.open_documents.lock().await.keys().cloned().collect();
369
370                for doc_uri in open_docs.iter() {
371                    if let Ok(diag) = self.diagnostics(doc_uri).await {
372                        all_diagnostics[doc_uri] = diag;
373                    }
374                }
375
376                Ok(all_diagnostics)
377            }
378        }
379    }
380
381    pub async fn code_actions(
382        &mut self,
383        uri: &str,
384        start_line: u32,
385        start_char: u32,
386        end_line: u32,
387        end_char: u32,
388    ) -> Result<Value> {
389        // First, try to get diagnostics for this range.
390        let diagnostics = self.diagnostics(uri).await.unwrap_or(json!([]));
391
392        // Filter diagnostics to only those in the requested range.
393        let filtered_diagnostics = filter_diagnostics_in_range(&diagnostics, start_line, end_line);
394
395        let params = json!({
396            "textDocument": { "uri": uri },
397            "range": {
398                "start": { "line": start_line, "character": start_char },
399                "end": { "line": end_line, "character": end_char }
400            },
401            "context": {
402                "diagnostics": filtered_diagnostics,
403                "only": ["quickfix", "refactor", "refactor.extract", "refactor.inline", "refactor.rewrite", "source"]
404            }
405        });
406
407        self.send_request("textDocument/codeAction", Some(params))
408            .await
409    }
410}
411
412fn filter_diagnostics_in_range(diagnostics: &Value, start_line: u32, end_line: u32) -> Value {
413    let Some(diag_array) = diagnostics.as_array() else {
414        return json!([]);
415    };
416
417    let filtered: Vec<Value> = diag_array
418        .iter()
419        .filter(|d| {
420            let Some(range) = d.get("range") else {
421                return false;
422            };
423            let Some(start) = range.get("start") else {
424                return false;
425            };
426            let Some(end) = range.get("end") else {
427                return false;
428            };
429
430            let diag_start_line = start.get("line").and_then(|l| l.as_u64()).unwrap_or(0) as u32;
431            let diag_end_line = end.get("line").and_then(|l| l.as_u64()).unwrap_or(0) as u32;
432
433            // Check if diagnostic overlaps with requested range.
434            diag_start_line <= end_line && diag_end_line >= start_line
435        })
436        .cloned()
437        .collect();
438
439    json!(filtered)
440}