1use crate::agent::lsp::manager::LspManager;
2use serde_json::{json, Value};
3use std::path::PathBuf;
4use std::sync::Arc;
5use tokio::sync::Mutex;
6
7fn adjust_position(root: &PathBuf, path: &str, line: u32, character: u32) -> u32 {
8 if character > 0 {
9 return character;
10 }
11
12 let abs_path = root.join(path);
13 if let Ok(content) = std::fs::read_to_string(&abs_path) {
14 let lines: Vec<&str> = content.lines().collect();
15 if let Some(l) = lines.get(line as usize) {
16 if let Some(first) = l.find(|c: char| !c.is_whitespace()) {
17 return first as u32;
18 }
19 }
20 }
21 character
22}
23
24pub async fn lsp_definitions(
25 lsp: Arc<Mutex<LspManager>>,
26 path: String,
27 line: u32,
28 character: u32,
29) -> Result<String, String> {
30 let mut manager = lsp.lock().await;
31 if manager.get_client_for_path(&path).is_none() {
32 let _ = manager.start_servers().await;
33 }
34 let _ = manager.ensure_opened(&path).await;
35 let client = manager
36 .get_client_for_path(&path)
37 .ok_or_else(|| "No Language Server active for this file type.".to_string())?;
38
39 let uri = manager.resolve_uri(&path);
40 let character = adjust_position(&manager.workspace_root, &path, line, character);
41 let params = json!({
42 "textDocument": { "uri": uri },
43 "position": { "line": line, "character": character }
44 });
45
46 let mut result = client
47 .call("textDocument/definition", params.clone())
48 .await?;
49
50 if result.is_null() && line > 0 {
52 let mut fallback_params = params.clone();
53 fallback_params["position"]["line"] = json!(line - 1);
54 let fallback_char = adjust_position(&manager.workspace_root, &path, line - 1, 0);
55 fallback_params["position"]["character"] = json!(fallback_char);
56
57 if let Ok(res) = client
58 .call("textDocument/definition", fallback_params)
59 .await
60 {
61 if !res.is_null() && !res.get("uri").is_none() {
62 result = res;
63 }
64 }
65 }
66
67 format_location_result(result)
68}
69
70pub async fn lsp_references(
71 lsp: Arc<Mutex<LspManager>>,
72 path: String,
73 line: u32,
74 character: u32,
75) -> Result<String, String> {
76 let mut manager = lsp.lock().await;
77 if manager.get_client_for_path(&path).is_none() {
78 let _ = manager.start_servers().await;
79 }
80 let _ = manager.ensure_opened(&path).await;
81 let client = manager
82 .get_client_for_path(&path)
83 .ok_or_else(|| "No Language Server active for this file type.".to_string())?;
84
85 let uri = manager.resolve_uri(&path);
86 let character = adjust_position(&manager.workspace_root, &path, line, character);
87 let params = json!({
88 "textDocument": { "uri": uri },
89 "position": { "line": line, "character": character },
90 "context": { "includeDeclaration": true }
91 });
92
93 let result = client.call("textDocument/references", params).await?;
94 format_location_result(result)
95}
96
97pub async fn lsp_hover(
98 lsp: Arc<Mutex<LspManager>>,
99 path: String,
100 line: u32,
101 character: u32,
102) -> Result<String, String> {
103 let mut manager = lsp.lock().await;
104 if manager.get_client_for_path(&path).is_none() {
105 let _ = manager.start_servers().await;
106 }
107 let _ = manager.ensure_opened(&path).await;
108 let client = manager
109 .get_client_for_path(&path)
110 .ok_or_else(|| "No Language Server active for this file type.".to_string())?;
111
112 let uri = manager.resolve_uri(&path);
113 let character = adjust_position(&manager.workspace_root, &path, line, character);
114 let params = json!({
115 "textDocument": { "uri": uri },
116 "position": { "line": line, "character": character }
117 });
118
119 let mut result = client.call("textDocument/hover", params.clone()).await?;
120
121 if result.is_null() && line > 0 {
123 let mut fallback_params = params.clone();
124 fallback_params["position"]["line"] = json!(line - 1);
125 let fallback_char = adjust_position(&manager.workspace_root, &path, line - 1, 0);
127 fallback_params["position"]["character"] = json!(fallback_char);
128
129 if let Ok(res) = client.call("textDocument/hover", fallback_params).await {
130 if !res.is_null() {
131 result = res;
132 }
133 }
134 }
135
136 if result.is_null() {
137 return Ok("No hover information available.".to_string());
138 }
139
140 let contents = result.get("contents").ok_or("Invalid hover response")?;
141 if let Some(s) = contents.as_str() {
143 Ok(s.to_string())
144 } else if let Some(obj) = contents.get("value") {
145 Ok(obj.as_str().unwrap_or("").to_string())
146 } else {
147 Ok(serde_json::to_string_pretty(contents).unwrap_or_default())
148 }
149}
150
151fn format_location_result(res: Value) -> Result<String, String> {
152 if res.is_null() {
153 return Ok("No results found.".to_string());
154 }
155
156 let mut output = Vec::new();
157 if let Some(arr) = res.as_array() {
158 for loc in arr {
159 output.push(format_location(loc));
160 }
161 } else {
162 output.push(format_location(&res));
163 }
164
165 Ok(output.join("\n"))
166}
167
168fn format_location(loc: &Value) -> String {
169 let uri = loc.get("uri").and_then(|v| v.as_str()).unwrap_or("unknown");
170 let range = loc.get("range");
171 let start = range.and_then(|r| r.get("start"));
172 let line = start
173 .and_then(|s| s.get("line").and_then(|v| v.as_u64()))
174 .unwrap_or(0);
175 let col = start
176 .and_then(|s| s.get("character").and_then(|v| v.as_u64()))
177 .unwrap_or(0);
178
179 format!("{}:{}:{}", uri.replace("file:///", ""), line, col)
180}
181
182pub async fn lsp_search_symbol(
183 lsp: Arc<Mutex<LspManager>>,
184 query: String,
185) -> Result<String, String> {
186 let mut manager = lsp.lock().await;
187 if manager.clients.is_empty() {
189 let _ = manager.start_servers().await;
190 }
191
192 let client = manager
193 .get_client("rust")
194 .ok_or_else(|| "No Language Server active for workspace symbol search.".to_string())?;
195
196 let params = json!({
197 "query": query
198 });
199
200 let result = client.call("workspace/symbol", params).await?;
201 if result.is_null() {
202 return Ok("No symbols found matching your query.".to_string());
203 }
204
205 let mut output = Vec::new();
206 if let Some(arr) = result.as_array() {
207 for sym in arr {
208 let name = sym
209 .get("name")
210 .and_then(|v| v.as_str())
211 .unwrap_or("unknown");
212 let location = sym.get("location");
213 if let Some(loc) = location {
214 let formatted = format_location(loc);
215 output.push(format!("{} -> {}", name, formatted));
216 }
217 }
218 }
219
220 if output.is_empty() {
221 Ok("No symbols found matching your query.".to_string())
222 } else {
223 Ok(output.join("\n"))
224 }
225}
226
227pub async fn lsp_rename_symbol(
228 lsp: Arc<Mutex<LspManager>>,
229 path: String,
230 line: u32,
231 character: u32,
232 new_name: String,
233) -> Result<String, String> {
234 let mut manager = lsp.lock().await;
235 let _ = manager.ensure_opened(&path).await;
236 let client = manager
237 .get_client_for_path(&path)
238 .ok_or_else(|| "No LSP client for this file.".to_string())?;
239
240 let uri = manager.resolve_uri(&path);
241 let character = adjust_position(&manager.workspace_root, &path, line, character);
242 let params = json!({
243 "textDocument": { "uri": uri },
244 "position": { "line": line, "character": character },
245 "newName": new_name
246 });
247
248 let result = client.call("textDocument/rename", params).await?;
249 if result.is_null() {
250 return Ok("Rename failed or no changes returned.".to_string());
251 }
252
253 Ok(format!(
254 "Rename successful. Workspace edit changes: \n{}",
255 serde_json::to_string_pretty(&result).unwrap_or_default()
256 ))
257}
258
259pub async fn lsp_get_diagnostics(
260 lsp: Arc<Mutex<LspManager>>,
261 path: String,
262) -> Result<String, String> {
263 let manager = lsp.lock().await;
264 let client = manager
265 .get_client_for_path(&path)
266 .ok_or_else(|| "No LSP client for this file.".to_string())?;
267
268 let uri = manager.resolve_uri(&path);
269 let all_diags = client.diagnostics.lock().await;
270
271 match all_diags.get(&uri) {
272 Some(Value::Array(indices)) if !indices.is_empty() => {
273 let mut out = format!("Diagnostics for {}:\n", path);
274 for diag in indices {
275 let msg = diag
276 .get("message")
277 .and_then(|v| v.as_str())
278 .unwrap_or("unknown error");
279 let severity = diag.get("severity").and_then(|v| v.as_u64()).unwrap_or(1);
280 let range = diag.get("range");
281 let start_line = range
282 .and_then(|r| r.get("start"))
283 .and_then(|s| s.get("line"))
284 .and_then(|v| v.as_u64())
285 .unwrap_or(0);
286
287 let sev_label = match severity {
288 1 => "[ERROR]",
289 2 => "[WARNING]",
290 3 => "[INFO]",
291 _ => "[HINT]",
292 };
293 out.push_str(&format!("{} Line {}: {}\n", sev_label, start_line + 1, msg));
294 }
295 Ok(out)
296 }
297 _ => Ok(format!(
298 "No diagnostics (errors/warnings) found for {}.",
299 path
300 )),
301 }
302}
303
304pub fn get_lsp_definitions() -> Vec<Value> {
305 vec![
306 json!({
307 "name": "lsp_definitions",
308 "description": "Find the definition of a symbol at a specific file and position (line/char). \
309 Requires /lsp to be active. Much more precise than grep for code navigation.",
310 "parameters": {
311 "type": "object",
312 "properties": {
313 "path": { "type": "string", "description": "Relative path to the file" },
314 "line": { "type": "integer", "description": "0-indexed line number" },
315 "character": { "type": "integer", "description": "0-indexed character offset" }
316 },
317 "required": ["path", "line", "character"]
318 }
319 }),
320 json!({
321 "name": "lsp_references",
322 "description": "Find all references to a symbol at a specific file and position. \
323 Use this to find where a function or struct is used across the project.",
324 "parameters": {
325 "type": "object",
326 "properties": {
327 "path": { "type": "string", "description": "Relative path to the file" },
328 "line": { "type": "integer", "description": "0-indexed line number" },
329 "character": { "type": "integer", "description": "0-indexed character offset" }
330 },
331 "required": ["path", "line", "character"]
332 }
333 }),
334 json!({
335 "name": "lsp_hover",
336 "description": "Get type information, documentation, and metadata for a symbol at a specific position. \
337 Like hovering your mouse over a symbol in an IDE.",
338 "parameters": {
339 "type": "object",
340 "properties": {
341 "path": { "type": "string", "description": "Relative path to the file" },
342 "line": { "type": "integer", "description": "0-indexed line number" },
343 "character": { "type": "integer", "description": "0-indexed character offset" }
344 },
345 "required": ["path", "line", "character"]
346 }
347 }),
348 json!({
349 "name": "lsp_search_symbol",
350 "description": "Find the location (file/line) of any function, struct, or variable in the entire project workspace. \
351 This is the fastest 'Golden Path' for navigating to a symbol by name.",
352 "parameters": {
353 "type": "object",
354 "properties": {
355 "query": { "type": "string", "description": "The name of the symbol to find (e.g. 'initialize_mcp')" }
356 },
357 "required": ["query"]
358 }
359 }),
360 json!({
361 "name": "lsp_rename_symbol",
362 "description": "Rename a symbol reliably across the whole project using the Language Server. \
363 This handles all variable/function name changes safely.",
364 "parameters": {
365 "type": "object",
366 "properties": {
367 "path": { "type": "string", "description": "Relative path to the file containing the symbol" },
368 "line": { "type": "integer", "description": "0-indexed line number" },
369 "character": { "type": "integer", "description": "0-indexed character offset" },
370 "new_name": { "type": "string", "description": "The new name for the symbol" }
371 },
372 "required": ["path", "line", "character", "new_name"]
373 }
374 }),
375 json!({
376 "name": "lsp_get_diagnostics",
377 "description": "Get current compiler/linter errors and warnings for a file. \
378 Use this to verify your changes fixed a bug or to find where your code is broken.",
379 "parameters": {
380 "type": "object",
381 "properties": {
382 "path": { "type": "string", "description": "Relative path to the file" }
383 },
384 "required": ["path"]
385 }
386 }),
387 ]
388}