1use crate::registry::{Tool, ToolContext};
8use anyhow::Result;
9use async_trait::async_trait;
10use serde_json::{json, Value};
11use std::path::Path;
12use std::sync::Arc;
13
14pub struct CodeNavTool {
16 manager: Arc<aegis_lsp::LspManager>,
17}
18
19impl CodeNavTool {
20 pub fn new(manager: Arc<aegis_lsp::LspManager>) -> Self {
22 Self { manager }
23 }
24}
25
26#[async_trait]
27impl Tool for CodeNavTool {
28 fn name(&self) -> &str {
29 "code_nav"
30 }
31
32 fn description(&self) -> &str {
33 "Navigate code via the language server: 'definition' (jump to where a symbol is defined), 'references' (find all uses), 'hover' (type/signature/docs), or 'symbols' (outline a file). Requires a configured language server for the file type."
34 }
35
36 fn parameters(&self) -> Value {
37 json!({
38 "type": "object",
39 "properties": {
40 "action": {
41 "type": "string",
42 "enum": ["definition", "references", "hover", "symbols"],
43 "description": "Navigation action"
44 },
45 "path": { "type": "string", "description": "Path to the source file" },
46 "line": { "type": "integer", "description": "1-based line of the symbol (required for definition/references/hover)" },
47 "column": { "type": "integer", "description": "1-based column of the symbol (default 1)" }
48 },
49 "required": ["action", "path"]
50 })
51 }
52
53 async fn execute(&self, args: Value, ctx: &ToolContext<'_>) -> Result<String> {
54 let action = args["action"].as_str().unwrap_or("").trim();
55 if action.is_empty() {
56 return Ok("Provide an `action`: definition, references, hover, or symbols.".to_string());
57 }
58 let Some(path) = args.get("path").and_then(|p| p.as_str()) else {
59 return Ok("Provide a `path` to a source file.".to_string());
60 };
61
62 let p = Path::new(path);
63 let abs = if p.is_absolute() { p.to_path_buf() } else { ctx.cwd.join(p) };
64 if !abs.exists() {
65 return Ok(format!("File not found: {path}"));
66 }
67 if !self.manager.handles(&abs) {
68 return Ok(format!(
69 "No language server configured for `{path}`. Configure one under [lsp.servers] in config.toml."
70 ));
71 }
72
73 let (line0, col0) = if action == "symbols" {
75 (0, 0)
76 } else {
77 let Some(line) = args["line"].as_u64() else {
78 return Ok(format!("`{action}` needs a 1-based `line` (and optionally `column`)."));
79 };
80 let col = args["column"].as_u64().unwrap_or(1);
81 (line.saturating_sub(1), col.saturating_sub(1))
83 };
84
85 Ok(self.manager.navigate(action, &abs, &ctx.cwd, line0, col0).await)
86 }
87}