Skip to main content

opendev_tools_impl/
lsp_query.rs

1//! LSP query tool — wraps LspWrapper methods for definition, references, hover, and symbols.
2
3use std::collections::HashMap;
4use std::path::PathBuf;
5use std::sync::Arc;
6
7use async_trait::async_trait;
8use tokio::sync::Mutex;
9
10use opendev_tools_core::{BaseTool, ToolContext, ToolDisplayMeta, ToolResult};
11use opendev_tools_lsp::LspWrapper;
12
13/// Tool that queries language servers for code intelligence.
14///
15/// Supports four actions:
16/// - `definition` — go to definition of a symbol at a position
17/// - `references` — find all references to a symbol at a position
18/// - `hover` — get hover information (type info, docs) for a symbol at a position
19/// - `symbols` — list all symbols in a file (document symbols)
20#[derive(Debug)]
21pub struct LspQueryTool {
22    /// Shared LSP wrapper (behind a mutex because methods take `&mut self`).
23    lsp: Arc<Mutex<LspWrapper>>,
24}
25
26impl LspQueryTool {
27    /// Create a new LSP query tool with the given LSP wrapper.
28    pub fn new(lsp: Arc<Mutex<LspWrapper>>) -> Self {
29        Self { lsp }
30    }
31}
32
33#[async_trait]
34impl BaseTool for LspQueryTool {
35    fn name(&self) -> &str {
36        "lsp_query"
37    }
38
39    fn description(&self) -> &str {
40        "Query a language server for code intelligence. Supports actions: \
41         \"definition\" (go to definition), \"references\" (find all references), \
42         \"hover\" (type/doc info), and \"symbols\" (list document symbols)."
43    }
44
45    fn parameter_schema(&self) -> serde_json::Value {
46        serde_json::json!({
47            "type": "object",
48            "properties": {
49                "action": {
50                    "type": "string",
51                    "enum": ["definition", "references", "hover", "symbols"],
52                    "description": "The LSP action to perform"
53                },
54                "file_path": {
55                    "type": "string",
56                    "description": "Path to the file to query"
57                },
58                "line": {
59                    "type": "number",
60                    "description": "0-based line number (required for definition, references, hover)"
61                },
62                "character": {
63                    "type": "number",
64                    "description": "0-based character offset (required for definition, references, hover)"
65                },
66                "query": {
67                    "type": "string",
68                    "description": "Symbol name filter for the symbols action (optional)"
69                }
70            },
71            "required": ["action", "file_path"]
72        })
73    }
74
75    async fn execute(
76        &self,
77        args: HashMap<String, serde_json::Value>,
78        ctx: &ToolContext,
79    ) -> ToolResult {
80        let action = match args.get("action").and_then(|v| v.as_str()) {
81            Some(a) => a,
82            None => return ToolResult::fail("Missing required parameter: action"),
83        };
84
85        let file_path_str = match args.get("file_path").and_then(|v| v.as_str()) {
86            Some(p) => p,
87            None => return ToolResult::fail("Missing required parameter: file_path"),
88        };
89
90        let file_path = PathBuf::from(file_path_str);
91        let file_path = if file_path.is_relative() {
92            ctx.working_dir.join(&file_path)
93        } else {
94            file_path
95        };
96
97        let workspace_root = ctx.working_dir.clone();
98
99        let line = args.get("line").and_then(|v| v.as_u64()).map(|v| v as u32);
100
101        let character = args
102            .get("character")
103            .and_then(|v| v.as_u64())
104            .map(|v| v as u32);
105
106        let mut lsp = self.lsp.lock().await;
107
108        match action {
109            "definition" => {
110                let (line, character) = match (line, character) {
111                    (Some(l), Some(c)) => (l, c),
112                    _ => {
113                        return ToolResult::fail(
114                            "Parameters 'line' and 'character' are required for the 'definition' action",
115                        );
116                    }
117                };
118
119                match lsp
120                    .goto_definition(&file_path, line, character, &workspace_root)
121                    .await
122                {
123                    Ok(locations) => {
124                        if locations.is_empty() {
125                            return ToolResult::ok("No definition found at the given position.");
126                        }
127                        let mut output = format!("Found {} definition(s):\n\n", locations.len());
128                        for loc in &locations {
129                            output.push_str(&format!(
130                                "  {}:{}:{}\n",
131                                loc.file_path.display(),
132                                loc.range.start.line + 1,
133                                loc.range.start.character + 1,
134                            ));
135                        }
136                        ToolResult::ok(output)
137                    }
138                    Err(e) => ToolResult::fail(format!("definition request failed: {e}")),
139                }
140            }
141
142            "references" => {
143                let (line, character) = match (line, character) {
144                    (Some(l), Some(c)) => (l, c),
145                    _ => {
146                        return ToolResult::fail(
147                            "Parameters 'line' and 'character' are required for the 'references' action",
148                        );
149                    }
150                };
151
152                match lsp
153                    .find_references(&file_path, line, character, &workspace_root)
154                    .await
155                {
156                    Ok(locations) => {
157                        if locations.is_empty() {
158                            return ToolResult::ok("No references found at the given position.");
159                        }
160                        let mut output = format!("Found {} reference(s):\n\n", locations.len());
161                        for loc in &locations {
162                            output.push_str(&format!(
163                                "  {}:{}:{}\n",
164                                loc.file_path.display(),
165                                loc.range.start.line + 1,
166                                loc.range.start.character + 1,
167                            ));
168                        }
169                        let mut metadata = HashMap::new();
170                        metadata.insert("count".to_string(), serde_json::json!(locations.len()));
171                        ToolResult::ok_with_metadata(output, metadata)
172                    }
173                    Err(e) => ToolResult::fail(format!("references request failed: {e}")),
174                }
175            }
176
177            "hover" => {
178                let (line, character) = match (line, character) {
179                    (Some(l), Some(c)) => (l, c),
180                    _ => {
181                        return ToolResult::fail(
182                            "Parameters 'line' and 'character' are required for the 'hover' action",
183                        );
184                    }
185                };
186
187                match lsp
188                    .hover(&file_path, line, character, &workspace_root)
189                    .await
190                {
191                    Ok(Some(text)) => ToolResult::ok(text),
192                    Ok(None) => {
193                        ToolResult::ok("No hover information available at the given position.")
194                    }
195                    Err(e) => ToolResult::fail(format!("hover request failed: {e}")),
196                }
197            }
198
199            "symbols" => {
200                let query_filter = args.get("query").and_then(|v| v.as_str());
201
202                match lsp.document_symbols(&file_path, &workspace_root).await {
203                    Ok(symbols) => {
204                        let filtered: Vec<_> = if let Some(q) = query_filter {
205                            let q_lower = q.to_lowercase();
206                            symbols
207                                .into_iter()
208                                .filter(|s| s.name.to_lowercase().contains(&q_lower))
209                                .collect()
210                        } else {
211                            symbols
212                        };
213
214                        if filtered.is_empty() {
215                            let msg = if let Some(q) = query_filter {
216                                format!(
217                                    "No symbols matching '{}' found in {}",
218                                    q,
219                                    file_path.display()
220                                )
221                            } else {
222                                format!("No symbols found in {}", file_path.display())
223                            };
224                            return ToolResult::ok(msg);
225                        }
226
227                        let mut output = format!(
228                            "Found {} symbol(s) in {}:\n\n",
229                            filtered.len(),
230                            file_path.display()
231                        );
232                        for sym in &filtered {
233                            let container = sym
234                                .container_name
235                                .as_deref()
236                                .map(|c| format!(" (in {c})"))
237                                .unwrap_or_default();
238                            output.push_str(&format!(
239                                "  {} ({}){}  — line {}\n",
240                                sym.name,
241                                sym.kind.display_name(),
242                                container,
243                                sym.range.start.line + 1,
244                            ));
245                        }
246
247                        let mut metadata = HashMap::new();
248                        metadata.insert("count".to_string(), serde_json::json!(filtered.len()));
249                        ToolResult::ok_with_metadata(output, metadata)
250                    }
251                    Err(e) => ToolResult::fail(format!("document symbols request failed: {e}")),
252                }
253            }
254
255            _ => ToolResult::fail(format!(
256                "Unknown action '{}'. Valid actions: definition, references, hover, symbols",
257                action
258            )),
259        }
260    }
261
262    fn display_meta(&self) -> Option<ToolDisplayMeta> {
263        Some(ToolDisplayMeta {
264            verb: "LSP",
265            label: "query",
266            category: "Symbol",
267            primary_arg_keys: &["action", "file_path"],
268        })
269    }
270}
271
272#[cfg(test)]
273#[path = "lsp_query_tests.rs"]
274mod tests;