1use crate::lsp::{LspActionResult, LspManager, detect_language_from_path};
4
5use super::{Tool, ToolResult};
6use anyhow::Result;
7use async_trait::async_trait;
8use serde_json::{Value, json};
9use std::path::Path;
10use std::sync::Arc;
11use tokio::sync::RwLock;
12
13static LSP_MANAGER: std::sync::OnceLock<Arc<RwLock<Option<Arc<LspManager>>>>> =
15 std::sync::OnceLock::new();
16
17pub struct LspTool {
19 root_uri: Option<String>,
20}
21
22impl LspTool {
23 pub fn new() -> Self {
24 Self { root_uri: None }
25 }
26
27 pub fn with_root(root_uri: String) -> Self {
29 Self {
30 root_uri: Some(root_uri),
31 }
32 }
33
34 pub async fn shutdown_all(&self) {
36 let cell = LSP_MANAGER.get_or_init(|| Arc::new(RwLock::new(None)));
37 let guard = cell.read().await;
38 if let Some(manager) = guard.as_ref() {
39 manager.shutdown_all().await;
40 }
41 }
42
43 async fn get_manager(&self) -> Arc<LspManager> {
45 let cell = LSP_MANAGER.get_or_init(|| Arc::new(RwLock::new(None)));
46 let mut guard = cell.write().await;
47
48 if guard.is_none() {
49 *guard = Some(Arc::new(LspManager::new(self.root_uri.clone())));
50 }
51
52 Arc::clone(guard.as_ref().unwrap())
53 }
54}
55
56impl Default for LspTool {
57 fn default() -> Self {
58 Self::new()
59 }
60}
61
62#[async_trait]
63impl Tool for LspTool {
64 fn id(&self) -> &str {
65 "lsp"
66 }
67
68 fn name(&self) -> &str {
69 "LSP Tool"
70 }
71
72 fn description(&self) -> &str {
73 "Perform Language Server Protocol (LSP) operations such as go-to-definition, find-references, hover, document-symbol, workspace-symbol, and more. This tool enables AI agents to query language servers for code intelligence features. Supports rust-analyzer, typescript-language-server, pylsp, gopls, and clangd."
74 }
75
76 fn parameters(&self) -> Value {
77 json!({
78 "type": "object",
79 "properties": {
80 "action": {
81 "type": "string",
82 "description": "The LSP operation to perform",
83 "enum": [
84 "goToDefinition",
85 "findReferences",
86 "hover",
87 "documentSymbol",
88 "workspaceSymbol",
89 "goToImplementation",
90 "completion"
91 ]
92 },
93 "file_path": {
94 "type": "string",
95 "description": "The absolute or relative path to the file"
96 },
97 "line": {
98 "type": "integer",
99 "description": "The line number (1-based, as shown in editors)",
100 "minimum": 1
101 },
102 "column": {
103 "type": "integer",
104 "description": "The character offset/column (1-based, as shown in editors)",
105 "minimum": 1
106 },
107 "query": {
108 "type": "string",
109 "description": "Search query for workspaceSymbol action"
110 },
111 "include_declaration": {
112 "type": "boolean",
113 "description": "For findReferences: include the declaration in results",
114 "default": true
115 }
116 },
117 "required": ["action", "file_path"]
118 })
119 }
120
121 async fn execute(&self, args: Value) -> Result<ToolResult> {
122 let action = args["action"]
123 .as_str()
124 .ok_or_else(|| anyhow::anyhow!("action is required"))?;
125 let file_path = args["file_path"]
126 .as_str()
127 .ok_or_else(|| anyhow::anyhow!("file_path is required"))?;
128
129 let path = Path::new(file_path);
130
131 let manager = self.get_manager().await;
133
134 if action == "workspaceSymbol" {
136 let query = args["query"].as_str().unwrap_or("");
137 let language = detect_language_from_path(file_path);
138
139 let client = if let Some(lang) = language {
140 manager.get_client(lang).await?
141 } else {
142 manager.get_client("rust").await?
144 };
145
146 let result = client.workspace_symbols(query).await?;
147 return format_result(result);
148 }
149
150 let line = args["line"]
152 .as_u64()
153 .ok_or_else(|| anyhow::anyhow!("line is required for action: {}", action))?
154 as u32;
155 let column = args["column"]
156 .as_u64()
157 .ok_or_else(|| anyhow::anyhow!("column is required for action: {}", action))?
158 as u32;
159
160 let client = manager.get_client_for_file(path).await?;
161
162 let result = match action {
163 "goToDefinition" => client.go_to_definition(path, line, column).await?,
164 "findReferences" => {
165 let include_decl = args["include_declaration"].as_bool().unwrap_or(true);
166 client
167 .find_references(path, line, column, include_decl)
168 .await?
169 }
170 "hover" => client.hover(path, line, column).await?,
171 "documentSymbol" => client.document_symbols(path).await?,
172 "goToImplementation" => client.go_to_implementation(path, line, column).await?,
173 "completion" => client.completion(path, line, column).await?,
174 _ => {
175 return Ok(ToolResult::error(format!("Unknown action: {}", action)));
176 }
177 };
178
179 format_result(result)
180 }
181}
182
183fn format_result(result: LspActionResult) -> Result<ToolResult> {
185 let output = match result {
186 LspActionResult::Definition { locations } => {
187 if locations.is_empty() {
188 "No definition found".to_string()
189 } else {
190 let mut out = format!("Found {} definition(s):\n\n", locations.len());
191 for loc in locations {
192 let uri = loc.uri;
193 let range = loc.range;
194 out.push_str(&format!(
195 " {}:{}:{}\n",
196 uri.trim_start_matches("file://"),
197 range.start.line + 1,
198 range.start.character + 1
199 ));
200 }
201 out
202 }
203 }
204 LspActionResult::References { locations } => {
205 if locations.is_empty() {
206 "No references found".to_string()
207 } else {
208 let mut out = format!("Found {} reference(s):\n\n", locations.len());
209 for loc in locations {
210 let uri = loc.uri;
211 let range = loc.range;
212 out.push_str(&format!(
213 " {}:{}:{}\n",
214 uri.trim_start_matches("file://"),
215 range.start.line + 1,
216 range.start.character + 1
217 ));
218 }
219 out
220 }
221 }
222 LspActionResult::Hover { contents, range } => {
223 let mut out = "Hover information:\n\n".to_string();
224 out.push_str(&contents);
225 if let Some(r) = range {
226 out.push_str(&format!(
227 "\n\nRange: line {}-{}, col {}-{}",
228 r.start.line + 1,
229 r.end.line + 1,
230 r.start.character + 1,
231 r.end.character + 1
232 ));
233 }
234 out
235 }
236 LspActionResult::DocumentSymbols { symbols } => {
237 if symbols.is_empty() {
238 "No symbols found in document".to_string()
239 } else {
240 let mut out = format!("Document symbols ({}):\n\n", symbols.len());
241 for sym in symbols {
242 out.push_str(&format!(" {} [{}]", sym.name, sym.kind));
243 if let Some(detail) = sym.detail {
244 out.push_str(&format!(" - {}", detail));
245 }
246 out.push('\n');
247 }
248 out
249 }
250 }
251 LspActionResult::WorkspaceSymbols { symbols } => {
252 if symbols.is_empty() {
253 "No symbols found matching query".to_string()
254 } else {
255 let mut out = format!("Workspace symbols ({}):\n\n", symbols.len());
256 for sym in symbols {
257 out.push_str(&format!(" {} [{}]", sym.name, sym.kind));
258 if let Some(uri) = sym.uri {
259 out.push_str(&format!(" - {}", uri.trim_start_matches("file://")));
260 }
261 out.push('\n');
262 }
263 out
264 }
265 }
266 LspActionResult::Implementation { locations } => {
267 if locations.is_empty() {
268 "No implementations found".to_string()
269 } else {
270 let mut out = format!("Found {} implementation(s):\n\n", locations.len());
271 for loc in locations {
272 let uri = loc.uri;
273 let range = loc.range;
274 out.push_str(&format!(
275 " {}:{}:{}\n",
276 uri.trim_start_matches("file://"),
277 range.start.line + 1,
278 range.start.character + 1
279 ));
280 }
281 out
282 }
283 }
284 LspActionResult::Completion { items } => {
285 if items.is_empty() {
286 "No completions available".to_string()
287 } else {
288 let mut out = format!("Completions ({}):\n\n", items.len());
289 for item in items {
290 out.push_str(&format!(" {}", item.label));
291 if let Some(kind) = item.kind {
292 out.push_str(&format!(" [{}]", kind));
293 }
294 if let Some(detail) = item.detail {
295 out.push_str(&format!(" - {}", detail));
296 }
297 out.push('\n');
298 }
299 out
300 }
301 }
302 LspActionResult::Error { message } => {
303 return Ok(ToolResult::error(message));
304 }
305 };
306
307 Ok(ToolResult::success(output))
308}