Skip to main content

oxi_agent/tools/
lsp.rs

1//! LSP tool — Language Server Protocol operations.
2//!
3//! Delegates to `LspProvider` capability (injected via `ToolContext.lsp`).
4//! When `None`, returns an error indicating LSP is not configured.
5//! The actual LSP client lives in the (future) `oxi-lsp` crate.
6
7use async_trait::async_trait;
8use serde_json::{Value, json};
9
10use super::{AgentTool, AgentToolResult, LspAction, ToolContext, ToolError};
11
12/// `lsp` agent tool — IDE-grade code intelligence.
13///
14/// Requires `LspProvider` capability. Supports diagnostics, definition,
15/// references, hover, rename, and symbols.
16pub struct LspTool;
17
18#[async_trait]
19impl AgentTool for LspTool {
20    fn name(&self) -> &str {
21        "lsp"
22    }
23
24    fn label(&self) -> &str {
25        "LSP"
26    }
27
28    fn essential(&self) -> bool {
29        false
30    }
31
32    fn description(&self) -> &str {
33        "Language Server Protocol operations: diagnostics, definition, references, \
34         hover, rename, symbols. Requires LSP enabled in settings."
35    }
36
37    fn parameters_schema(&self) -> Value {
38        json!({
39            "type": "object",
40            "properties": {
41                "action": {
42                    "type": "string",
43                    "enum": ["diagnostics", "definition", "references", "hover", "rename", "symbols", "status"]
44                },
45                "file": {"type": "string", "description": "File path"},
46                "line": {"type": "integer", "description": "1-based line number"},
47                "symbol": {"type": "string", "description": "Symbol name (optional, for disambiguation)"},
48                "new_name": {"type": "string", "description": "New name for rename"},
49                "apply": {"type": "boolean", "description": "Apply changes (vs preview)"},
50                "query": {"type": "string", "description": "Search query for symbols"}
51            },
52            "required": ["action"]
53        })
54    }
55
56    async fn execute(
57        &self,
58        _tool_call_id: &str,
59        params: Value,
60        _signal: Option<tokio::sync::oneshot::Receiver<()>>,
61        ctx: &ToolContext,
62    ) -> Result<AgentToolResult, ToolError> {
63        let provider = ctx
64            .lsp
65            .as_ref()
66            .ok_or("LSP not configured. Enable LSP in settings or install language servers.")?;
67
68        let action = parse_lsp_action(&params)?;
69
70        let result = provider.execute_action(&action).await?;
71
72        Ok(AgentToolResult::success(result))
73    }
74}
75
76/// Parse JSON params into an `LspAction`.
77fn parse_lsp_action(params: &Value) -> Result<LspAction, ToolError> {
78    let action = params
79        .get("action")
80        .and_then(|v| v.as_str())
81        .ok_or("Missing required parameter: action")?;
82
83    let file = params
84        .get("file")
85        .and_then(|v| v.as_str())
86        .unwrap_or("")
87        .to_string();
88
89    let line = params
90        .get("line")
91        .and_then(|v| v.as_u64())
92        .map(|n| n as u32)
93        .unwrap_or(1);
94
95    let symbol = params
96        .get("symbol")
97        .and_then(|v| v.as_str())
98        .map(String::from);
99
100    match action {
101        "diagnostics" => Ok(LspAction::Diagnostics { file }),
102        "definition" => Ok(LspAction::Definition { file, line, symbol }),
103        "references" => Ok(LspAction::References { file, line, symbol }),
104        "hover" => Ok(LspAction::Hover { file, line, symbol }),
105        "rename" => {
106            let new_name = params
107                .get("new_name")
108                .and_then(|v| v.as_str())
109                .ok_or("rename requires 'new_name'")?
110                .to_string();
111            let sym = symbol.clone().unwrap_or_default();
112            let apply = params
113                .get("apply")
114                .and_then(|v| v.as_bool())
115                .unwrap_or(false);
116            Ok(LspAction::Rename {
117                file,
118                line,
119                symbol: sym,
120                new_name,
121                apply,
122            })
123        }
124        "symbols" => {
125            let query = params
126                .get("query")
127                .and_then(|v| v.as_str())
128                .map(String::from);
129            Ok(LspAction::Symbols { file, query })
130        }
131        "status" => Ok(LspAction::Status),
132        other => Err(format!("Unknown LSP action: '{}'", other)),
133    }
134}