Skip to main content

matrixcode_core/lsp/
tools.rs

1//! LSP Tools for AI Agents
2//!
3//! Provides 4 LSP tools for AI to call:
4//! - `lsp_hover`: Get type signature and documentation
5//! - `lsp_definition`: Jump to definition
6//! - `lsp_references`: Find all references
7//! - `lsp_diagnostics`: Get diagnostics for a file
8
9use 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
21// ============================================================================
22// Helper Functions
23// ============================================================================
24
25/// Detect language identifier from file path extension
26pub 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        // Rust
30        "rs" => "rust",
31        // TypeScript/JavaScript
32        "ts" => "typescript",
33        "tsx" => "typescript",
34        "js" => "javascript",
35        "jsx" => "javascript",
36        "mjs" => "javascript",
37        "cjs" => "javascript",
38        // Python
39        "py" => "python",
40        "pyi" => "python",
41        // Go
42        "go" => "go",
43        // C/C++
44        "c" => "c",
45        "h" => "c",
46        "cpp" => "cpp",
47        "hpp" => "cpp",
48        "cc" => "cpp",
49        "cxx" => "cpp",
50        // Java
51        "java" => "java",
52        // C#
53        "cs" => "csharp",
54        // Ruby
55        "rb" => "ruby",
56        // PHP
57        "php" => "php",
58        // Swift
59        "swift" => "swift",
60        // Kotlin
61        "kt" => "kotlin",
62        "kts" => "kotlin",
63        // Scala
64        "scala" => "scala",
65        // Lua
66        "lua" => "lua",
67        // Rust
68        "vue" => "vue",
69        // Svelte
70        "svelte" => "svelte",
71        // HTML/CSS
72        "html" => "html",
73        "htm" => "html",
74        "css" => "css",
75        "scss" => "scss",
76        "less" => "less",
77        // JSON/YAML/TOML
78        "json" => "json",
79        "yaml" => "yaml",
80        "yml" => "yaml",
81        "toml" => "toml",
82        // Markdown
83        "md" => "markdown",
84        // Shell
85        "sh" => "shell",
86        "bash" => "shell",
87        "zsh" => "shell",
88        // Other
89        _ => return None,
90    };
91    Some(language.to_string())
92}
93
94// ============================================================================
95// LSP Hover Tool
96// ============================================================================
97
98/// Tool for getting hover information (type signature and documentation)
99pub 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(&language).await
153            .ok_or_else(|| anyhow!("语言 '{}' 的 LSP 客户端未启动", language))?;
154
155        // Open file first
156        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        // Get hover
162        let position = Position { line, character: column };
163        let hover = client.hover(&uri, position).await?
164            .ok_or_else(|| anyhow!("该位置没有 hover 信息"))?;
165
166        // Format output
167        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
181// ============================================================================
182// LSP Definition Tool
183// ============================================================================
184
185/// Tool for jumping to definition
186pub 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(&language).await
240            .ok_or_else(|| anyhow!("语言 '{}' 的 LSP 客户端未启动", language))?;
241
242        // Open file first
243        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        // Get definition
249        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
274// ============================================================================
275// LSP References Tool
276// ============================================================================
277
278/// Tool for finding all references
279pub 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(&language).await
339            .ok_or_else(|| anyhow!("语言 '{}' 的 LSP 客户端未启动", language))?;
340
341        // Open file first
342        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        // Get references
348        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
368// ============================================================================
369// LSP Diagnostics Tool
370// ============================================================================
371
372/// Tool for getting diagnostics (errors, warnings) for a file
373pub 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(&language).await
413            .ok_or_else(|| anyhow!("语言 '{}' 的 LSP 客户端未启动", language))?;
414
415        // Open file first to trigger diagnostics
416        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        // Get diagnostics
422        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
460// ============================================================================
461// Factory Function
462// ============================================================================
463
464/// Create all LSP tools with the given registry
465pub fn lsp_tools(registry: Arc<LspClientRegistry>) -> Vec<Box<dyn Tool>> {
466    vec![
467        Box::new(LspHoverTool::new(Arc::clone(&registry))),
468        Box::new(LspDefinitionTool::new(Arc::clone(&registry))),
469        Box::new(LspReferencesTool::new(Arc::clone(&registry))),
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}