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": [
44                        "diagnostics", "definition", "references", "hover",
45                        "rename", "symbols", "status",
46                        "code_actions", "type_definition", "implementation",
47                        "file_rename"
48                    ]
49                },
50                "file": {"type": "string", "description": "File path"},
51                "old_path": {"type": "string", "description": "Existing path for file_rename"},
52                "new_path": {"type": "string", "description": "Target path for file_rename"},
53                "line": {"type": "integer", "description": "1-based line number"},
54                "symbol": {"type": "string", "description": "Symbol name (optional, for disambiguation)"},
55                "new_name": {"type": "string", "description": "New name for rename"},
56                "apply": {"type": "boolean", "description": "Apply changes (vs preview)"},
57                "query": {"type": "string", "description": "Search query for symbols"}
58            },
59            "required": ["action"]
60        })
61    }
62
63    async fn execute(
64        &self,
65        _tool_call_id: &str,
66        params: Value,
67        _signal: Option<tokio::sync::oneshot::Receiver<()>>,
68        ctx: &ToolContext,
69    ) -> Result<AgentToolResult, ToolError> {
70        let provider = ctx
71            .lsp
72            .as_ref()
73            .ok_or("LSP not configured. Enable LSP in settings or install language servers.")?;
74
75        let action = parse_lsp_action(&params)?;
76
77        let result = provider.execute_action(&action).await?;
78
79        Ok(AgentToolResult::success(result))
80    }
81}
82
83/// Parse JSON params into an `LspAction`.
84fn parse_lsp_action(params: &Value) -> Result<LspAction, ToolError> {
85    let action = params
86        .get("action")
87        .and_then(|v| v.as_str())
88        .ok_or("Missing required parameter: action")?;
89
90    let file = params
91        .get("file")
92        .and_then(|v| v.as_str())
93        .unwrap_or("")
94        .to_string();
95
96    let line = params
97        .get("line")
98        .and_then(|v| v.as_u64())
99        .map(|n| n as u32)
100        .unwrap_or(1);
101
102    let symbol = params
103        .get("symbol")
104        .and_then(|v| v.as_str())
105        .map(String::from);
106
107    match action {
108        "diagnostics" => Ok(LspAction::Diagnostics { file }),
109        "definition" => Ok(LspAction::Definition { file, line, symbol }),
110        "references" => Ok(LspAction::References { file, line, symbol }),
111        "hover" => Ok(LspAction::Hover { file, line, symbol }),
112        "rename" => {
113            let new_name = params
114                .get("new_name")
115                .and_then(|v| v.as_str())
116                .ok_or("rename requires 'new_name'")?
117                .to_string();
118            let sym = symbol.clone().unwrap_or_default();
119            let apply = params
120                .get("apply")
121                .and_then(|v| v.as_bool())
122                .unwrap_or(false);
123            Ok(LspAction::Rename {
124                file,
125                line,
126                symbol: sym,
127                new_name,
128                apply,
129            })
130        }
131        "symbols" => {
132            let query = params
133                .get("query")
134                .and_then(|v| v.as_str())
135                .map(String::from);
136            Ok(LspAction::Symbols { file, query })
137        }
138        "status" => Ok(LspAction::Status),
139        "code_actions" => Ok(LspAction::CodeActions { file, line, symbol }),
140        "type_definition" => Ok(LspAction::TypeDefinition { file, line, symbol }),
141        "implementation" => Ok(LspAction::Implementation { file, line, symbol }),
142        "file_rename" => {
143            let old_path = params
144                .get("old_path")
145                .and_then(|v| v.as_str())
146                .ok_or("file_rename requires 'old_path'")?
147                .to_string();
148            let new_path = params
149                .get("new_path")
150                .and_then(|v| v.as_str())
151                .ok_or("file_rename requires 'new_path'")?
152                .to_string();
153            let apply = params
154                .get("apply")
155                .and_then(|v| v.as_bool())
156                .unwrap_or(false);
157            Ok(LspAction::FileRename {
158                old_path,
159                new_path,
160                apply,
161            })
162        }
163        other => Err(format!("Unknown LSP action: '{}'", other)),
164    }
165}