atomcode-core 4.23.1

Open-source terminal AI coding agent
Documentation
use anyhow::Result;
use async_trait::async_trait;
use serde::Deserialize;
use serde_json::json;

use super::{ApprovalRequirement, Tool, ToolContext, ToolDef, ToolResult};

pub struct ListSymbolsTool;

#[derive(Deserialize)]
struct ListSymbolsArgs {
    file_path: String,
}

#[async_trait]
impl Tool for ListSymbolsTool {
    fn definition(&self) -> ToolDef {
        ToolDef {
            name: "list_symbols",
            description: "List all functions, classes, structs, and other top-level symbols in a file.\n\
                Returns symbol names with line ranges. Use this to understand a file's structure before editing.\n\
                This is faster and more precise than read_file for understanding file structure.\n\
                Examples:\n\
                - {\"file_path\": \"/path/to/main.rs\"} → lists all functions, structs, impls with line numbers".to_string(),
            parameters: json!({
                "type": "object",
                "properties": {
                    "file_path": { "type": "string", "description": "Absolute path to the source file" }
                },
                "required": ["file_path"]
            }),
        }
    }

    fn approval(&self, _args: &str) -> ApprovalRequirement {
        ApprovalRequirement::AutoApprove
    }

    fn approval_with_context(&self, args: &str, ctx: &ToolContext) -> ApprovalRequirement {
        let parsed = match serde_json::from_str::<ListSymbolsArgs>(args) {
            Ok(parsed) => parsed,
            Err(_) => return self.approval(args),
        };
        let working_dir = match ctx.working_dir.try_read() {
            Ok(wd) => wd.clone(),
            Err(_) => return self.approval(args),
        };
        match super::approval_for_path(
            &parsed.file_path,
            &working_dir,
            super::ExternalPathAction::Read,
        ) {
            Ok(approval) => approval,
            Err(_) => self.approval(args),
        }
    }

    async fn execute(&self, args: &str, ctx: &ToolContext) -> Result<ToolResult> {
        let parsed: ListSymbolsArgs = serde_json::from_str(args)?;
        let working_dir = ctx.working_dir.read().await.clone();
        let path = match super::inspect_path_access(&parsed.file_path, &working_dir) {
            Ok(access) => access.path,
            Err(err) => {
                return Ok(ToolResult {
                    call_id: String::new(),
                    output: err.to_string(),
                    success: false,
                });
            }
        };

        if !path.exists() {
            return Ok(ToolResult {
                call_id: String::new(),
                output: format!("File not found: {}", parsed.file_path),
                success: false,
            });
        }

        let mut searcher = ctx.semantic.lock().await;
        match searcher.list_symbols(&path) {
            Some(symbols) if symbols.is_empty() => Ok(ToolResult {
                call_id: String::new(),
                output: format!("No symbols found in {}", parsed.file_path),
                success: true,
            }),
            Some(symbols) => {
                let mut out = format!(
                    "Symbols in {} ({} total):\n\n",
                    parsed.file_path,
                    symbols.len()
                );
                for sym in &symbols {
                    out.push_str(&format!(
                        "  {:4}-{:4}  {}  ({})\n",
                        sym.start_line, sym.end_line, sym.name, sym.kind
                    ));
                }
                out.push_str("\n[Use read_symbol to read any symbol's full source code]");
                Ok(ToolResult {
                    call_id: String::new(),
                    output: out,
                    success: true,
                })
            }
            None => Ok(ToolResult {
                call_id: String::new(),
                output: format!("Failed to parse {}", parsed.file_path),
                success: false,
            }),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn approval_auto_for_workspace_file() {
        let dir = tempfile::tempdir().unwrap();
        let file = dir.path().join("main.rs");
        std::fs::write(&file, "fn main() {}\n").unwrap();
        let ctx = ToolContext::new(dir.path().to_path_buf());
        let args = serde_json::json!({ "file_path": "main.rs" }).to_string();

        assert!(matches!(
            ListSymbolsTool.approval_with_context(&args, &ctx),
            ApprovalRequirement::AutoApprove
        ));
    }

    #[tokio::test]
    async fn approval_requires_read_confirmation_for_external_file() {
        let workspace = tempfile::tempdir().unwrap();
        let outside = tempfile::tempdir().unwrap();
        let file = outside.path().join("main.rs");
        std::fs::write(&file, "fn main() {}\n").unwrap();
        let ctx = ToolContext::new(workspace.path().to_path_buf());
        let args = serde_json::json!({ "file_path": file }).to_string();

        assert!(matches!(
            ListSymbolsTool.approval_with_context(&args, &ctx),
            ApprovalRequirement::RequireApproval(_)
        ));
    }
}