agent_code_lib/tools/
lsp_tool.rs1use 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 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}