Skip to main content

cersei_tools/
lsp_tool.rs

1//! LSP tool: query language servers for code intelligence.
2//!
3//! Supports 5 operations:
4//! - `hover`: Get type/documentation info at a position
5//! - `definition`: Go to where a symbol is defined
6//! - `references`: Find all references to a symbol
7//! - `symbols`: List all symbols in a file (outline)
8//! - `diagnostics`: Get compiler errors/warnings for a file
9
10use super::*;
11use cersei_lsp::{LspManager, LspServerConfig};
12use serde::Deserialize;
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15use tokio::sync::Mutex;
16
17pub struct LspTool {
18    manager: Arc<Mutex<LspManager>>,
19}
20
21impl LspTool {
22    pub fn new(working_dir: &Path) -> Self {
23        let mut mgr = LspManager::new(working_dir);
24        mgr.register_builtins();
25        Self {
26            manager: Arc::new(Mutex::new(mgr)),
27        }
28    }
29
30    pub fn with_configs(working_dir: &Path, extra_configs: &[LspServerConfig]) -> Self {
31        let mut mgr = LspManager::new(working_dir);
32        mgr.register_builtins();
33        mgr.seed_from_configs(extra_configs);
34        Self {
35            manager: Arc::new(Mutex::new(mgr)),
36        }
37    }
38}
39
40#[async_trait]
41impl Tool for LspTool {
42    fn name(&self) -> &str {
43        "LSP"
44    }
45
46    fn description(&self) -> &str {
47        "Query a language server for code intelligence. Supports hover (type info), \
48         definition (go-to-def), references (find usages), symbols (file outline), \
49         and diagnostics (compiler errors). Language servers are auto-detected \
50         and started on demand based on file extension."
51    }
52
53    fn permission_level(&self) -> PermissionLevel {
54        PermissionLevel::ReadOnly
55    }
56
57    fn category(&self) -> ToolCategory {
58        ToolCategory::FileSystem
59    }
60
61    fn input_schema(&self) -> serde_json::Value {
62        serde_json::json!({
63            "type": "object",
64            "properties": {
65                "action": {
66                    "type": "string",
67                    "enum": ["hover", "definition", "references", "symbols", "diagnostics"],
68                    "description": "The LSP operation to perform"
69                },
70                "file": {
71                    "type": "string",
72                    "description": "Absolute or relative file path"
73                },
74                "line": {
75                    "type": "integer",
76                    "description": "1-based line number (required for hover, definition, references)"
77                },
78                "column": {
79                    "type": "integer",
80                    "description": "1-based column number (required for hover, definition, references)"
81                }
82            },
83            "required": ["action", "file"]
84        })
85    }
86
87    async fn execute(&self, input: serde_json::Value, ctx: &ToolContext) -> ToolResult {
88        #[derive(Deserialize)]
89        struct Input {
90            action: String,
91            file: String,
92            line: Option<u32>,
93            column: Option<u32>,
94        }
95
96        let input: Input = match serde_json::from_value(input) {
97            Ok(i) => i,
98            Err(e) => return ToolResult::error(format!("Invalid input: {e}")),
99        };
100
101        // Resolve path
102        let path = if Path::new(&input.file).is_absolute() {
103            PathBuf::from(&input.file)
104        } else {
105            ctx.working_dir.join(&input.file)
106        };
107
108        if !path.exists() {
109            return ToolResult::error(format!("File not found: {}", path.display()));
110        }
111
112        let mut mgr = self.manager.lock().await;
113
114        // Check if we have a server for this file type
115        if !mgr.has_server_for(&path) {
116            let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("?");
117            return ToolResult::error(format!(
118                "No language server configured for .{ext} files. \
119                 Available servers: {}",
120                mgr.servers()
121                    .iter()
122                    .map(|s| s.name.as_str())
123                    .collect::<Vec<_>>()
124                    .join(", ")
125            ));
126        }
127
128        // Convert 1-based to 0-based for LSP
129        let line = input.line.unwrap_or(1).saturating_sub(1);
130        let col = input.column.unwrap_or(1).saturating_sub(1);
131
132        match input.action.as_str() {
133            "hover" => match mgr.hover(&path, line, col).await {
134                Ok(Some(text)) => ToolResult::success(text),
135                Ok(None) => ToolResult::success("No hover information available at this position."),
136                Err(e) => ToolResult::error(format!("Hover failed: {e}")),
137            },
138
139            "definition" => match mgr.definition(&path, line, col).await {
140                Ok(locations) => {
141                    if locations.is_empty() {
142                        ToolResult::success("No definition found at this position.")
143                    } else {
144                        ToolResult::success(locations.join("\n"))
145                    }
146                }
147                Err(e) => ToolResult::error(format!("Definition lookup failed: {e}")),
148            },
149
150            "references" => match mgr.references(&path, line, col).await {
151                Ok(locations) => {
152                    if locations.is_empty() {
153                        ToolResult::success("No references found at this position.")
154                    } else {
155                        let count = locations.len();
156                        let mut result = locations.join("\n");
157                        result.push_str(&format!("\n\n{count} reference(s) found."));
158                        ToolResult::success(result)
159                    }
160                }
161                Err(e) => ToolResult::error(format!("References lookup failed: {e}")),
162            },
163
164            "symbols" => match mgr.document_symbols(&path).await {
165                Ok(symbols) => {
166                    if symbols.is_empty() {
167                        ToolResult::success("No symbols found in this file.")
168                    } else {
169                        let output: String = symbols.iter().map(|s| s.format(0)).collect();
170                        ToolResult::success(output.trim_end())
171                    }
172                }
173                Err(e) => ToolResult::error(format!("Symbol extraction failed: {e}")),
174            },
175
176            "diagnostics" => match mgr.diagnostics(&path).await {
177                Ok(diags) => {
178                    ToolResult::success(LspManager::format_diagnostics(&diags))
179                }
180                Err(e) => ToolResult::error(format!("Diagnostics failed: {e}")),
181            },
182
183            other => ToolResult::error(format!(
184                "Unknown action: '{other}'. Use: hover, definition, references, symbols, diagnostics"
185            )),
186        }
187    }
188}