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