1use anyhow::{anyhow, Result};
21use regex::Regex;
22use serde::{Deserialize, Serialize};
23use serde_json::{json, Value};
24use std::collections::HashMap;
25use std::path::Path;
26use tree_sitter::{Language, Node, Parser};
27
28const SKIP_DIRS: &[&str] = &[".git", "node_modules", "target", "dist", "__pycache__", ".venv", "venv"];
29
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
32#[serde(rename_all = "snake_case")]
33pub enum CodeAction {
34 Parse,
35 Serialize,
36 Symbols,
37 Outline,
38 Definition,
39 References,
40 SearchSymbol,
41 Transform,
42 Summarize,
43 Metrics,
44 Exports,
45 Types,
46 Hierarchy,
47 Rename,
48 GrepReplace,
49 Help,
50}
51
52impl Default for CodeAction {
53 fn default() -> Self {
54 Self::Help
55 }
56}
57
58impl std::str::FromStr for CodeAction {
59 type Err = anyhow::Error;
60
61 fn from_str(s: &str) -> Result<Self> {
62 match s.to_lowercase().as_str() {
63 "parse" | "ast" => Ok(Self::Parse),
64 "serialize" => Ok(Self::Serialize),
65 "symbols" => Ok(Self::Symbols),
66 "outline" => Ok(Self::Outline),
67 "definition" | "goto" => Ok(Self::Definition),
68 "references" | "refs" => Ok(Self::References),
69 "search_symbol" | "search" => Ok(Self::SearchSymbol),
70 "transform" | "codemod" => Ok(Self::Transform),
71 "summarize" | "summary" => Ok(Self::Summarize),
72 "metrics" => Ok(Self::Metrics),
73 "exports" => Ok(Self::Exports),
74 "types" => Ok(Self::Types),
75 "hierarchy" => Ok(Self::Hierarchy),
76 "rename" => Ok(Self::Rename),
77 "grep_replace" => Ok(Self::GrepReplace),
78 "help" | "" => Ok(Self::Help),
79 _ => Err(anyhow!("Unknown action: {}", s)),
80 }
81 }
82}
83
84#[derive(Debug, Clone, Default, Serialize, Deserialize)]
86pub struct CodeToolArgs {
87 pub action: Option<String>,
88 pub uri: Option<String>,
89 pub path: Option<String>,
90 pub symbol: Option<String>,
91 pub language: Option<String>,
92 pub query: Option<String>,
93 pub spec: Option<String>,
94 pub text: Option<String>,
95 pub pattern: Option<String>,
96 pub new_name: Option<String>,
97 pub replacement: Option<String>,
98 pub max_results: Option<usize>,
99 pub scope: Option<String>,
100 pub ast: Option<Value>,
101}
102
103pub struct CodeToolDefinition;
105
106impl CodeToolDefinition {
107 pub fn schema() -> Value {
108 json!({
109 "name": "code",
110 "description": "Code semantics: parse, serialize, symbols, outline, definition, references, search_symbol, transform, summarize, metrics, exports, types, hierarchy, rename, grep_replace",
111 "inputSchema": {
112 "type": "object",
113 "properties": {
114 "action": {
115 "type": "string",
116 "enum": ["parse", "serialize", "symbols", "outline", "definition", "references", "search_symbol", "transform", "summarize", "metrics", "exports", "types", "hierarchy", "rename", "grep_replace", "help"],
117 "description": "Code action"
118 },
119 "uri": { "type": "string", "description": "File path" },
120 "path": { "type": "string", "description": "File or directory path (alias for uri)" },
121 "symbol": { "type": "string", "description": "Symbol name" },
122 "language": { "type": "string", "description": "Programming language" },
123 "query": { "type": "string", "description": "Search query or symbol name" },
124 "spec": { "type": "string", "description": "Transform specification" },
125 "text": { "type": "string", "description": "Raw text input" },
126 "pattern": { "type": "string", "description": "File glob pattern" },
127 "new_name": { "type": "string", "description": "New name for rename" },
128 "replacement": { "type": "string", "description": "Replacement for grep_replace" },
129 "max_results": { "type": "number", "default": 20 },
130 "scope": { "type": "string", "description": "Search scope" },
131 "ast": { "type": "object", "description": "Pre-parsed AST node to serialize back to text" }
132 },
133 "required": ["action"]
134 }
135 })
136 }
137}
138
139pub struct CodeTool;
141
142impl CodeTool {
143 pub fn new() -> Self {
144 Self
145 }
146
147 fn resolve_uri<'a>(&self, args: &'a CodeToolArgs) -> Option<&'a str> {
148 args.uri.as_deref().or(args.path.as_deref())
149 }
150
151 fn is_code_file(path: &Path) -> bool {
152 matches!(
153 path.extension().and_then(|e| e.to_str()),
154 Some("rs" | "py" | "js" | "ts" | "tsx" | "jsx" | "go" | "java" | "c" | "cpp" | "h" | "hpp" | "rb" | "swift" | "kt" | "cs" | "lua" | "sh")
155 )
156 }
157
158 fn should_skip(entry: &std::fs::DirEntry) -> bool {
159 entry.file_name().to_str().map_or(false, |n| SKIP_DIRS.contains(&n))
160 }
161
162 fn walk_files(dir: &Path) -> Vec<std::path::PathBuf> {
163 let mut files = Vec::new();
164 fn walk(dir: &Path, files: &mut Vec<std::path::PathBuf>) {
165 if let Ok(entries) = std::fs::read_dir(dir) {
166 for entry in entries.flatten() {
167 let path = entry.path();
168 if path.is_dir() {
169 if !CodeTool::should_skip(&entry) {
170 walk(&path, files);
171 }
172 } else if CodeTool::is_code_file(&path) {
173 files.push(path);
174 }
175 }
176 }
177 }
178 walk(dir, &mut files);
179 files
180 }
181
182 fn detect_symbol(line: &str) -> Option<(&'static str, String)> {
183 let trimmed = line.trim();
184 let kind = if trimmed.starts_with("fn ") || trimmed.starts_with("pub fn ") || trimmed.starts_with("async fn ") || trimmed.starts_with("pub async fn ") {
185 Some("function")
186 } else if trimmed.starts_with("struct ") || trimmed.starts_with("pub struct ") {
187 Some("struct")
188 } else if trimmed.starts_with("enum ") || trimmed.starts_with("pub enum ") {
189 Some("enum")
190 } else if trimmed.starts_with("trait ") || trimmed.starts_with("pub trait ") {
191 Some("trait")
192 } else if trimmed.starts_with("class ") || trimmed.starts_with("export class ") {
193 Some("class")
194 } else if trimmed.starts_with("def ") || trimmed.starts_with("async def ") {
195 Some("function")
196 } else if trimmed.starts_with("function ") || trimmed.starts_with("export function ") || trimmed.starts_with("async function ") {
197 Some("function")
198 } else if trimmed.starts_with("interface ") || trimmed.starts_with("export interface ") {
199 Some("interface")
200 } else if trimmed.starts_with("type ") || trimmed.starts_with("export type ") {
201 Some("type")
202 } else {
203 None
204 };
205
206 kind.and_then(|k| {
207 let skip = ["fn", "pub", "async", "struct", "enum", "trait", "class", "def", "function", "export", "interface", "type"];
208 let name = trimmed.split_whitespace()
209 .find(|w| !skip.contains(w))
210 .unwrap_or("")
211 .trim_end_matches(|c: char| !c.is_alphanumeric() && c != '_')
212 .to_string();
213 if name.is_empty() { None } else { Some((k, name)) }
214 })
215 }
216
217 pub async fn execute(&self, args: CodeToolArgs) -> Result<Value> {
218 let action: CodeAction = args.action
219 .as_deref()
220 .unwrap_or("help")
221 .parse()?;
222
223 match action {
224 CodeAction::Parse => self.parse(&args).await,
225 CodeAction::Serialize => self.serialize(&args).await,
226 CodeAction::Symbols => self.symbols(&args).await,
227 CodeAction::Outline => self.outline(&args).await,
228 CodeAction::Definition => self.definition(&args).await,
229 CodeAction::References => self.references(&args).await,
230 CodeAction::SearchSymbol => self.search_symbol(&args).await,
231 CodeAction::Transform => self.transform(&args).await,
232 CodeAction::Summarize => self.summarize(&args).await,
233 CodeAction::Metrics => self.metrics(&args).await,
234 CodeAction::Exports => self.exports(&args).await,
235 CodeAction::Types => self.types(&args).await,
236 CodeAction::Hierarchy => self.hierarchy(&args).await,
237 CodeAction::Rename => self.rename(&args).await,
238 CodeAction::GrepReplace => self.grep_replace(&args).await,
239 CodeAction::Help => Ok(self.help()),
240 }
241 }
242
243 async fn parse(&self, args: &CodeToolArgs) -> Result<Value> {
244 let uri = self.resolve_uri(args).ok_or_else(|| anyhow!("uri required"))?;
245 let content = tokio::fs::read_to_string(uri).await?;
246 let lines = content.lines().count();
247 let lang = args.language.clone().unwrap_or_else(|| {
248 Path::new(uri).extension().and_then(|e| e.to_str()).unwrap_or("text").to_string()
249 });
250
251 Ok(json!({
252 "ok": true,
253 "data": { "uri": uri, "language": lang, "lines": lines },
254 "error": null,
255 "meta": { "tool": "code", "action": "parse" }
256 }))
257 }
258
259 fn ts_language(name: &str) -> Option<Language> {
261 Some(match name {
262 "rust" => tree_sitter_rust::language(),
263 "javascript" | "jsx" => tree_sitter_javascript::language(),
264 "typescript" => tree_sitter_typescript::language_typescript(),
265 "tsx" => tree_sitter_typescript::language_tsx(),
266 "python" => tree_sitter_python::language(),
267 "go" => tree_sitter_go::language(),
268 "java" => tree_sitter_java::language(),
269 "cpp" => tree_sitter_cpp::language(),
270 "c" => tree_sitter_c::language(),
271 _ => return None,
272 })
273 }
274
275 fn ts_lang_name(ext: &str) -> &'static str {
277 match ext {
278 "rs" => "rust",
279 "js" | "mjs" | "cjs" => "javascript",
280 "jsx" => "jsx",
281 "ts" => "typescript",
282 "tsx" => "tsx",
283 "py" => "python",
284 "go" => "go",
285 "java" => "java",
286 "cpp" | "cc" | "cxx" | "hpp" => "cpp",
287 "c" | "h" => "c",
288 _ => "unknown",
289 }
290 }
291
292 fn walk_leaves(node: Node, source: &str, out: &mut String, last_end: &mut usize) {
296 if node.child_count() == 0 {
297 let range = node.byte_range();
298 if range.start > *last_end {
299 out.push_str(&source[*last_end..range.start]);
300 }
301 out.push_str(&source[range.clone()]);
302 *last_end = range.end;
303 return;
304 }
305 let mut cursor = node.walk();
306 for child in node.children(&mut cursor) {
307 Self::walk_leaves(child, source, out, last_end);
308 }
309 }
310
311 fn ast_node_text(node: &Value) -> String {
314 if let Some(text) = node.get("text").and_then(Value::as_str) {
315 return text.to_string();
316 }
317 node.get("children")
318 .and_then(Value::as_array)
319 .map(|children| children.iter().map(Self::ast_node_text).collect::<Vec<_>>().join(" "))
320 .unwrap_or_default()
321 }
322
323 async fn serialize(&self, args: &CodeToolArgs) -> Result<Value> {
324 if let Some(ast) = &args.ast {
327 let root = ast.get("root").unwrap_or(ast);
328 let text = Self::ast_node_text(root);
329 let lang = args.language.clone()
330 .or_else(|| ast.get("lang").and_then(Value::as_str).map(str::to_string))
331 .unwrap_or_else(|| "unknown".to_string());
332 return Ok(json!({
333 "ok": true,
334 "data": { "text": text, "lang": lang, "method": "ast", "supported": true },
335 "error": null,
336 "meta": { "tool": "code", "action": "serialize" }
337 }));
338 }
339
340 let source = if let Some(text) = &args.text {
343 text.clone()
344 } else if let Some(uri) = self.resolve_uri(args) {
345 tokio::fs::read_to_string(uri).await?
346 } else {
347 return Err(anyhow!("ast, text, or uri required"));
348 };
349
350 let lang_name = args.language.clone().unwrap_or_else(|| {
351 let ext = self.resolve_uri(args)
352 .and_then(|u| Path::new(u).extension().and_then(|e| e.to_str()))
353 .unwrap_or("");
354 Self::ts_lang_name(ext).to_string()
355 });
356
357 let language = Self::ts_language(&lang_name)
358 .ok_or_else(|| anyhow!("Unsupported language for serialize: {}", lang_name))?;
359 let mut parser = Parser::new();
360 parser.set_language(language).map_err(|e| anyhow!("set_language failed: {}", e))?;
361 let tree = parser.parse(source.as_bytes(), None)
362 .ok_or_else(|| anyhow!("Parse failed for language: {}", lang_name))?;
363
364 let mut text = String::with_capacity(source.len());
365 let mut last_end = 0usize;
366 Self::walk_leaves(tree.root_node(), &source, &mut text, &mut last_end);
367 if last_end < source.len() {
368 text.push_str(&source[last_end..]);
369 }
370
371 Ok(json!({
372 "ok": true,
373 "data": {
374 "text": text,
375 "lang": lang_name,
376 "method": "round_trip",
377 "supported": true,
378 "round_trip": text == source
379 },
380 "error": null,
381 "meta": { "tool": "code", "action": "serialize" }
382 }))
383 }
384
385 async fn symbols(&self, args: &CodeToolArgs) -> Result<Value> {
386 let uri = self.resolve_uri(args).ok_or_else(|| anyhow!("uri required"))?;
387 let content = args.text.clone().unwrap_or(tokio::fs::read_to_string(uri).await?);
388 let mut symbols = Vec::new();
389
390 for (i, line) in content.lines().enumerate() {
391 if let Some((kind, name)) = Self::detect_symbol(line) {
392 symbols.push(json!({ "name": name, "kind": kind, "line": i + 1 }));
393 }
394 }
395
396 Ok(json!({
397 "ok": true,
398 "data": { "uri": uri, "symbols": symbols, "count": symbols.len() },
399 "error": null,
400 "meta": { "tool": "code", "action": "symbols" }
401 }))
402 }
403
404 async fn outline(&self, args: &CodeToolArgs) -> Result<Value> {
405 let uri = self.resolve_uri(args).ok_or_else(|| anyhow!("uri required"))?;
406 let content = args.text.clone().unwrap_or(tokio::fs::read_to_string(uri).await?);
407 let mut symbols = Vec::new();
408 let mut imports = 0;
409
410 for (i, line) in content.lines().enumerate() {
411 let trimmed = line.trim();
412 if trimmed.starts_with("import ") || trimmed.starts_with("from ") || trimmed.starts_with("use ") || trimmed.starts_with("require") {
413 imports += 1;
414 }
415 if let Some((kind, name)) = Self::detect_symbol(line) {
416 let exported = trimmed.starts_with("export ") || trimmed.starts_with("pub ");
417 symbols.push(json!({ "name": name, "kind": kind, "line": i + 1, "exported": exported }));
418 }
419 }
420
421 Ok(json!({
422 "ok": true,
423 "data": { "uri": uri, "symbols": symbols, "imports": imports, "lines": content.lines().count() },
424 "error": null,
425 "meta": { "tool": "code", "action": "outline" }
426 }))
427 }
428
429 async fn definition(&self, args: &CodeToolArgs) -> Result<Value> {
430 let uri = self.resolve_uri(args).ok_or_else(|| anyhow!("uri required"))?;
431 let symbol = args.symbol.as_deref().or(args.query.as_deref()).ok_or_else(|| anyhow!("symbol or query required"))?;
432 let content = tokio::fs::read_to_string(uri).await?;
433
434 for (i, line) in content.lines().enumerate() {
435 if line.contains(symbol) {
436 if let Some(_) = Self::detect_symbol(line) {
437 return Ok(json!({
438 "ok": true,
439 "data": { "uri": uri, "symbol": symbol, "line": i + 1, "text": line.trim() },
440 "error": null,
441 "meta": { "tool": "code", "action": "definition" }
442 }));
443 }
444 }
445 }
446
447 Ok(json!({
448 "ok": false,
449 "data": null,
450 "error": { "code": "NOT_FOUND", "message": format!("Symbol '{}' not found", symbol) },
451 "meta": { "tool": "code", "action": "definition" }
452 }))
453 }
454
455 async fn references(&self, args: &CodeToolArgs) -> Result<Value> {
456 let symbol = args.symbol.as_deref().or(args.query.as_deref()).ok_or_else(|| anyhow!("symbol or query required"))?;
457 let uri = self.resolve_uri(args).ok_or_else(|| anyhow!("uri required"))?;
458 let content = tokio::fs::read_to_string(uri).await?;
459 let mut refs = Vec::new();
460
461 for (i, line) in content.lines().enumerate() {
462 if line.contains(symbol) {
463 refs.push(json!({ "line": i + 1, "text": line.trim() }));
464 }
465 }
466
467 Ok(json!({
468 "ok": true,
469 "data": { "uri": uri, "symbol": symbol, "references": refs, "count": refs.len() },
470 "error": null,
471 "meta": { "tool": "code", "action": "references" }
472 }))
473 }
474
475 async fn search_symbol(&self, args: &CodeToolArgs) -> Result<Value> {
476 let query = args.query.as_deref().ok_or_else(|| anyhow!("query required"))?;
477 let dir = self.resolve_uri(args).unwrap_or(".");
478 let max = args.max_results.unwrap_or(20);
479 let files = Self::walk_files(Path::new(dir));
480 let mut results = Vec::new();
481
482 for file in files {
483 if results.len() >= max { break; }
484 if let Ok(content) = std::fs::read_to_string(&file) {
485 for (i, line) in content.lines().enumerate() {
486 if results.len() >= max { break; }
487 if line.contains(query) {
488 if Self::detect_symbol(line).is_some() || line.contains(query) {
489 let is_def = Self::detect_symbol(line).is_some();
490 results.push(json!({
491 "uri": file.display().to_string(),
492 "line": i + 1,
493 "text": line.trim(),
494 "type": if is_def { "definition" } else { "reference" }
495 }));
496 }
497 }
498 }
499 }
500 }
501
502 Ok(json!({
503 "ok": true,
504 "data": { "query": query, "results": results, "count": results.len() },
505 "error": null,
506 "meta": { "tool": "code", "action": "search_symbol" }
507 }))
508 }
509
510 async fn transform(&self, _args: &CodeToolArgs) -> Result<Value> {
511 Ok(json!({
512 "ok": false,
513 "data": null,
514 "error": { "code": "NOT_IMPLEMENTED", "message": "Transform requires tree-sitter; use fs(action=apply_patch) for edits" },
515 "meta": { "tool": "code", "action": "transform" }
516 }))
517 }
518
519 async fn summarize(&self, args: &CodeToolArgs) -> Result<Value> {
520 let uri = self.resolve_uri(args);
521 let content = if let Some(text) = &args.text {
522 text.clone()
523 } else if let Some(uri) = uri {
524 tokio::fs::read_to_string(uri).await?
525 } else {
526 return Err(anyhow!("uri or text required"));
527 };
528
529 let lines = content.lines().count();
530 let chars = content.len();
531 let words = content.split_whitespace().count();
532
533 let is_diff = content.contains("---") && content.contains("+++");
535 let summary = if is_diff {
536 let adds = content.lines().filter(|l| l.starts_with('+') && !l.starts_with("+++")).count();
537 let dels = content.lines().filter(|l| l.starts_with('-') && !l.starts_with("---")).count();
538 format!("Diff: {} lines, {} additions, {} deletions", lines, adds, dels)
539 } else {
540 format!("{} lines, {} words, {} bytes", lines, words, chars)
541 };
542
543 Ok(json!({
544 "ok": true,
545 "data": { "uri": uri, "lines": lines, "chars": chars, "words": words, "summary": summary },
546 "error": null,
547 "meta": { "tool": "code", "action": "summarize" }
548 }))
549 }
550
551 async fn metrics(&self, args: &CodeToolArgs) -> Result<Value> {
552 let dir = self.resolve_uri(args).unwrap_or(".");
553 let files = Self::walk_files(Path::new(dir));
554 let mut by_ext: HashMap<String, (usize, usize)> = HashMap::new(); let mut total_files = 0usize;
556 let mut total_lines = 0usize;
557
558 for file in files {
559 if let Ok(content) = std::fs::read_to_string(&file) {
560 let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("other");
561 let ext_key = format!(".{}", ext);
562 let lines = content.lines().count();
563 let entry = by_ext.entry(ext_key).or_insert((0, 0));
564 entry.0 += 1;
565 entry.1 += lines;
566 total_files += 1;
567 total_lines += lines;
568 }
569 }
570
571 let by_extension: HashMap<String, Value> = by_ext.into_iter()
572 .map(|(k, (f, l))| (k, json!({ "files": f, "lines": l })))
573 .collect();
574
575 Ok(json!({
576 "ok": true,
577 "data": { "total_files": total_files, "total_lines": total_lines, "by_extension": by_extension },
578 "error": null,
579 "meta": { "tool": "code", "action": "metrics" }
580 }))
581 }
582
583 async fn exports(&self, args: &CodeToolArgs) -> Result<Value> {
584 let uri = self.resolve_uri(args).ok_or_else(|| anyhow!("uri required"))?;
585 let content = tokio::fs::read_to_string(uri).await?;
586 let mut exports = Vec::new();
587
588 for line in content.lines() {
589 let trimmed = line.trim();
590 if trimmed.starts_with("export ") || trimmed.starts_with("pub ") || trimmed.starts_with("__all__") {
591 exports.push(trimmed.to_string());
592 }
593 }
594
595 Ok(json!({
596 "ok": true,
597 "data": { "uri": uri, "exports": exports, "count": exports.len() },
598 "error": null,
599 "meta": { "tool": "code", "action": "exports" }
600 }))
601 }
602
603 async fn types(&self, args: &CodeToolArgs) -> Result<Value> {
604 let uri = self.resolve_uri(args).ok_or_else(|| anyhow!("uri required"))?;
605 let content = tokio::fs::read_to_string(uri).await?;
606 let mut type_defs = Vec::new();
607
608 let re = Regex::new(r"(?:interface|type|enum|struct)\s+(\w+)").unwrap();
609 for (i, line) in content.lines().enumerate() {
610 if let Some(m) = re.captures(line) {
611 type_defs.push(json!({
612 "name": m.get(1).map(|m| m.as_str()).unwrap_or(""),
613 "line": i + 1,
614 "text": line.trim()
615 }));
616 }
617 }
618
619 Ok(json!({
620 "ok": true,
621 "data": { "uri": uri, "types": type_defs, "count": type_defs.len() },
622 "error": null,
623 "meta": { "tool": "code", "action": "types" }
624 }))
625 }
626
627 async fn hierarchy(&self, args: &CodeToolArgs) -> Result<Value> {
628 let query = args.query.as_deref().ok_or_else(|| anyhow!("query (class name) required"))?;
629 let dir = self.resolve_uri(args).unwrap_or(".");
630 let files = Self::walk_files(Path::new(dir));
631 let mut classes: HashMap<String, Vec<String>> = HashMap::new();
632
633 let re = Regex::new(r"class\s+(\w+)(?:\s+extends\s+(\w+)|\s*\((\w+)\))?").unwrap();
634 for file in files {
635 if let Ok(content) = std::fs::read_to_string(&file) {
636 for cap in re.captures_iter(&content) {
637 let name = cap.get(1).map(|m| m.as_str().to_string()).unwrap_or_default();
638 let parent = cap.get(2).or(cap.get(3)).map(|m| m.as_str().to_string());
639 classes.entry(name.clone()).or_default();
640 if let Some(p) = parent {
641 classes.entry(p.clone()).or_default().push(name);
642 }
643 }
644 }
645 }
646
647 fn build_tree(name: &str, classes: &HashMap<String, Vec<String>>, depth: usize) -> String {
648 let mut out = " ".repeat(depth) + name + "\n";
649 if let Some(children) = classes.get(name) {
650 for child in children {
651 out += &build_tree(child, classes, depth + 1);
652 }
653 }
654 out
655 }
656
657 let tree = build_tree(query, &classes, 0);
658 let children = classes.get(query).cloned().unwrap_or_default();
659
660 Ok(json!({
661 "ok": true,
662 "data": { "root": query, "tree": tree, "children": children },
663 "error": null,
664 "meta": { "tool": "code", "action": "hierarchy" }
665 }))
666 }
667
668 async fn rename(&self, args: &CodeToolArgs) -> Result<Value> {
669 let query = args.query.as_deref().ok_or_else(|| anyhow!("query (old name) required"))?;
670 let new_name = args.new_name.as_deref().ok_or_else(|| anyhow!("new_name required"))?;
671 let dir = self.resolve_uri(args).unwrap_or(".");
672 let files = Self::walk_files(Path::new(dir));
673 let re = Regex::new(&format!(r"\b{}\b", regex::escape(query)))?;
674 let mut total_changes = 0usize;
675 let mut changed = Vec::new();
676
677 for file in files {
678 if let Ok(content) = std::fs::read_to_string(&file) {
679 if re.is_match(&content) {
680 let count = re.find_iter(&content).count();
681 let updated = re.replace_all(&content, new_name).to_string();
682 std::fs::write(&file, &updated)?;
683 total_changes += count;
684 changed.push(format!("{}: {} replacements", file.display(), count));
685 }
686 }
687 }
688
689 Ok(json!({
690 "ok": true,
691 "data": { "old_name": query, "new_name": new_name, "files_changed": changed.len(), "total_replacements": total_changes, "changed": changed },
692 "error": null,
693 "meta": { "tool": "code", "action": "rename" }
694 }))
695 }
696
697 async fn grep_replace(&self, args: &CodeToolArgs) -> Result<Value> {
698 let pattern = args.query.as_deref().ok_or_else(|| anyhow!("query (pattern) required"))?;
699 let replacement = args.replacement.as_deref().ok_or_else(|| anyhow!("replacement required"))?;
700 let dir = self.resolve_uri(args).unwrap_or(".");
701 let files = Self::walk_files(Path::new(dir));
702 let re = Regex::new(pattern)?;
703 let mut total_changes = 0usize;
704 let mut changed = Vec::new();
705
706 for file in files {
707 if let Ok(content) = std::fs::read_to_string(&file) {
708 if re.is_match(&content) {
709 let count = re.find_iter(&content).count();
710 let updated = re.replace_all(&content, replacement).to_string();
711 std::fs::write(&file, &updated)?;
712 total_changes += count;
713 changed.push(format!("{}: {}", file.display(), count));
714 }
715 }
716 }
717
718 Ok(json!({
719 "ok": true,
720 "data": { "pattern": pattern, "replacement": replacement, "files_changed": changed.len(), "total_replacements": total_changes, "changed": changed },
721 "error": null,
722 "meta": { "tool": "code", "action": "grep_replace" }
723 }))
724 }
725
726 fn help(&self) -> Value {
727 json!({
728 "ok": true,
729 "data": {
730 "tool": "code",
731 "actions": {
732 "parse": "Parse source to AST (requires uri)",
733 "serialize": "Convert AST to text (round-trip; requires ast, or text/uri)",
734 "symbols": "List symbols in file (requires uri)",
735 "outline": "Symbols with imports/exports (requires uri)",
736 "definition": "Go to symbol definition (requires uri, symbol/query)",
737 "references": "Find all references (requires uri, symbol/query)",
738 "search_symbol": "Find symbols across project (requires query)",
739 "transform": "Codemod → Patch (requires uri, spec)",
740 "summarize": "Compress to summary (requires uri or text)",
741 "metrics": "Count files/lines by extension (optional uri for dir)",
742 "exports": "Extract public exports (requires uri)",
743 "types": "Find type definitions (requires uri)",
744 "hierarchy": "Build class inheritance tree (requires query)",
745 "rename": "Rename symbols across files (requires query, new_name)",
746 "grep_replace": "Pattern replacement across files (requires query, replacement)"
747 }
748 },
749 "error": null,
750 "meta": { "tool": "code", "action": "help" }
751 })
752 }
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758
759 #[test]
760 fn test_code_action_parse() {
761 let action: CodeAction = "parse".parse().unwrap();
762 assert_eq!(action, CodeAction::Parse);
763 }
764
765 #[test]
766 fn test_code_action_aliases() {
767 let action: CodeAction = "outline".parse().unwrap();
768 assert_eq!(action, CodeAction::Outline);
769 let action: CodeAction = "search".parse().unwrap();
770 assert_eq!(action, CodeAction::SearchSymbol);
771 }
772
773 #[test]
774 fn test_code_action_new_variants() {
775 assert_eq!("metrics".parse::<CodeAction>().unwrap(), CodeAction::Metrics);
776 assert_eq!("exports".parse::<CodeAction>().unwrap(), CodeAction::Exports);
777 assert_eq!("types".parse::<CodeAction>().unwrap(), CodeAction::Types);
778 assert_eq!("hierarchy".parse::<CodeAction>().unwrap(), CodeAction::Hierarchy);
779 assert_eq!("rename".parse::<CodeAction>().unwrap(), CodeAction::Rename);
780 assert_eq!("grep_replace".parse::<CodeAction>().unwrap(), CodeAction::GrepReplace);
781 assert_eq!("serialize".parse::<CodeAction>().unwrap(), CodeAction::Serialize);
782 }
783
784 #[tokio::test]
785 async fn test_serialize_round_trips_source() {
786 let src = "fn main() {\n let x = 1;\n println!(\"{}\", x);\n}\n";
787 let tool = CodeTool::new();
788 let args = CodeToolArgs {
789 action: Some("serialize".into()),
790 text: Some(src.to_string()),
791 language: Some("rust".into()),
792 ..Default::default()
793 };
794 let out = tool.serialize(&args).await.unwrap();
795 assert_eq!(out["data"]["text"].as_str().unwrap(), src);
796 assert_eq!(out["data"]["round_trip"], json!(true));
797 assert_eq!(out["data"]["method"], json!("round_trip"));
798 }
799
800 #[tokio::test]
801 async fn test_serialize_from_ast_node() {
802 let ast = json!({
803 "lang": "python",
804 "root": { "children": [ { "text": "x" }, { "text": "=" }, { "text": "1" } ] }
805 });
806 let tool = CodeTool::new();
807 let args = CodeToolArgs {
808 action: Some("serialize".into()),
809 ast: Some(ast),
810 ..Default::default()
811 };
812 let out = tool.serialize(&args).await.unwrap();
813 assert_eq!(out["data"]["text"].as_str().unwrap(), "x = 1");
814 assert_eq!(out["data"]["lang"], json!("python"));
815 assert_eq!(out["data"]["method"], json!("ast"));
816 }
817}