Skip to main content

atomcode_core/tool/
list_symbols.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 ListSymbolsTool;
9
10#[derive(Deserialize)]
11struct ListSymbolsArgs {
12    file_path: String,
13}
14
15#[async_trait]
16impl Tool for ListSymbolsTool {
17    fn definition(&self) -> ToolDef {
18        ToolDef {
19            name: "list_symbols",
20            description: "List all functions, classes, structs, and other top-level symbols in a file.\n\
21                Returns symbol names with line ranges. Use this to understand a file's structure before editing.\n\
22                This is faster and more precise than read_file for understanding file structure.\n\
23                Examples:\n\
24                - {\"file_path\": \"/path/to/main.rs\"} → lists all functions, structs, impls with line numbers".to_string(),
25            parameters: json!({
26                "type": "object",
27                "properties": {
28                    "file_path": { "type": "string", "description": "Absolute path to the source file" }
29                },
30                "required": ["file_path"]
31            }),
32        }
33    }
34
35    fn approval(&self, _args: &str) -> ApprovalRequirement {
36        ApprovalRequirement::AutoApprove
37    }
38
39    fn approval_with_context(&self, args: &str, ctx: &ToolContext) -> ApprovalRequirement {
40        let parsed = match serde_json::from_str::<ListSymbolsArgs>(args) {
41            Ok(parsed) => parsed,
42            Err(_) => return self.approval(args),
43        };
44        let working_dir = match ctx.working_dir.try_read() {
45            Ok(wd) => wd.clone(),
46            Err(_) => return self.approval(args),
47        };
48        match super::approval_for_path(
49            &parsed.file_path,
50            &working_dir,
51            super::ExternalPathAction::Read,
52        ) {
53            Ok(approval) => approval,
54            Err(_) => self.approval(args),
55        }
56    }
57
58    async fn execute(&self, args: &str, ctx: &ToolContext) -> Result<ToolResult> {
59        let parsed: ListSymbolsArgs = serde_json::from_str(args)?;
60        let working_dir = ctx.working_dir.read().await.clone();
61        let path = match super::inspect_path_access(&parsed.file_path, &working_dir) {
62            Ok(access) => access.path,
63            Err(err) => {
64                return Ok(ToolResult {
65                    call_id: String::new(),
66                    output: err.to_string(),
67                    success: false,
68                });
69            }
70        };
71
72        if !path.exists() {
73            return Ok(ToolResult {
74                call_id: String::new(),
75                output: format!("File not found: {}", parsed.file_path),
76                success: false,
77            });
78        }
79
80        let mut searcher = ctx.semantic.lock().await;
81        match searcher.list_symbols(&path) {
82            Some(symbols) if symbols.is_empty() => Ok(ToolResult {
83                call_id: String::new(),
84                output: format!("No symbols found in {}", parsed.file_path),
85                success: true,
86            }),
87            Some(symbols) => {
88                let mut out = format!(
89                    "Symbols in {} ({} total):\n\n",
90                    parsed.file_path,
91                    symbols.len()
92                );
93                for sym in &symbols {
94                    out.push_str(&format!(
95                        "  {:4}-{:4}  {}  ({})\n",
96                        sym.start_line, sym.end_line, sym.name, sym.kind
97                    ));
98                }
99                out.push_str("\n[Use read_symbol to read any symbol's full source code]");
100                Ok(ToolResult {
101                    call_id: String::new(),
102                    output: out,
103                    success: true,
104                })
105            }
106            None => Ok(ToolResult {
107                call_id: String::new(),
108                output: format!("Failed to parse {}", parsed.file_path),
109                success: false,
110            }),
111        }
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[tokio::test]
120    async fn approval_auto_for_workspace_file() {
121        let dir = tempfile::tempdir().unwrap();
122        let file = dir.path().join("main.rs");
123        std::fs::write(&file, "fn main() {}\n").unwrap();
124        let ctx = ToolContext::new(dir.path().to_path_buf());
125        let args = serde_json::json!({ "file_path": "main.rs" }).to_string();
126
127        assert!(matches!(
128            ListSymbolsTool.approval_with_context(&args, &ctx),
129            ApprovalRequirement::AutoApprove
130        ));
131    }
132
133    #[tokio::test]
134    async fn approval_requires_read_confirmation_for_external_file() {
135        let workspace = tempfile::tempdir().unwrap();
136        let outside = tempfile::tempdir().unwrap();
137        let file = outside.path().join("main.rs");
138        std::fs::write(&file, "fn main() {}\n").unwrap();
139        let ctx = ToolContext::new(workspace.path().to_path_buf());
140        let args = serde_json::json!({ "file_path": file }).to_string();
141
142        assert!(matches!(
143            ListSymbolsTool.approval_with_context(&args, &ctx),
144            ApprovalRequirement::RequireApproval(_)
145        ));
146    }
147}