Skip to main content

rust_analyzer_mcp/lsp/
handlers.rs

1use anyhow::Result;
2use log::info;
3use serde_json::{json, Value};
4
5use super::client::RustAnalyzerClient;
6
7impl RustAnalyzerClient {
8    pub async fn hover(&mut self, uri: &str, line: u32, character: u32) -> Result<Value> {
9        let params = json!({
10            "textDocument": { "uri": uri },
11            "position": { "line": line, "character": character }
12        });
13
14        self.send_request("textDocument/hover", Some(params)).await
15    }
16
17    pub async fn definition(&mut self, uri: &str, line: u32, character: u32) -> Result<Value> {
18        let params = json!({
19            "textDocument": { "uri": uri },
20            "position": { "line": line, "character": character }
21        });
22
23        self.send_request("textDocument/definition", Some(params))
24            .await
25    }
26
27    pub async fn references(&mut self, uri: &str, line: u32, character: u32) -> Result<Value> {
28        let params = json!({
29            "textDocument": { "uri": uri },
30            "position": { "line": line, "character": character },
31            "context": { "includeDeclaration": true }
32        });
33
34        self.send_request("textDocument/references", Some(params))
35            .await
36    }
37
38    pub async fn completion(&mut self, uri: &str, line: u32, character: u32) -> Result<Value> {
39        let params = json!({
40            "textDocument": { "uri": uri },
41            "position": { "line": line, "character": character }
42        });
43
44        self.send_request("textDocument/completion", Some(params))
45            .await
46    }
47
48    pub async fn document_symbols(&mut self, uri: &str) -> Result<Value> {
49        let params = json!({
50            "textDocument": { "uri": uri }
51        });
52
53        self.send_request("textDocument/documentSymbol", Some(params))
54            .await
55    }
56
57    pub async fn formatting(&mut self, uri: &str) -> Result<Value> {
58        let params = json!({
59            "textDocument": { "uri": uri },
60            "options": {
61                "tabSize": 4,
62                "insertSpaces": true
63            }
64        });
65
66        self.send_request("textDocument/formatting", Some(params))
67            .await
68    }
69
70    pub async fn diagnostics(&mut self, uri: &str) -> Result<Value> {
71        // First check if we have stored diagnostics from publishDiagnostics.
72        let diag_lock = self.diagnostics.lock().await;
73        info!("Looking for diagnostics for URI: {}", uri);
74        info!(
75            "Available URIs with diagnostics: {:?}",
76            diag_lock.keys().collect::<Vec<_>>()
77        );
78        if let Some(diags) = diag_lock.get(uri) {
79            info!("Found {} stored diagnostics for {}", diags.len(), uri);
80            return Ok(json!(diags));
81        }
82        drop(diag_lock);
83
84        info!("No stored diagnostics for {}, trying pull model", uri);
85        // If no stored diagnostics, try the pull model as fallback.
86        let params = json!({
87            "textDocument": { "uri": uri }
88        });
89
90        let response = self
91            .send_request("textDocument/diagnostic", Some(params))
92            .await?;
93
94        // Extract diagnostics from the response.
95        if let Some(items) = response.get("items") {
96            Ok(items.clone())
97        } else {
98            Ok(json!([]))
99        }
100    }
101
102    pub async fn workspace_diagnostics(&mut self) -> Result<Value> {
103        // Try workspace/diagnostic if available, otherwise collect from all open documents.
104        let params = json!({
105            "identifier": "rust-analyzer",
106            "previousResultId": null
107        });
108
109        match self
110            .send_request("workspace/diagnostic", Some(params))
111            .await
112        {
113            Ok(response) => Ok(response),
114            // A dead rust-analyzer must not pass for a clean workspace.
115            Err(e) if self.is_gone() => Err(e),
116            Err(_) => {
117                // Fallback: return diagnostics for all open documents.
118                let mut all_diagnostics = json!({});
119                let open_docs = self.open_documents.lock().await.clone();
120
121                for doc_uri in open_docs.iter() {
122                    if let Ok(diag) = self.diagnostics(doc_uri).await {
123                        all_diagnostics[doc_uri] = diag;
124                    }
125                }
126
127                Ok(all_diagnostics)
128            }
129        }
130    }
131
132    pub async fn code_actions(
133        &mut self,
134        uri: &str,
135        start_line: u32,
136        start_char: u32,
137        end_line: u32,
138        end_char: u32,
139    ) -> Result<Value> {
140        // First, try to get diagnostics for this range.
141        let diagnostics = self.diagnostics(uri).await.unwrap_or(json!([]));
142
143        // Filter diagnostics to only those in the requested range.
144        let filtered_diagnostics = filter_diagnostics_in_range(&diagnostics, start_line, end_line);
145
146        let params = json!({
147            "textDocument": { "uri": uri },
148            "range": {
149                "start": { "line": start_line, "character": start_char },
150                "end": { "line": end_line, "character": end_char }
151            },
152            "context": {
153                "diagnostics": filtered_diagnostics,
154                "only": ["quickfix", "refactor", "refactor.extract", "refactor.inline", "refactor.rewrite", "source"]
155            }
156        });
157
158        self.send_request("textDocument/codeAction", Some(params))
159            .await
160    }
161}
162
163fn filter_diagnostics_in_range(diagnostics: &Value, start_line: u32, end_line: u32) -> Value {
164    let Some(diag_array) = diagnostics.as_array() else {
165        return json!([]);
166    };
167
168    let filtered: Vec<Value> = diag_array
169        .iter()
170        .filter(|d| {
171            let Some(range) = d.get("range") else {
172                return false;
173            };
174            let Some(start) = range.get("start") else {
175                return false;
176            };
177            let Some(end) = range.get("end") else {
178                return false;
179            };
180
181            let diag_start_line = start.get("line").and_then(|l| l.as_u64()).unwrap_or(0) as u32;
182            let diag_end_line = end.get("line").and_then(|l| l.as_u64()).unwrap_or(0) as u32;
183
184            // Check if diagnostic overlaps with requested range.
185            diag_start_line <= end_line && diag_end_line >= start_line
186        })
187        .cloned()
188        .collect();
189
190    json!(filtered)
191}