1use anyhow::{anyhow, Result};
10use async_trait::async_trait;
11use lsp_types::Position;
12use serde_json::{json, Value};
13use std::path::PathBuf;
14use std::sync::Arc;
15
16use crate::approval::RiskLevel;
17use crate::tools::{Tool, ToolDefinition};
18use super::client::{format_diagnostic, format_location, path_to_uri};
19use super::registry::LspClientRegistry;
20
21pub fn detect_language_from_path(path: &PathBuf) -> Option<String> {
27 let ext = path.extension()?.to_str()?.to_lowercase();
28 let language = match ext.as_str() {
29 "rs" => "rust",
31 "ts" => "typescript",
33 "tsx" => "typescript",
34 "js" => "javascript",
35 "jsx" => "javascript",
36 "mjs" => "javascript",
37 "cjs" => "javascript",
38 "py" => "python",
40 "pyi" => "python",
41 "go" => "go",
43 "c" => "c",
45 "h" => "c",
46 "cpp" => "cpp",
47 "hpp" => "cpp",
48 "cc" => "cpp",
49 "cxx" => "cpp",
50 "java" => "java",
52 "cs" => "csharp",
54 "rb" => "ruby",
56 "php" => "php",
58 "swift" => "swift",
60 "kt" => "kotlin",
62 "kts" => "kotlin",
63 "scala" => "scala",
65 "lua" => "lua",
67 "vue" => "vue",
69 "svelte" => "svelte",
71 "html" => "html",
73 "htm" => "html",
74 "css" => "css",
75 "scss" => "scss",
76 "less" => "less",
77 "json" => "json",
79 "yaml" => "yaml",
80 "yml" => "yaml",
81 "toml" => "toml",
82 "md" => "markdown",
84 "sh" => "shell",
86 "bash" => "shell",
87 "zsh" => "shell",
88 _ => return None,
90 };
91 Some(language.to_string())
92}
93
94pub struct LspHoverTool {
100 registry: Arc<LspClientRegistry>,
101}
102
103impl LspHoverTool {
104 pub fn new(registry: Arc<LspClientRegistry>) -> Self {
105 Self { registry }
106 }
107}
108
109#[async_trait]
110impl Tool for LspHoverTool {
111 fn definition(&self) -> ToolDefinition {
112 ToolDefinition {
113 name: "lsp_hover".to_string(),
114 description: "获取指定位置的类型签名和文档。返回类型信息、函数签名、文档注释等。需要 LSP 服务器支持。".to_string(),
115 parameters: json!({
116 "type": "object",
117 "properties": {
118 "file": {
119 "type": "string",
120 "description": "文件路径(绝对路径)"
121 },
122 "line": {
123 "type": "integer",
124 "description": "行号(0-based)"
125 },
126 "column": {
127 "type": "integer",
128 "description": "列号(0-based)"
129 }
130 },
131 "required": ["file", "line", "column"]
132 }),
133 is_priority: false,
134 }
135 }
136
137 async fn execute(&self, params: Value) -> Result<String> {
138 let file_path = params["file"]
139 .as_str()
140 .ok_or_else(|| anyhow!("缺少 'file' 参数"))?;
141 let line = params["line"]
142 .as_u64()
143 .ok_or_else(|| anyhow!("缺少 'line' 参数"))? as u32;
144 let column = params["column"]
145 .as_u64()
146 .ok_or_else(|| anyhow!("缺少 'column' 参数"))? as u32;
147
148 let path = PathBuf::from(file_path);
149 let language = detect_language_from_path(&path)
150 .ok_or_else(|| anyhow!("无法识别文件类型: {}", file_path))?;
151
152 let client = self.registry.get_client_or_wait(&language).await?;
154
155 let uri = path_to_uri(&path)?;
157 let content = tokio::fs::read_to_string(&path).await
158 .map_err(|e| anyhow!("读取文件失败: {}", e))?;
159 client.open_file(&uri, &content).await?;
160
161 let position = Position { line, character: column };
163 let hover = client.hover(&uri, position).await?
164 .ok_or_else(|| anyhow!("该位置没有 hover 信息"))?;
165
166 let mut result = format!("【类型】{}\n", hover.signature);
168 if let Some(doc) = &hover.documentation {
169 result.push_str(&format!("【文档】{}\n", doc));
170 }
171 result.push_str(&format!("【来源】{}", client.server_name()));
172
173 Ok(result)
174 }
175
176 fn risk_level(&self) -> RiskLevel {
177 RiskLevel::Safe
178 }
179}
180
181pub struct LspDefinitionTool {
187 registry: Arc<LspClientRegistry>,
188}
189
190impl LspDefinitionTool {
191 pub fn new(registry: Arc<LspClientRegistry>) -> Self {
192 Self { registry }
193 }
194}
195
196#[async_trait]
197impl Tool for LspDefinitionTool {
198 fn definition(&self) -> ToolDefinition {
199 ToolDefinition {
200 name: "lsp_definition".to_string(),
201 description: "跳转到定义位置。返回符号定义的文件路径、行号和列号。需要 LSP 服务器支持。".to_string(),
202 parameters: json!({
203 "type": "object",
204 "properties": {
205 "file": {
206 "type": "string",
207 "description": "文件路径(绝对路径)"
208 },
209 "line": {
210 "type": "integer",
211 "description": "行号(0-based)"
212 },
213 "column": {
214 "type": "integer",
215 "description": "列号(0-based)"
216 }
217 },
218 "required": ["file", "line", "column"]
219 }),
220 is_priority: false,
221 }
222 }
223
224 async fn execute(&self, params: Value) -> Result<String> {
225 let file_path = params["file"]
226 .as_str()
227 .ok_or_else(|| anyhow!("缺少 'file' 参数"))?;
228 let line = params["line"]
229 .as_u64()
230 .ok_or_else(|| anyhow!("缺少 'line' 参数"))? as u32;
231 let column = params["column"]
232 .as_u64()
233 .ok_or_else(|| anyhow!("缺少 'column' 参数"))? as u32;
234
235 let path = PathBuf::from(file_path);
236 let language = detect_language_from_path(&path)
237 .ok_or_else(|| anyhow!("无法识别文件类型: {}", file_path))?;
238
239 let client = self.registry.get_client_or_wait(&language).await?;
241
242 let uri = path_to_uri(&path)?;
244 let content = tokio::fs::read_to_string(&path).await
245 .map_err(|e| anyhow!("读取文件失败: {}", e))?;
246 client.open_file(&uri, &content).await?;
247
248 let position = Position { line, character: column };
250 let locations = client.definition(&uri, position).await?;
251
252 if locations.is_empty() {
253 return Ok("未找到定义位置".to_string());
254 }
255
256 let mut result = String::new();
257 if locations.len() == 1 {
258 result.push_str(&format!("定义位于: {}", format_location(&locations[0])));
259 } else {
260 result.push_str(&format!("找到 {} 个定义位置:\n", locations.len()));
261 for (i, loc) in locations.iter().enumerate() {
262 result.push_str(&format!("{}. {}\n", i + 1, format_location(loc)));
263 }
264 }
265
266 Ok(result)
267 }
268
269 fn risk_level(&self) -> RiskLevel {
270 RiskLevel::Safe
271 }
272}
273
274pub struct LspReferencesTool {
280 registry: Arc<LspClientRegistry>,
281}
282
283impl LspReferencesTool {
284 pub fn new(registry: Arc<LspClientRegistry>) -> Self {
285 Self { registry }
286 }
287}
288
289#[async_trait]
290impl Tool for LspReferencesTool {
291 fn definition(&self) -> ToolDefinition {
292 ToolDefinition {
293 name: "lsp_references".to_string(),
294 description: "查找符号的所有引用位置。返回每个引用的文件路径、行号和列号。需要 LSP 服务器支持。".to_string(),
295 parameters: json!({
296 "type": "object",
297 "properties": {
298 "file": {
299 "type": "string",
300 "description": "文件路径(绝对路径)"
301 },
302 "line": {
303 "type": "integer",
304 "description": "行号(0-based)"
305 },
306 "column": {
307 "type": "integer",
308 "description": "列号(0-based)"
309 },
310 "include_declaration": {
311 "type": "boolean",
312 "description": "是否包含声明位置(默认 true)",
313 "default": true
314 }
315 },
316 "required": ["file", "line", "column"]
317 }),
318 is_priority: false,
319 }
320 }
321
322 async fn execute(&self, params: Value) -> Result<String> {
323 let file_path = params["file"]
324 .as_str()
325 .ok_or_else(|| anyhow!("缺少 'file' 参数"))?;
326 let line = params["line"]
327 .as_u64()
328 .ok_or_else(|| anyhow!("缺少 'line' 参数"))? as u32;
329 let column = params["column"]
330 .as_u64()
331 .ok_or_else(|| anyhow!("缺少 'column' 参数"))? as u32;
332 let include_declaration = params["include_declaration"].as_bool().unwrap_or(true);
333
334 let path = PathBuf::from(file_path);
335 let language = detect_language_from_path(&path)
336 .ok_or_else(|| anyhow!("无法识别文件类型: {}", file_path))?;
337
338 let client = self.registry.get_client_or_wait(&language).await?;
340
341 let uri = path_to_uri(&path)?;
343 let content = tokio::fs::read_to_string(&path).await
344 .map_err(|e| anyhow!("读取文件失败: {}", e))?;
345 client.open_file(&uri, &content).await?;
346
347 let position = Position { line, character: column };
349 let locations = client.references(&uri, position, include_declaration).await?;
350
351 if locations.is_empty() {
352 return Ok("未找到引用".to_string());
353 }
354
355 let mut result = format!("找到 {} 个引用:\n", locations.len());
356 for (i, loc) in locations.iter().enumerate() {
357 result.push_str(&format!("{}. {}\n", i + 1, format_location(loc)));
358 }
359
360 Ok(result)
361 }
362
363 fn risk_level(&self) -> RiskLevel {
364 RiskLevel::Safe
365 }
366}
367
368pub struct LspDiagnosticsTool {
374 registry: Arc<LspClientRegistry>,
375}
376
377impl LspDiagnosticsTool {
378 pub fn new(registry: Arc<LspClientRegistry>) -> Self {
379 Self { registry }
380 }
381}
382
383#[async_trait]
384impl Tool for LspDiagnosticsTool {
385 fn definition(&self) -> ToolDefinition {
386 ToolDefinition {
387 name: "lsp_diagnostics".to_string(),
388 description: "获取文件的诊断信息(错误、警告等)。返回诊断类型、消息和位置。需要 LSP 服务器支持。".to_string(),
389 parameters: json!({
390 "type": "object",
391 "properties": {
392 "file": {
393 "type": "string",
394 "description": "文件路径(绝对路径)"
395 }
396 },
397 "required": ["file"]
398 }),
399 is_priority: false,
400 }
401 }
402
403 async fn execute(&self, params: Value) -> Result<String> {
404 let file_path = params["file"]
405 .as_str()
406 .ok_or_else(|| anyhow!("缺少 'file' 参数"))?;
407
408 let path = PathBuf::from(file_path);
409 let language = detect_language_from_path(&path)
410 .ok_or_else(|| anyhow!("无法识别文件类型: {}", file_path))?;
411
412 let client = self.registry.get_client_or_wait(&language).await?;
414
415 let uri = path_to_uri(&path)?;
417 let content = tokio::fs::read_to_string(&path).await
418 .map_err(|e| anyhow!("读取文件失败: {}", e))?;
419 client.open_file(&uri, &content).await?;
420
421 let diagnostics = client.diagnostics(&uri).await?;
423
424 if diagnostics.is_empty() {
425 return Ok(format!("诊断结果 ({}): 无错误或警告", file_path));
426 }
427
428 let mut result = format!("诊断结果 ({}):\n", file_path);
429 for diagnostic in &diagnostics {
430 let severity_icon = match diagnostic.severity {
431 Some(s) => match s {
432 lsp_types::DiagnosticSeverity::ERROR => "❌",
433 lsp_types::DiagnosticSeverity::WARNING => "⚠️",
434 lsp_types::DiagnosticSeverity::INFORMATION => "ℹ️",
435 lsp_types::DiagnosticSeverity::HINT => "💡",
436 _ => "•",
437 },
438 None => "•",
439 };
440
441 let formatted = format_diagnostic(diagnostic);
442 let start = diagnostic.range.start;
443 result.push_str(&format!(
444 "{} {} 位置: {}:{}\n",
445 severity_icon,
446 formatted,
447 start.line + 1,
448 start.character + 1
449 ));
450 }
451
452 Ok(result)
453 }
454
455 fn risk_level(&self) -> RiskLevel {
456 RiskLevel::Safe
457 }
458}
459
460pub fn lsp_tools(registry: Arc<LspClientRegistry>) -> Vec<Box<dyn Tool>> {
466 vec![
467 Box::new(LspHoverTool::new(Arc::clone(®istry))),
468 Box::new(LspDefinitionTool::new(Arc::clone(®istry))),
469 Box::new(LspReferencesTool::new(Arc::clone(®istry))),
470 Box::new(LspDiagnosticsTool::new(registry)),
471 ]
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477
478 #[test]
479 fn test_detect_language_from_path() {
480 assert_eq!(detect_language_from_path(&PathBuf::from("test.rs")), Some("rust".to_string()));
481 assert_eq!(detect_language_from_path(&PathBuf::from("test.ts")), Some("typescript".to_string()));
482 assert_eq!(detect_language_from_path(&PathBuf::from("test.tsx")), Some("typescript".to_string()));
483 assert_eq!(detect_language_from_path(&PathBuf::from("test.js")), Some("javascript".to_string()));
484 assert_eq!(detect_language_from_path(&PathBuf::from("test.py")), Some("python".to_string()));
485 assert_eq!(detect_language_from_path(&PathBuf::from("test.go")), Some("go".to_string()));
486 assert_eq!(detect_language_from_path(&PathBuf::from("test.java")), Some("java".to_string()));
487 assert_eq!(detect_language_from_path(&PathBuf::from("test.unknown")), None);
488 }
489
490 #[test]
491 fn test_lsp_hover_tool_definition() {
492 let registry = Arc::new(LspClientRegistry::new());
493 let tool = LspHoverTool::new(registry);
494 let def = tool.definition();
495 assert_eq!(def.name, "lsp_hover");
496 assert!(def.description.contains("类型签名"));
497 assert_eq!(tool.risk_level(), RiskLevel::Safe);
498 }
499
500 #[test]
501 fn test_lsp_definition_tool_definition() {
502 let registry = Arc::new(LspClientRegistry::new());
503 let tool = LspDefinitionTool::new(registry);
504 let def = tool.definition();
505 assert_eq!(def.name, "lsp_definition");
506 assert!(def.description.contains("定义"));
507 }
508
509 #[test]
510 fn test_lsp_references_tool_definition() {
511 let registry = Arc::new(LspClientRegistry::new());
512 let tool = LspReferencesTool::new(registry);
513 let def = tool.definition();
514 assert_eq!(def.name, "lsp_references");
515 assert!(def.description.contains("引用"));
516 }
517
518 #[test]
519 fn test_lsp_diagnostics_tool_definition() {
520 let registry = Arc::new(LspClientRegistry::new());
521 let tool = LspDiagnosticsTool::new(registry);
522 let def = tool.definition();
523 assert_eq!(def.name, "lsp_diagnostics");
524 assert!(def.description.contains("诊断"));
525 }
526
527 #[test]
528 fn test_lsp_tools_creates_all_tools() {
529 let registry = Arc::new(LspClientRegistry::new());
530 let tools = lsp_tools(registry);
531 assert_eq!(tools.len(), 4);
532 }
533}