1use 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#[derive(Debug)]
21pub struct LspQueryTool {
22 lsp: Arc<Mutex<LspWrapper>>,
24}
25
26impl LspQueryTool {
27 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;