1use 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 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 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 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}