use async_trait::async_trait;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use tokio::sync::oneshot;
use super::{AgentTool, AgentToolResult, LspAction, ToolContext, ToolError};
use crate::tools::typed::TypedTool;
#[derive(Deserialize, Serialize, JsonSchema)]
pub struct LspArgs {
action: String,
file: Option<String>,
#[serde(rename = "oldPath")]
old_path: Option<String>,
#[serde(rename = "newPath")]
new_path: Option<String>,
line: Option<u32>,
symbol: Option<String>,
#[serde(rename = "newName")]
new_name: Option<String>,
#[serde(default)]
apply: bool,
query: Option<String>,
}
pub struct LspTool;
#[async_trait]
impl AgentTool for LspTool {
fn name(&self) -> &str {
"lsp"
}
fn label(&self) -> &str {
"LSP"
}
fn essential(&self) -> bool {
false
}
fn description(&self) -> &str {
"Language Server Protocol operations: diagnostics, definition, references, \
hover, rename, symbols. Requires LSP enabled in settings."
}
fn parameters_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": [
"diagnostics", "definition", "references", "hover",
"rename", "symbols", "status",
"code_actions", "type_definition", "implementation",
"file_rename"
]
},
"file": {"type": "string", "description": "File path"},
"old_path": {"type": "string", "description": "Existing path for file_rename"},
"new_path": {"type": "string", "description": "Target path for file_rename"},
"line": {"type": "integer", "description": "1-based line number"},
"symbol": {"type": "string", "description": "Symbol name (optional, for disambiguation)"},
"new_name": {"type": "string", "description": "New name for rename"},
"apply": {"type": "boolean", "description": "Apply changes (vs preview)"},
"query": {"type": "string", "description": "Search query for symbols"}
},
"required": ["action"]
})
}
async fn execute(
&self,
_tool_call_id: &str,
params: Value,
_signal: Option<oneshot::Receiver<()>>,
ctx: &ToolContext,
) -> Result<AgentToolResult, ToolError> {
let args: LspArgs =
serde_json::from_value(params).map_err(|e| format!("invalid params: {e}"))?;
self.execute_typed(_tool_call_id, args, _signal, ctx).await
}
}
#[async_trait]
impl TypedTool for LspTool {
type Args = LspArgs;
async fn execute_typed(
&self,
_tool_call_id: &str,
args: Self::Args,
_signal: Option<oneshot::Receiver<()>>,
ctx: &ToolContext,
) -> Result<AgentToolResult, ToolError> {
let provider = ctx
.lsp
.as_ref()
.ok_or("LSP not configured. Enable LSP in settings or install language servers.")?;
let params = serde_json::to_value(&args).map_err(|e| format!("serialize: {e}"))?;
let action = parse_lsp_action(¶ms)?;
let result = provider.execute_action(&action).await?;
Ok(AgentToolResult::success(result))
}
}
fn parse_lsp_action(params: &Value) -> Result<LspAction, ToolError> {
let action = params
.get("action")
.and_then(|v| v.as_str())
.ok_or("Missing required parameter: action")?;
let file = params
.get("file")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let line = params
.get("line")
.and_then(|v| v.as_u64())
.map(|n| n as u32)
.unwrap_or(1);
let symbol = params
.get("symbol")
.and_then(|v| v.as_str())
.map(String::from);
match action {
"diagnostics" => Ok(LspAction::Diagnostics { file }),
"definition" => Ok(LspAction::Definition { file, line, symbol }),
"references" => Ok(LspAction::References { file, line, symbol }),
"hover" => Ok(LspAction::Hover { file, line, symbol }),
"rename" => {
let new_name = params
.get("new_name")
.and_then(|v| v.as_str())
.ok_or("rename requires 'new_name'")?
.to_string();
let sym = symbol.clone().unwrap_or_default();
let apply = params
.get("apply")
.and_then(|v| v.as_bool())
.unwrap_or(false);
Ok(LspAction::Rename {
file,
line,
symbol: sym,
new_name,
apply,
})
}
"symbols" => {
let query = params
.get("query")
.and_then(|v| v.as_str())
.map(String::from);
Ok(LspAction::Symbols { file, query })
}
"status" => Ok(LspAction::Status),
"code_actions" => Ok(LspAction::CodeActions { file, line, symbol }),
"type_definition" => Ok(LspAction::TypeDefinition { file, line, symbol }),
"implementation" => Ok(LspAction::Implementation { file, line, symbol }),
"file_rename" => {
let old_path = params
.get("old_path")
.and_then(|v| v.as_str())
.ok_or("file_rename requires 'old_path'")?
.to_string();
let new_path = params
.get("new_path")
.and_then(|v| v.as_str())
.ok_or("file_rename requires 'new_path'")?
.to_string();
let apply = params
.get("apply")
.and_then(|v| v.as_bool())
.unwrap_or(false);
Ok(LspAction::FileRename {
old_path,
new_path,
apply,
})
}
other => Err(format!("Unknown LSP action: '{}'", other)),
}
}