atomcode_core/tool/
list_symbols.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 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}