Skip to main content

agent_code_lib/tools/
lsp_tool.rs

1//! LSP tool: query language servers for diagnostics and symbols.
2
3use async_trait::async_trait;
4use serde_json::json;
5
6use super::{Tool, ToolContext, ToolResult};
7use crate::error::ToolError;
8
9pub struct LspTool;
10
11#[async_trait]
12impl Tool for LspTool {
13    fn name(&self) -> &'static str {
14        "LSP"
15    }
16
17    fn description(&self) -> &'static str {
18        "Query a language server for diagnostics, definitions, references, \
19         or symbols in the current project."
20    }
21
22    fn input_schema(&self) -> serde_json::Value {
23        json!({
24            "type": "object",
25            "required": ["action"],
26            "properties": {
27                "action": {
28                    "type": "string",
29                    "enum": ["diagnostics", "definition", "references", "symbols"],
30                    "description": "What to query from the language server"
31                },
32                "file_path": {
33                    "type": "string",
34                    "description": "File to query (required for diagnostics/definition/references)"
35                },
36                "line": {
37                    "type": "integer",
38                    "description": "Line number (1-based, for definition/references)"
39                },
40                "column": {
41                    "type": "integer",
42                    "description": "Column number (1-based, for definition/references)"
43                },
44                "query": {
45                    "type": "string",
46                    "description": "Symbol name to search (for symbols action)"
47                }
48            }
49        })
50    }
51
52    fn is_read_only(&self) -> bool {
53        true
54    }
55
56    fn is_concurrency_safe(&self) -> bool {
57        true
58    }
59
60    async fn call(
61        &self,
62        input: serde_json::Value,
63        ctx: &ToolContext,
64    ) -> Result<ToolResult, ToolError> {
65        let action = input
66            .get("action")
67            .and_then(|v| v.as_str())
68            .ok_or_else(|| ToolError::InvalidInput("'action' is required".into()))?;
69
70        match action {
71            "diagnostics" => {
72                let file = input
73                    .get("file_path")
74                    .and_then(|v| v.as_str())
75                    .ok_or_else(|| {
76                        ToolError::InvalidInput("'file_path' required for diagnostics".into())
77                    })?;
78
79                // Shell out to common linters as a fallback when no LSP is connected.
80                let ext = std::path::Path::new(file)
81                    .extension()
82                    .and_then(|e| e.to_str())
83                    .unwrap_or("");
84
85                let (cmd, args) = match ext {
86                    "rs" => ("cargo", vec!["check", "--message-format=short"]),
87                    "py" => ("python3", vec!["-m", "py_compile", file]),
88                    "js" | "ts" | "tsx" | "jsx" => ("npx", vec!["tsc", "--noEmit", "--pretty"]),
89                    "go" => ("go", vec!["vet", "./..."]),
90                    "rb" => ("ruby", vec!["-c", file]),
91                    _ => {
92                        return Ok(ToolResult::success(format!(
93                            "No linter available for .{ext} files. \
94                             Connect an LSP server for full diagnostics."
95                        )));
96                    }
97                };
98
99                let output = tokio::process::Command::new(cmd)
100                    .args(&args)
101                    .current_dir(&ctx.cwd)
102                    .output()
103                    .await
104                    .map_err(|e| ToolError::ExecutionFailed(format!("{cmd} failed: {e}")))?;
105
106                let stdout = String::from_utf8_lossy(&output.stdout);
107                let stderr = String::from_utf8_lossy(&output.stderr);
108                let combined = format!("{stdout}{stderr}");
109
110                if combined.trim().is_empty() {
111                    Ok(ToolResult::success("No diagnostics found."))
112                } else {
113                    Ok(ToolResult::success(combined.to_string()))
114                }
115            }
116            "definition" | "references" | "symbols" => Ok(ToolResult::success(format!(
117                "LSP '{action}' requires a connected language server. \
118                     Configure one in your settings to enable this feature."
119            ))),
120            other => Err(ToolError::InvalidInput(format!(
121                "Unknown action '{other}'. Use: diagnostics, definition, references, symbols"
122            ))),
123        }
124    }
125}