atomcode_core/tool/
read_symbol.rs1use anyhow::Result;
2use async_trait::async_trait;
3use serde::Deserialize;
4use serde_json::json;
5
6use super::{ApprovalRequirement, Tool, ToolContext, ToolDef, ToolResult};
7
8pub struct ReadSymbolTool;
9
10#[derive(Deserialize)]
11struct ReadSymbolArgs {
12 file_path: String,
13 symbol: String,
14}
15
16#[async_trait]
17impl Tool for ReadSymbolTool {
18 fn definition(&self) -> ToolDef {
19 ToolDef {
20 name: "read_symbol",
21 description: "Read the complete source code of a specific function, class, or struct by name.\n\
22 More precise than read_file — returns exactly the symbol you need with line numbers.\n\
23 Use list_symbols first to discover available symbols, then read_symbol to get the code.\n\
24 Examples:\n\
25 - {\"file_path\": \"/path/to/main.rs\", \"symbol\": \"process_data\"}\n\
26 - {\"file_path\": \"/path/to/app.py\", \"symbol\": \"UserService\"}".to_string(),
27 parameters: json!({
28 "type": "object",
29 "properties": {
30 "file_path": { "type": "string", "description": "Absolute path to the source file" },
31 "symbol": { "type": "string", "description": "Name of the function, class, or struct to read" }
32 },
33 "required": ["file_path", "symbol"]
34 }),
35 }
36 }
37
38 fn approval(&self, _args: &str) -> ApprovalRequirement {
39 ApprovalRequirement::AutoApprove
40 }
41
42 fn approval_with_context(&self, args: &str, ctx: &ToolContext) -> ApprovalRequirement {
43 let parsed = match serde_json::from_str::<ReadSymbolArgs>(args) {
44 Ok(parsed) => parsed,
45 Err(_) => return self.approval(args),
46 };
47 let working_dir = match ctx.working_dir.try_read() {
48 Ok(wd) => wd.clone(),
49 Err(_) => return self.approval(args),
50 };
51 match super::approval_for_path(
52 &parsed.file_path,
53 &working_dir,
54 super::ExternalPathAction::Read,
55 ) {
56 Ok(approval) => approval,
57 Err(_) => self.approval(args),
58 }
59 }
60
61 async fn execute(&self, args: &str, ctx: &ToolContext) -> Result<ToolResult> {
62 let parsed: ReadSymbolArgs = serde_json::from_str(args)?;
63 let working_dir = ctx.working_dir.read().await.clone();
64 let path = match super::inspect_path_access(&parsed.file_path, &working_dir) {
65 Ok(access) => access.path,
66 Err(err) => {
67 return Ok(ToolResult {
68 call_id: String::new(),
69 output: err.to_string(),
70 success: false,
71 });
72 }
73 };
74
75 if !path.exists() {
76 return Ok(ToolResult {
77 call_id: String::new(),
78 output: format!("File not found: {}", parsed.file_path),
79 success: false,
80 });
81 }
82
83 let mut searcher = ctx.semantic.lock().await;
84 match searcher.extract_symbol(&path, &parsed.symbol) {
85 Some(slice) => {
86 let mut out = format!(
87 "{} ({}, lines {}-{})\n\n",
88 slice.name, slice.kind, slice.start_line, slice.end_line
89 );
90 for (i, line) in slice.text.lines().enumerate() {
92 out.push_str(&format!("{:4}| {}\n", slice.start_line + i, line));
93 }
94 Ok(ToolResult {
95 call_id: String::new(),
96 output: out,
97 success: true,
98 })
99 }
100 None => {
101 let hint = match searcher.list_symbols(&path) {
103 Some(symbols) => {
104 let names: Vec<String> = symbols.iter().map(|s| s.name.clone()).collect();
105 format!(
106 "Symbol '{}' not found in {}.\nAvailable symbols: {}",
107 parsed.symbol,
108 parsed.file_path,
109 names.join(", ")
110 )
111 }
112 None => format!(
113 "Symbol '{}' not found in {}",
114 parsed.symbol, parsed.file_path
115 ),
116 };
117 Ok(ToolResult {
118 call_id: String::new(),
119 output: hint,
120 success: false,
121 })
122 }
123 }
124 }
125}