Skip to main content

atomcode_core/tool/
read_symbol.rs

1use 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                // Add line numbers to the source
91                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                // Try to list available symbols as a helpful hint
102                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}