Skip to main content

aegis_tools/
lsp_nav.rs

1//! # CodeNavTool
2//!
3//! Exposes language-server navigation to the agent: go-to-definition, find
4//! references, hover (type/signature/docs), and document symbols. Wraps the
5//! shared [`aegis_lsp::LspManager`]. Registered only when `[lsp].enabled`.
6
7use 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
14/// Language-server code navigation (definition/references/hover/symbols).
15pub struct CodeNavTool {
16    manager: Arc<aegis_lsp::LspManager>,
17}
18
19impl CodeNavTool {
20    /// Create a new `CodeNavTool` sharing the given LSP manager.
21    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        // Position-based actions need a line; symbols does not.
74        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            // Convert 1-based (human) → 0-based (LSP).
82            (line.saturating_sub(1), col.saturating_sub(1))
83        };
84
85        Ok(self.manager.navigate(action, &abs, &ctx.cwd, line0, col0).await)
86    }
87}