1use ast_grep_core::Pattern;
2use ast_grep_language::{LanguageExt, SupportLang};
3use async_trait::async_trait;
4use ignore::WalkBuilder;
5use limit_agent::error::AgentError;
6use limit_agent::Tool;
7use regex::Regex;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use std::fs;
11use std::path::{Path, PathBuf};
12use std::process::Command;
13use tracing::{debug, info};
14
15const GREP_MAX_RESULTS: usize = 1000;
16const GREP_CONTEXT_LINES: usize = 3;
17
18#[derive(Debug, Clone, Deserialize, Serialize)]
19pub struct Position {
20 pub line: u32,
21 pub character: u32,
22}
23pub struct GrepTool;
24
25impl GrepTool {
26 pub fn new() -> Self {
27 GrepTool
28 }
29}
30
31impl Default for GrepTool {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37#[async_trait]
38impl Tool for GrepTool {
39 fn name(&self) -> &str {
40 "grep"
41 }
42
43 async fn execute(&self, args: Value) -> Result<Value, AgentError> {
44 let pattern: String = serde_json::from_value(args["pattern"].clone())
45 .map_err(|e| AgentError::ToolError(format!("Invalid pattern argument: {}", e)))?;
46
47 if pattern.trim().is_empty() {
48 return Err(AgentError::ToolError(
49 "pattern argument cannot be empty".to_string(),
50 ));
51 }
52
53 Regex::new(&pattern)
55 .map_err(|e| AgentError::ToolError(format!("Invalid regex pattern: {}", e)))?;
56
57 let default_path = std::env::current_dir()
58 .unwrap_or_else(|_| PathBuf::from("."))
59 .to_string_lossy()
60 .to_string();
61 let path = args
62 .get("path")
63 .and_then(|v| v.as_str())
64 .unwrap_or(&default_path);
65
66 if !Path::new(path).exists() {
68 return Err(AgentError::ToolError(format!("Path not found: {}", path)));
69 }
70
71 let mut cmd = Command::new("grep");
73 cmd.arg("-r")
74 .arg("-n")
75 .arg("-I") .arg("--color=never")
77 .args(["-C", &GREP_CONTEXT_LINES.to_string()])
78 .arg(&pattern)
79 .arg(path);
80
81 let output = cmd
82 .output()
83 .map_err(|e| AgentError::ToolError(format!("Failed to execute grep: {}", e)))?;
84
85 if !output.status.success() {
86 let stderr = String::from_utf8_lossy(&output.stderr);
88 if !stderr.is_empty() && !stderr.contains("No such file") {
89 return Err(AgentError::ToolError(format!("grep failed: {}", stderr)));
90 }
91 }
92
93 let stdout = String::from_utf8_lossy(&output.stdout);
94 let lines: Vec<&str> = stdout.lines().collect();
95
96 let limited_lines = if lines.len() > GREP_MAX_RESULTS {
98 lines[..GREP_MAX_RESULTS].to_vec()
99 } else {
100 lines
101 };
102
103 let mut matches = Vec::new();
105 for line in limited_lines {
106 if let Some((rest, content)) = line.split_once(':') {
108 if let Some((file_path, line_number)) = rest.split_once(':') {
109 if let Ok(line_num) = line_number.parse::<usize>() {
110 matches.push(serde_json::json!({
111 "file": file_path,
112 "line": line_num,
113 "content": content
114 }));
115 }
116 }
117 }
118 }
119
120 Ok(serde_json::json!({
121 "matches": matches,
122 "count": matches.len(),
123 "pattern": pattern
124 }))
125 }
126}
127
128pub struct AstGrepTool;
129
130impl AstGrepTool {
131 pub fn new() -> Self {
132 AstGrepTool
133 }
134
135 fn get_language_support(lang: &str) -> Result<SupportLang, AgentError> {
136 lang.parse()
137 .map_err(|_| AgentError::ToolError(format!("Unsupported language: {}. Use a valid language name or alias (e.g., rs, py, js, rust, python, javascript).", lang)))
138 }
139
140 async fn execute_search(&self, args: Value) -> Result<Value, AgentError> {
141 let pattern: String = serde_json::from_value(args["pattern"].clone())
142 .map_err(|e| AgentError::ToolError(format!("Invalid pattern argument: {}", e)))?;
143
144 if pattern.trim().is_empty() {
145 return Err(AgentError::ToolError(
146 "pattern argument cannot be empty".to_string(),
147 ));
148 }
149
150 let language: String = serde_json::from_value(args["language"].clone())
151 .map_err(|e| AgentError::ToolError(format!("Invalid language argument: {}", e)))?;
152
153 let lang = Self::get_language_support(&language)?;
154
155 let default_path = std::env::current_dir()
156 .unwrap_or_else(|_| PathBuf::from("."))
157 .to_string_lossy()
158 .to_string();
159 let path = args
160 .get("path")
161 .and_then(|v| v.as_str())
162 .unwrap_or(&default_path);
163
164 let path_obj = Path::new(path);
165 if !path_obj.exists() {
166 return Err(AgentError::ToolError(format!("Path not found: {}", path)));
167 }
168
169 debug!("ast_grep: searching in path={}", path);
170
171 let context_after = args
172 .get("context_after")
173 .and_then(|v| v.as_u64())
174 .unwrap_or(0);
175 let context_before = args
176 .get("context_before")
177 .and_then(|v| v.as_u64())
178 .unwrap_or(0);
179
180 let globs: Option<Vec<String>> = args.get("globs").and_then(|v| {
181 v.as_array().map(|arr| {
182 arr.iter()
183 .filter_map(|val| val.as_str().map(String::from))
184 .collect()
185 })
186 });
187
188 let mut all_matches = Vec::new();
189
190 let search_pattern = Pattern::try_new(&pattern, lang)
191 .map_err(|e| AgentError::ToolError(format!("Invalid pattern: {}", e)))?;
192
193 if path_obj.is_file() {
194 let content = fs::read_to_string(path_obj)
195 .map_err(|e| AgentError::ToolError(format!("Failed to read file: {}", e)))?;
196
197 let grep = lang.ast_grep(&content);
198
199 for match_ in grep.root().find_all(&search_pattern) {
200 let line = match_.start_pos().line();
201 let text = match_.text();
202
203 let mut match_obj = serde_json::json!({
204 "file": path,
205 "line": line,
206 "text": text,
207 "language": language
208 });
209
210 if context_after > 0 || context_before > 0 {
211 let lines: Vec<&str> = content.lines().collect();
212 let start_line = line.saturating_sub(context_before as usize);
213 let end_line = (line + context_after as usize + 1).min(lines.len());
214
215 let context_lines: Vec<String> = lines[start_line..end_line]
216 .iter()
217 .map(|s: &&str| s.to_string())
218 .collect();
219
220 match_obj["context_lines"] = serde_json::json!(context_lines);
221 }
222
223 all_matches.push(match_obj);
224 }
225 } else {
226 let mut builder = WalkBuilder::new(path);
227
228 if let Some(ref glob_patterns) = globs {
229 let mut override_builder = ignore::overrides::OverrideBuilder::new(path);
230 for glob in glob_patterns {
231 if let Err(e) = override_builder.add(glob) {
232 return Err(AgentError::ToolError(format!(
233 "Invalid glob pattern '{}': {}",
234 glob, e
235 )));
236 }
237 }
238 if let Ok(overrides) = override_builder.build() {
239 builder.overrides(overrides);
240 }
241 }
242
243 let mut files_walked = 0usize;
244 let mut files_matched_lang = 0usize;
245 let mut files_rejected_lang = 0usize;
246 let mut files_no_extension = 0usize;
247 let mut files_read_errors = 0usize;
248
249 for entry in builder.build().filter_map(|e| e.ok()) {
250 if entry.file_type().is_some_and(|ft| ft.is_file()) {
251 files_walked += 1;
252 let file_path = entry.path();
253
254 if let Some(ext) = file_path.extension().and_then(|e| e.to_str()) {
255 let ext_lower = ext.to_lowercase();
256 let lang_str = lang.to_string().to_lowercase();
257
258 let matches_lang = match lang_str.as_str() {
259 "rust" => ext_lower == "rs",
260 "python" => ext_lower == "py",
261 "javascript" => ext_lower == "js",
262 "typescript" => ext_lower == "ts",
263 "tsx" => ext_lower == "tsx",
264 "go" => ext_lower == "go",
265 "java" => ext_lower == "java",
266 "c" => ext_lower == "c",
267 "cpp" => ext_lower == "cpp" || ext_lower == "cc" || ext_lower == "cxx",
268 "csharp" => ext_lower == "cs",
269 "ruby" => ext_lower == "rb",
270 "php" => ext_lower == "php",
271 "swift" => ext_lower == "swift",
272 "kotlin" => ext_lower == "kt",
273 "scala" => ext_lower == "scala",
274 "haskell" => ext_lower == "hs",
275 "lua" => ext_lower == "lua",
276 "elixir" => ext_lower == "ex",
277 "nix" => ext_lower == "nix",
278 "solidity" => ext_lower == "sol",
279 "bash" => ext_lower == "sh" || ext_lower == "bash",
280 "yaml" => ext_lower == "yaml" || ext_lower == "yml",
281 "json" => ext_lower == "json",
282 "html" => ext_lower == "html" || ext_lower == "htm",
283 "css" => ext_lower == "css",
284 _ => false,
285 };
286
287 if files_rejected_lang < 3 && ext_lower == "rs" {
288 debug!(
289 "ast_grep: file={}, ext={}, lang={}, matches_lang={}",
290 file_path.display(),
291 ext_lower,
292 lang_str,
293 matches_lang
294 );
295 }
296
297 if !matches_lang {
298 files_rejected_lang += 1;
299 continue;
300 }
301 } else {
302 files_no_extension += 1;
303 continue;
304 }
305
306 files_matched_lang += 1;
307 let content = match fs::read_to_string(file_path) {
308 Ok(c) => c,
309 Err(e) => {
310 files_read_errors += 1;
311 debug!("ast_grep: failed to read {}: {}", file_path.display(), e);
312 continue;
313 }
314 };
315
316 let grep = lang.ast_grep(&content);
317
318 for match_ in grep.root().find_all(&search_pattern) {
319 let line = match_.start_pos().line();
320 let text = match_.text();
321 let display_path = file_path.display().to_string();
322
323 let mut match_obj = serde_json::json!({
324 "file": display_path,
325 "line": line,
326 "text": text,
327 "language": language
328 });
329
330 if context_after > 0 || context_before > 0 {
331 let lines: Vec<&str> = content.lines().collect();
332 let start_line = line.saturating_sub(context_before as usize);
333 let end_line = (line + context_after as usize + 1).min(lines.len());
334
335 let context_lines: Vec<String> = lines[start_line..end_line]
336 .iter()
337 .map(|s: &&str| s.to_string())
338 .collect();
339
340 match_obj["context_lines"] = serde_json::json!(context_lines);
341 }
342
343 all_matches.push(match_obj);
344 }
345 }
346 }
347 debug!(
348 "ast_grep search stats: files_walked={}, files_matched_lang={}, files_rejected_lang={}, files_no_extension={}, files_read_errors={}, matches_found={}",
349 files_walked, files_matched_lang, files_rejected_lang, files_no_extension, files_read_errors, all_matches.len()
350 );
351 }
352
353 Ok(serde_json::json!({
354 "matches": all_matches,
355 "count": all_matches.len(),
356 "pattern": pattern,
357 "language": language,
358 "command": "search"
359 }))
360 }
361
362 async fn execute_replace(&self, args: Value) -> Result<Value, AgentError> {
363 let pattern: String = serde_json::from_value(args["pattern"].clone())
364 .map_err(|e| AgentError::ToolError(format!("Invalid pattern argument: {}", e)))?;
365
366 if pattern.trim().is_empty() {
367 return Err(AgentError::ToolError(
368 "pattern argument cannot be empty".to_string(),
369 ));
370 }
371
372 let language: String = serde_json::from_value(args["language"].clone())
373 .map_err(|e| AgentError::ToolError(format!("Invalid language argument: {}", e)))?;
374
375 let rewrite: String = serde_json::from_value(args["rewrite"].clone())
376 .map_err(|e| AgentError::ToolError(format!("Invalid rewrite argument: {}", e)))?;
377
378 if rewrite.trim().is_empty() {
379 return Err(AgentError::ToolError(
380 "rewrite argument cannot be empty".to_string(),
381 ));
382 }
383
384 let lang = Self::get_language_support(&language)?;
385
386 let default_path = std::env::current_dir()
387 .unwrap_or_else(|_| PathBuf::from("."))
388 .to_string_lossy()
389 .to_string();
390 let path = args
391 .get("path")
392 .and_then(|v| v.as_str())
393 .unwrap_or(&default_path);
394
395 let dry_run = args
396 .get("dry_run")
397 .and_then(|v| v.as_bool())
398 .unwrap_or(false);
399
400 let path_obj = Path::new(path);
401 if !path_obj.exists() {
402 return Err(AgentError::ToolError(format!("Path not found: {}", path)));
403 }
404
405 let globs: Option<Vec<String>> = args.get("globs").and_then(|v| {
406 v.as_array().map(|arr| {
407 arr.iter()
408 .filter_map(|val| val.as_str().map(String::from))
409 .collect()
410 })
411 });
412
413 let mut all_matches = Vec::new();
414
415 if path_obj.is_file() {
416 let content = fs::read_to_string(path_obj)
417 .map_err(|e| AgentError::ToolError(format!("Failed to read file: {}", e)))?;
418
419 let search_pattern = Pattern::try_new(&pattern, lang)
420 .map_err(|e| AgentError::ToolError(format!("Invalid pattern: {}", e)))?;
421
422 let grep = lang.ast_grep(&content);
423
424 for match_ in grep.root().find_all(&search_pattern) {
425 let text = match_.text();
426 all_matches.push(serde_json::json!({
427 "file": path,
428 "text": text
429 }));
430 }
431
432 if !all_matches.is_empty() && !dry_run {
433 let mut content = content;
434 loop {
435 let mut grep = lang.ast_grep(&content);
436 let replaced =
437 grep.replace(pattern.as_str(), rewrite.as_str())
438 .map_err(|e| {
439 AgentError::ToolError(format!("Failed to apply pattern: {}", e))
440 })?;
441 if !replaced {
442 break;
443 }
444 content = grep.generate();
445 }
446 fs::write(path_obj, content)
447 .map_err(|e| AgentError::ToolError(format!("Failed to write file: {}", e)))?;
448 }
449 } else {
450 let mut builder = WalkBuilder::new(path);
451
452 if let Some(ref glob_patterns) = globs {
453 let mut override_builder = ignore::overrides::OverrideBuilder::new(path);
454 for glob in glob_patterns {
455 if let Err(e) = override_builder.add(glob) {
456 return Err(AgentError::ToolError(format!(
457 "Invalid glob pattern '{}': {}",
458 glob, e
459 )));
460 }
461 }
462 if let Ok(overrides) = override_builder.build() {
463 builder.overrides(overrides);
464 }
465 }
466
467 for entry in builder.build().filter_map(|e| e.ok()) {
468 if entry.file_type().is_some_and(|ft| ft.is_file()) {
469 let file_path = entry.path();
470
471 if let Some(ext) = file_path.extension().and_then(|e| e.to_str()) {
472 let ext_lower = ext.to_lowercase();
473 let lang_str = lang.to_string().to_lowercase();
474
475 let matches_lang = match lang_str.as_str() {
476 "rust" => ext_lower == "rs",
477 "python" => ext_lower == "py",
478 "javascript" => ext_lower == "js",
479 "typescript" => ext_lower == "ts",
480 "tsx" => ext_lower == "tsx",
481 "go" => ext_lower == "go",
482 "java" => ext_lower == "java",
483 "c" => ext_lower == "c",
484 "cpp" => ext_lower == "cpp" || ext_lower == "cc" || ext_lower == "cxx",
485 "csharp" => ext_lower == "cs",
486 "ruby" => ext_lower == "rb",
487 "php" => ext_lower == "php",
488 "swift" => ext_lower == "swift",
489 "kotlin" => ext_lower == "kt",
490 "scala" => ext_lower == "scala",
491 "haskell" => ext_lower == "hs",
492 "lua" => ext_lower == "lua",
493 "elixir" => ext_lower == "ex",
494 "nix" => ext_lower == "nix",
495 "solidity" => ext_lower == "sol",
496 "bash" => ext_lower == "sh" || ext_lower == "bash",
497 "yaml" => ext_lower == "yaml" || ext_lower == "yml",
498 "json" => ext_lower == "json",
499 "html" => ext_lower == "html" || ext_lower == "htm",
500 "css" => ext_lower == "css",
501 _ => false,
502 };
503
504 if !matches_lang {
505 continue;
506 }
507 }
508
509 let display_path = file_path.display().to_string();
510 let content = match fs::read_to_string(file_path) {
511 Ok(c) => c,
512 Err(_) => continue,
513 };
514
515 let search_pattern = Pattern::try_new(&pattern, lang)
516 .map_err(|e| AgentError::ToolError(format!("Invalid pattern: {}", e)))?;
517
518 let grep = lang.ast_grep(&content);
519
520 let file_matches: Vec<serde_json::Value> = grep
521 .root()
522 .find_all(&search_pattern)
523 .map(|match_| {
524 let text = match_.text();
525 serde_json::json!({
526 "file": display_path,
527 "text": text
528 })
529 })
530 .collect();
531
532 if !file_matches.is_empty() && !dry_run {
533 let mut file_content = content;
534 loop {
535 let mut grep = lang.ast_grep(&file_content);
536 let replaced = grep
537 .replace(pattern.as_str(), rewrite.as_str())
538 .map_err(|e| {
539 AgentError::ToolError(format!("Failed to apply pattern: {}", e))
540 })?;
541 if !replaced {
542 break;
543 }
544 file_content = grep.generate();
545 }
546 if let Err(e) = fs::write(file_path, file_content) {
547 return Err(AgentError::ToolError(format!(
548 "Failed to write file {}: {}",
549 display_path, e
550 )));
551 }
552 }
553
554 all_matches.extend(file_matches);
555 }
556 }
557 }
558
559 Ok(serde_json::json!({
560 "matches": all_matches,
561 "count": all_matches.len(),
562 "pattern": pattern,
563 "language": language,
564 "rewrite": rewrite,
565 "dry_run": dry_run,
566 "command": "replace"
567 }))
568 }
569
570 async fn execute_scan(&self, _args: Value) -> Result<Value, AgentError> {
571 Err(AgentError::ToolError(
572 "scan command is not yet supported via ast-grep crates. Please use the search or replace commands.".to_string(),
573 ))
574 }
575}
576
577impl Default for AstGrepTool {
578 fn default() -> Self {
579 Self::new()
580 }
581}
582
583#[async_trait]
584impl Tool for AstGrepTool {
585 fn name(&self) -> &str {
586 "ast_grep"
587 }
588
589 async fn execute(&self, args: Value) -> Result<Value, AgentError> {
590 let command = args
591 .get("command")
592 .and_then(|v| v.as_str())
593 .unwrap_or("search");
594
595 debug!(
596 "ast_grep invoked: command={}, pattern={:?}, language={:?}, path={:?}",
597 command,
598 args.get("pattern").and_then(|v| v.as_str()),
599 args.get("language").and_then(|v| v.as_str()),
600 args.get("path").and_then(|v| v.as_str())
601 );
602
603 let result = match command {
604 "search" => self.execute_search(args).await,
605 "replace" => self.execute_replace(args).await,
606 "scan" => self.execute_scan(args).await,
607 _ => Err(AgentError::ToolError(format!(
608 "Unsupported command: {}. Supported: search, replace, scan",
609 command
610 ))),
611 };
612
613 match &result {
614 Ok(value) => {
615 if let Some(obj) = value.as_object() {
616 let count = obj.get("count").and_then(|v| v.as_u64()).unwrap_or(0);
617 info!("ast_grep result: {} matches", count);
618 } else {
619 info!("ast_grep result: {:?}", value);
620 }
621 }
622 Err(e) => debug!("ast_grep error: {}", e),
623 }
624
625 result
626 }
627}
628
629pub struct LspTool;
630
631impl LspTool {
632 pub fn new() -> Self {
633 LspTool
634 }
635
636 fn get_lsp_server(file_path: &Path) -> Result<String, AgentError> {
637 let extension = file_path
638 .extension()
639 .and_then(|ext| ext.to_str())
640 .unwrap_or("");
641
642 match extension {
643 "rs" => Ok("rust-analyzer".to_string()),
644 "ts" | "tsx" | "js" | "jsx" => Ok("typescript-language-server".to_string()),
645 "py" => Ok("pylsp".to_string()),
646 _ => Err(AgentError::ToolError(format!(
647 "Unsupported file extension: {}. Supported: rs, ts, tsx, js, jsx, py",
648 extension
649 ))),
650 }
651 }
652
653 fn check_lsp_server_available(server_name: &str) -> Result<(), AgentError> {
654 let result = Command::new(server_name).arg("--version").output();
655
656 match result {
657 Ok(output) if output.status.success() => Ok(()),
658 Ok(_) => Err(AgentError::ToolError(format!(
659 "LSP server {} failed to execute",
660 server_name
661 ))),
662 Err(_) => Err(AgentError::ToolError(format!(
663 "LSP server {} not found in PATH. Please install it to use LSP features.",
664 server_name
665 ))),
666 }
667 }
668}
669
670impl Default for LspTool {
671 fn default() -> Self {
672 Self::new()
673 }
674}
675
676#[async_trait]
677impl Tool for LspTool {
678 fn name(&self) -> &str {
679 "lsp"
680 }
681
682 async fn execute(&self, args: Value) -> Result<Value, AgentError> {
683 let command: String = serde_json::from_value(args["command"].clone())
684 .map_err(|e| AgentError::ToolError(format!("Invalid command argument: {}", e)))?;
685
686 match command.as_str() {
688 "goto_definition" | "find_references" => {}
689 _ => {
690 return Err(AgentError::ToolError(format!(
691 "Unsupported LSP command: {}. Supported: goto_definition, find_references",
692 command
693 )));
694 }
695 }
696
697 let file_path: String = serde_json::from_value(args["file_path"].clone())
698 .map_err(|e| AgentError::ToolError(format!("Invalid file_path argument: {}", e)))?;
699
700 if !Path::new(&file_path).exists() {
701 return Err(AgentError::ToolError(format!(
702 "File not found: {}",
703 file_path
704 )));
705 }
706
707 let position: Position = serde_json::from_value(args["position"].clone())
708 .map_err(|e| AgentError::ToolError(format!("Invalid position argument: {}", e)))?;
709 let lsp_server = Self::get_lsp_server(Path::new(&file_path))?;
710 Self::check_lsp_server_available(&lsp_server)?;
711
712 match command.as_str() {
719 "goto_definition" => Ok(serde_json::json!({
720 "command": command,
721 "file_path": file_path,
722 "position": position,
723 "result": "LSP goto_definition requires full LSP client implementation",
724 "note": "This is a placeholder. Implement full LSP client for production use."
725 })),
726 "find_references" => Ok(serde_json::json!({
727 "command": command,
728 "file_path": file_path,
729 "position": position,
730 "result": "LSP find_references requires full LSP client implementation",
731 "note": "This is a placeholder. Implement full LSP client for production use."
732 })),
733 _ => unreachable!(),
734 }
735 }
736}
737#[cfg(test)]
738mod tests {
739 use super::*;
740 use std::io::Write;
741 use tempfile::NamedTempFile;
742
743 #[tokio::test]
744 async fn test_grep_tool_name() {
745 let tool = GrepTool::new();
746 assert_eq!(tool.name(), "grep");
747 }
748
749 #[tokio::test]
750 async fn test_grep_tool_default() {
751 let tool = GrepTool;
752 assert_eq!(tool.name(), "grep");
753 }
754
755 #[tokio::test]
756 async fn test_grep_tool_empty_pattern() {
757 let tool = GrepTool::new();
758 let args = serde_json::json!({
759 "pattern": ""
760 });
761
762 let result = tool.execute(args).await;
763 assert!(result.is_err());
764 assert!(result.unwrap_err().to_string().contains("cannot be empty"));
765 }
766
767 #[tokio::test]
768 async fn test_lsp_tool_unsupported_extension() {
769 let tool = LspTool::new();
770
771 let mut temp_file = NamedTempFile::new().unwrap();
772 writeln!(temp_file, "test").unwrap();
773
774 let args = serde_json::json!({
775 "command": "goto_definition",
776 "file_path": temp_file.path(),
777 "position": {"line": 1, "character": 0}
778 });
779
780 let result = tool.execute(args).await;
781 assert!(result.is_err());
782 assert!(result
783 .unwrap_err()
784 .to_string()
785 .contains("Unsupported file extension"));
786 }
787
788 #[tokio::test]
789 async fn test_lsp_tool_missing_server() {
790 let tool = LspTool::new();
791
792 let temp_dir = tempfile::tempdir().unwrap();
794 let rust_file = temp_dir.path().join("test.rs");
795 std::fs::write(&rust_file, "fn main() {}").unwrap();
796
797 let args = serde_json::json!({
798 "command": "goto_definition",
799 "file_path": rust_file,
800 "position": {"line": 0, "character": 0}
801 });
802 let result = tool.execute(args).await;
805
806 match result {
808 Ok(value) => {
809 assert!(value["command"] == "goto_definition");
811 }
812 Err(e) => {
813 let error_msg = e.to_string();
814 assert!(
815 error_msg.contains("not found in PATH")
816 || error_msg.contains("failed to execute"),
817 "Unexpected error: {}",
818 error_msg
819 );
820 }
821 }
822 }
823
824 #[tokio::test]
825 async fn test_ast_grep_search_single_file() {
826 let tool = AstGrepTool::new();
827
828 let mut temp_file = NamedTempFile::new().unwrap();
829 writeln!(temp_file, "fn foo() {{}}").unwrap();
830 writeln!(temp_file, "fn bar() {{}}").unwrap();
831 temp_file.flush().unwrap();
832
833 let args = serde_json::json!({
834 "pattern": "fn $NAME() {}",
835 "language": "rust",
836 "path": temp_file.path()
837 });
838
839 let result = tool.execute(args).await;
840 assert!(result.is_ok());
841 let value = result.unwrap();
842 assert_eq!(value["count"], 2);
843 assert_eq!(value["matches"].as_array().unwrap().len(), 2);
844 assert_eq!(value["command"], "search");
845 }
846
847 #[tokio::test]
848 async fn test_ast_grep_search_multi_file() {
849 let tool = AstGrepTool::new();
850
851 let mut temp_file1 = NamedTempFile::new().unwrap();
852 writeln!(temp_file1, "fn foo() {{}}").unwrap();
853 writeln!(temp_file1, "fn bar() {{}}").unwrap();
854 temp_file1.flush().unwrap();
855
856 let mut temp_file2 = NamedTempFile::new().unwrap();
857 writeln!(temp_file2, "fn baz() {{}}").unwrap();
858 temp_file2.flush().unwrap();
859
860 let args1 = serde_json::json!({
861 "pattern": "fn $NAME() {}",
862 "language": "rust",
863 "path": temp_file1.path()
864 });
865
866 let args2 = serde_json::json!({
867 "pattern": "fn $NAME() {}",
868 "language": "rust",
869 "path": temp_file2.path()
870 });
871
872 let result1 = tool.execute(args1).await;
873 let result2 = tool.execute(args2).await;
874
875 assert!(result1.is_ok());
876 assert!(result2.is_ok());
877
878 let value1 = result1.unwrap();
879 let value2 = result2.unwrap();
880
881 let count1 = value1["count"].as_u64().unwrap_or(0);
882 let count2 = value2["count"].as_u64().unwrap_or(0);
883
884 assert_eq!(count1, 2);
885 assert_eq!(count2, 1);
886 }
887
888 #[tokio::test]
889 async fn test_ast_grep_search_with_globs() {
890 let tool = AstGrepTool::new();
891
892 let mut temp_file = NamedTempFile::new().unwrap();
893 writeln!(temp_file, "fn foo() {{}}").unwrap();
894 writeln!(temp_file, "fn bar() {{}}").unwrap();
895 temp_file.flush().unwrap();
896
897 let args = serde_json::json!({
898 "pattern": "fn $NAME() {}",
899 "language": "rust",
900 "path": temp_file.path(),
901 "globs": ["*.rs"]
902 });
903
904 let result = tool.execute(args).await;
905 assert!(result.is_ok());
906 let value = result.unwrap();
907 assert_eq!(value["count"], 2);
908 }
909
910 #[tokio::test]
911 async fn test_ast_grep_search_no_match() {
912 let tool = AstGrepTool::new();
913
914 let mut temp_file = NamedTempFile::new().unwrap();
915 writeln!(temp_file, "fn foo() {{}}").unwrap();
916 temp_file.flush().unwrap();
917
918 let args = serde_json::json!({
919 "pattern": "fn bar() {}",
920 "language": "rust",
921 "path": temp_file.path()
922 });
923
924 let result = tool.execute(args).await;
925 assert!(result.is_ok());
926 let value = result.unwrap();
927 assert_eq!(value["count"], 0);
928 assert_eq!(value["matches"].as_array().unwrap().len(), 0);
929 }
930
931 #[tokio::test]
932 async fn test_ast_grep_replace_dry_run() {
933 let tool = AstGrepTool::new();
934
935 let mut temp_file = NamedTempFile::new().unwrap();
936 writeln!(temp_file, "var x = 1;").unwrap();
937 writeln!(temp_file, "var y = 2;").unwrap();
938 temp_file.flush().unwrap();
939 let path = temp_file.path().to_path_buf();
940
941 let args = serde_json::json!({
942 "command": "replace",
943 "pattern": "var $A = $B;",
944 "rewrite": "let $A = $B;",
945 "language": "javascript",
946 "path": &path,
947 "dry_run": true
948 });
949
950 let result = tool.execute(args).await;
951 assert!(result.is_ok());
952 let value = result.unwrap();
953 assert_eq!(value["count"], 2);
954 assert_eq!(value["dry_run"], true);
955
956 let content = fs::read_to_string(&path).unwrap();
957 assert!(content.contains("var x = 1;"));
958 assert!(content.contains("var y = 2;"));
959 }
960
961 #[tokio::test]
962 async fn test_ast_grep_replace_writes_file() {
963 let tool = AstGrepTool::new();
964
965 let mut temp_file = NamedTempFile::new().unwrap();
966 writeln!(temp_file, "var x = 1;").unwrap();
967 writeln!(temp_file, "var y = 2;").unwrap();
968 temp_file.flush().unwrap();
969 let path = temp_file.path().to_path_buf();
970
971 let args = serde_json::json!({
972 "command": "replace",
973 "pattern": "var $A = $B;",
974 "rewrite": "let $A = $B;",
975 "language": "javascript",
976 "path": &path,
977 "dry_run": false
978 });
979
980 let result = tool.execute(args).await;
981 assert!(result.is_ok());
982 let value = result.unwrap();
983 assert_eq!(value["count"], 2);
984 assert_eq!(value["dry_run"], false);
985
986 let content = fs::read_to_string(&path).unwrap();
987 assert!(content.contains("let x = 1;"));
988 assert!(content.contains("let y = 2;"));
989 assert!(!content.contains("var x = 1;"));
990 assert!(!content.contains("var y = 2;"));
991 }
992
993 #[tokio::test]
994 async fn test_ast_grep_language_case_insensitive() {
995 let tool = AstGrepTool::new();
996
997 let mut temp_file = NamedTempFile::new().unwrap();
998 writeln!(temp_file, "fn foo() {{}}").unwrap();
999 temp_file.flush().unwrap();
1000
1001 for lang in ["RUST", "Rust", "rust"] {
1002 let args = serde_json::json!({
1003 "pattern": "fn $NAME() {}",
1004 "language": lang,
1005 "path": temp_file.path()
1006 });
1007
1008 let result = tool.execute(args).await;
1009 assert!(result.is_ok(), "Failed for language: {}", lang);
1010 let value = result.unwrap();
1011 assert_eq!(value["count"], 1);
1012 }
1013 }
1014
1015 #[tokio::test]
1016 async fn test_all_tools_implement_default() {
1017 let _grep = GrepTool;
1018 let _ast_grep = AstGrepTool;
1019 let _lsp = LspTool;
1020 }
1021
1022 #[tokio::test]
1023 async fn test_position_deserialize() {
1024 let json = serde_json::json!({"line": 10, "character": 5});
1025 let pos: Position = serde_json::from_value(json).unwrap();
1026 assert_eq!(pos.line, 10);
1027 assert_eq!(pos.character, 5);
1028 }
1029
1030 #[tokio::test]
1031 async fn test_ast_grep_tool_new_language_go() {
1032 let tool = AstGrepTool::new();
1033
1034 let mut temp_file = NamedTempFile::with_suffix(".go").unwrap();
1035 writeln!(temp_file, "func foo() {{}}").unwrap();
1036 writeln!(temp_file, "func bar() {{}}").unwrap();
1037 temp_file.flush().unwrap();
1038
1039 let args = serde_json::json!({
1040 "pattern": "func $NAME($$$) { }",
1041 "language": "go",
1042 "path": temp_file.path()
1043 });
1044
1045 let result = tool.execute(args).await;
1046 assert!(result.is_ok(), "Expected Ok, got Err: {:?}", result.err());
1047 let value = result.unwrap();
1048 assert_eq!(value["count"], 2);
1049 assert_eq!(value["command"], "search");
1050 }
1051
1052 #[tokio::test]
1053 async fn test_ast_grep_tool_language_alias_js() {
1054 let tool = AstGrepTool::new();
1055
1056 let mut temp_file = NamedTempFile::with_suffix(".js").unwrap();
1057 writeln!(temp_file, "console.log('hello');").unwrap();
1058 writeln!(temp_file, "console.log('world');").unwrap();
1059 temp_file.flush().unwrap();
1060
1061 let args = serde_json::json!({
1062 "pattern": "console.log($X)",
1063 "language": "js",
1064 "path": temp_file.path()
1065 });
1066
1067 let result = tool.execute(args).await;
1068 assert!(result.is_ok(), "Expected Ok, got Err: {:?}", result.err());
1069 let value = result.unwrap();
1070 assert_eq!(value["count"], 2);
1071 assert_eq!(value["command"], "search");
1072 }
1073
1074 #[tokio::test]
1075 async fn test_ast_grep_tool_language_alias_py() {
1076 let tool = AstGrepTool::new();
1077
1078 let mut temp_file = NamedTempFile::with_suffix(".py").unwrap();
1079 writeln!(temp_file, "def foo():").unwrap();
1080 writeln!(temp_file, "def bar():").unwrap();
1081 temp_file.flush().unwrap();
1082
1083 let args = serde_json::json!({
1084 "pattern": "def $FUNC():",
1085 "language": "py",
1086 "path": temp_file.path()
1087 });
1088
1089 let result = tool.execute(args).await;
1090 assert!(result.is_ok(), "Expected Ok, got Err: {:?}", result.err());
1091 let value = result.unwrap();
1092 assert_eq!(value["count"], 2);
1093 assert_eq!(value["command"], "search");
1094 }
1095
1096 #[tokio::test]
1097 async fn test_ast_grep_tool_language_alias_rs() {
1098 let tool = AstGrepTool::new();
1099
1100 let mut temp_file = NamedTempFile::with_suffix(".rs").unwrap();
1101 writeln!(temp_file, "fn foo() {{}}").unwrap();
1102 writeln!(temp_file, "fn bar() {{}}").unwrap();
1103 temp_file.flush().unwrap();
1104
1105 let args = serde_json::json!({
1106 "pattern": "fn $NAME() {}",
1107 "language": "rs",
1108 "path": temp_file.path()
1109 });
1110
1111 let result = tool.execute(args).await;
1112 assert!(result.is_ok(), "Expected Ok, got Err: {:?}", result.err());
1113 let value = result.unwrap();
1114 assert_eq!(value["count"], 2);
1115 assert_eq!(value["command"], "search");
1116 }
1117
1118 #[tokio::test]
1119 async fn test_ast_grep_tool_unsupported_command() {
1120 let tool = AstGrepTool::new();
1121 let args = serde_json::json!({
1122 "command": "test",
1123 "pattern": "fn main()",
1124 "language": "rust"
1125 });
1126
1127 let result = tool.execute(args).await;
1128 assert!(result.is_err());
1129 assert!(result
1130 .unwrap_err()
1131 .to_string()
1132 .contains("Unsupported command"));
1133 }
1134
1135 #[tokio::test]
1136 async fn test_ast_grep_tool_replace_missing_rewrite() {
1137 let tool = AstGrepTool::new();
1138 let args = serde_json::json!({
1139 "command": "replace",
1140 "pattern": "console.log($X)",
1141 "language": "javascript"
1142 });
1143
1144 let result = tool.execute(args).await;
1145 assert!(result.is_err());
1146 }
1147
1148 #[tokio::test]
1149 async fn test_ast_grep_tool_scan_path_not_found() {
1150 let tool = AstGrepTool::new();
1151 let args = serde_json::json!({
1152 "command": "scan",
1153 "path": "/nonexistent/path"
1154 });
1155
1156 let result = tool.execute(args).await;
1157 assert!(result.is_err());
1158 assert!(result
1159 .unwrap_err()
1160 .to_string()
1161 .contains("not yet supported"));
1162 }
1163
1164 #[tokio::test]
1165 async fn test_ast_grep_tool_backward_compat_no_command() {
1166 let tool = AstGrepTool::new();
1167
1168 let mut temp_file = NamedTempFile::new().unwrap();
1169 writeln!(temp_file, "fn foo() {{}}").unwrap();
1170 writeln!(temp_file, "fn bar() {{}}").unwrap();
1171 temp_file.flush().unwrap();
1172
1173 let args = serde_json::json!({
1174 "pattern": "fn $NAME() {}",
1175 "language": "rust",
1176 "path": temp_file.path()
1177 });
1178
1179 let result = tool.execute(args).await;
1180 assert!(result.is_ok(), "Expected Ok, got Err: {:?}", result.err());
1181 let value = result.unwrap();
1182 assert_eq!(value["command"], "search");
1183 assert_eq!(value["count"], 2);
1184 }
1185
1186 #[tokio::test]
1187 async fn test_ast_grep_search_directory() {
1188 let tool = AstGrepTool::new();
1192
1193 let temp_dir = tempfile::tempdir().unwrap();
1194 let file1 = temp_dir.path().join("test1.rs");
1195 let file2 = temp_dir.path().join("test2.rs");
1196
1197 fs::write(&file1, "fn foo() {}\nfn bar() {}").unwrap();
1198 fs::write(&file2, "fn baz() {}").unwrap();
1199
1200 let args = serde_json::json!({
1201 "pattern": "fn $NAME() {}",
1202 "language": "rust",
1203 "path": temp_dir.path()
1204 });
1205
1206 let result = tool.execute(args).await;
1207 assert!(result.is_ok(), "Expected Ok, got Err: {:?}", result.err());
1208 let value = result.unwrap();
1209 assert_eq!(value["count"], 3, "Should find 3 functions in directory");
1210 }
1211}