1use async_trait::async_trait;
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10use serde_json::{Value, json};
11use tokio::sync::oneshot;
12
13use super::{AgentTool, AgentToolResult, LspAction, ToolContext, ToolError};
14use crate::tools::typed::TypedTool;
15
16#[derive(Deserialize, Serialize, JsonSchema)]
18pub struct LspArgs {
19 action: String,
20 file: Option<String>,
21 #[serde(rename = "oldPath")]
22 old_path: Option<String>,
23 #[serde(rename = "newPath")]
24 new_path: Option<String>,
25 line: Option<u32>,
26 symbol: Option<String>,
27 #[serde(rename = "newName")]
28 new_name: Option<String>,
29 #[serde(default)]
30 apply: bool,
31 query: Option<String>,
32}
33
34pub struct LspTool;
39
40#[async_trait]
41impl AgentTool for LspTool {
42 fn name(&self) -> &str {
43 "lsp"
44 }
45
46 fn label(&self) -> &str {
47 "LSP"
48 }
49
50 fn essential(&self) -> bool {
51 false
52 }
53
54 fn description(&self) -> &str {
55 "Language Server Protocol operations: diagnostics, definition, references, \
56 hover, rename, symbols. Requires LSP enabled in settings."
57 }
58
59 fn parameters_schema(&self) -> Value {
60 json!({
61 "type": "object",
62 "properties": {
63 "action": {
64 "type": "string",
65 "enum": [
66 "diagnostics", "definition", "references", "hover",
67 "rename", "symbols", "status",
68 "code_actions", "type_definition", "implementation",
69 "file_rename"
70 ]
71 },
72 "file": {"type": "string", "description": "File path"},
73 "old_path": {"type": "string", "description": "Existing path for file_rename"},
74 "new_path": {"type": "string", "description": "Target path for file_rename"},
75 "line": {"type": "integer", "description": "1-based line number"},
76 "symbol": {"type": "string", "description": "Symbol name (optional, for disambiguation)"},
77 "new_name": {"type": "string", "description": "New name for rename"},
78 "apply": {"type": "boolean", "description": "Apply changes (vs preview)"},
79 "query": {"type": "string", "description": "Search query for symbols"}
80 },
81 "required": ["action"]
82 })
83 }
84
85 async fn execute(
86 &self,
87 _tool_call_id: &str,
88 params: Value,
89 _signal: Option<oneshot::Receiver<()>>,
90 ctx: &ToolContext,
91 ) -> Result<AgentToolResult, ToolError> {
92 let args: LspArgs =
93 serde_json::from_value(params).map_err(|e| format!("invalid params: {e}"))?;
94 self.execute_typed(_tool_call_id, args, _signal, ctx).await
95 }
96}
97
98#[async_trait]
99impl TypedTool for LspTool {
100 type Args = LspArgs;
101
102 async fn execute_typed(
103 &self,
104 _tool_call_id: &str,
105 args: Self::Args,
106 _signal: Option<oneshot::Receiver<()>>,
107 ctx: &ToolContext,
108 ) -> Result<AgentToolResult, ToolError> {
109 let provider = ctx
110 .lsp
111 .as_ref()
112 .ok_or("LSP not configured. Enable LSP in settings or install language servers.")?;
113
114 let params = serde_json::to_value(&args).map_err(|e| format!("serialize: {e}"))?;
115 let action = parse_lsp_action(¶ms)?;
116 let result = provider.execute_action(&action).await?;
117 Ok(AgentToolResult::success(result))
118 }
119}
120
121fn parse_lsp_action(params: &Value) -> Result<LspAction, ToolError> {
123 let action = params
124 .get("action")
125 .and_then(|v| v.as_str())
126 .ok_or("Missing required parameter: action")?;
127
128 let file = params
129 .get("file")
130 .and_then(|v| v.as_str())
131 .unwrap_or("")
132 .to_string();
133
134 let line = params
135 .get("line")
136 .and_then(|v| v.as_u64())
137 .map(|n| n as u32)
138 .unwrap_or(1);
139
140 let symbol = params
141 .get("symbol")
142 .and_then(|v| v.as_str())
143 .map(String::from);
144
145 match action {
146 "diagnostics" => Ok(LspAction::Diagnostics { file }),
147 "definition" => Ok(LspAction::Definition { file, line, symbol }),
148 "references" => Ok(LspAction::References { file, line, symbol }),
149 "hover" => Ok(LspAction::Hover { file, line, symbol }),
150 "rename" => {
151 let new_name = params
152 .get("new_name")
153 .and_then(|v| v.as_str())
154 .ok_or("rename requires 'new_name'")?
155 .to_string();
156 let sym = symbol.clone().unwrap_or_default();
157 let apply = params
158 .get("apply")
159 .and_then(|v| v.as_bool())
160 .unwrap_or(false);
161 Ok(LspAction::Rename {
162 file,
163 line,
164 symbol: sym,
165 new_name,
166 apply,
167 })
168 }
169 "symbols" => {
170 let query = params
171 .get("query")
172 .and_then(|v| v.as_str())
173 .map(String::from);
174 Ok(LspAction::Symbols { file, query })
175 }
176 "status" => Ok(LspAction::Status),
177 "code_actions" => Ok(LspAction::CodeActions { file, line, symbol }),
178 "type_definition" => Ok(LspAction::TypeDefinition { file, line, symbol }),
179 "implementation" => Ok(LspAction::Implementation { file, line, symbol }),
180 "file_rename" => {
181 let old_path = params
182 .get("old_path")
183 .and_then(|v| v.as_str())
184 .ok_or("file_rename requires 'old_path'")?
185 .to_string();
186 let new_path = params
187 .get("new_path")
188 .and_then(|v| v.as_str())
189 .ok_or("file_rename requires 'new_path'")?
190 .to_string();
191 let apply = params
192 .get("apply")
193 .and_then(|v| v.as_bool())
194 .unwrap_or(false);
195 Ok(LspAction::FileRename {
196 old_path,
197 new_path,
198 apply,
199 })
200 }
201 other => Err(format!("Unknown LSP action: '{}'", other)),
202 }
203}