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 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/// Typed arguments for [`LspTool`].
17#[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
34/// `lsp` agent tool — IDE-grade code intelligence.
35///
36/// Requires `LspProvider` capability. Supports diagnostics, definition,
37/// references, hover, rename, and symbols.
38pub 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(&params)?;
116        let result = provider.execute_action(&action).await?;
117        Ok(AgentToolResult::success(result))
118    }
119}
120
121/// Parse JSON params into an `LspAction`.
122fn 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}