Skip to main content

codesearch/
find.rs

1//! Structural symbol find: definitions, references, callers
2//!
3//! Combines extract, callgraph, and search for "grep that understands code".
4
5use crate::callgraph::build_call_graph;
6use crate::extract::{extract_classes, extract_functions};
7use crate::parser::read_file_content;
8use crate::search::list_files;
9use crate::search::search_code;
10use crate::types::SearchOptions;
11use serde::Serialize;
12use std::path::Path;
13
14/// A single find result (definition, reference, or call site)
15#[derive(Debug, Clone, Serialize)]
16pub struct FindResult {
17    pub kind: String,
18    pub file: String,
19    pub line: usize,
20    pub content: String,
21    pub symbol: String,
22}
23
24/// Result of find_symbol
25#[derive(Debug, Clone, Serialize)]
26pub struct FindReport {
27    pub symbol: String,
28    pub definitions: Vec<FindResult>,
29    pub references: Vec<FindResult>,
30    pub callers: Vec<FindResult>,
31}
32
33/// Find symbol: definitions, references, and callers
34pub fn find_symbol(
35    symbol: &str,
36    path: &Path,
37    extensions: Option<&[String]>,
38    exclude: Option<&[String]>,
39    find_type: FindType,
40) -> Result<FindReport, Box<dyn std::error::Error>> {
41    let mut definitions = Vec::new();
42    let mut references = Vec::new();
43    let mut callers = Vec::new();
44
45    let files = list_files(path, extensions, exclude)?;
46
47    if find_type == FindType::Definition || find_type == FindType::All {
48        for file in &files {
49            let content = read_file_content(&file.path);
50            let path_str = file.path.as_str();
51
52            for (name, line) in extract_functions(&content, path_str) {
53                if name == symbol {
54                    let line_content = content
55                        .lines()
56                        .nth(line.saturating_sub(1))
57                        .unwrap_or("")
58                        .trim();
59                    definitions.push(FindResult {
60                        kind: "FUNCTION".to_string(),
61                        file: path_str.to_string(),
62                        line,
63                        content: line_content.to_string(),
64                        symbol: symbol.to_string(),
65                    });
66                }
67            }
68            for (name, line) in extract_classes(&content, path_str) {
69                if name == symbol {
70                    let line_content = content
71                        .lines()
72                        .nth(line.saturating_sub(1))
73                        .unwrap_or("")
74                        .trim();
75                    definitions.push(FindResult {
76                        kind: "CLASS".to_string(),
77                        file: path_str.to_string(),
78                        line,
79                        content: line_content.to_string(),
80                        symbol: symbol.to_string(),
81                    });
82                }
83            }
84        }
85    }
86
87    if find_type == FindType::References || find_type == FindType::All {
88        let pattern = format!(r"\b{}\b", regex::escape(symbol));
89        let options = SearchOptions {
90            extensions: extensions.map(|s| s.to_vec()),
91            ignore_case: false,
92            fuzzy: false,
93            fuzzy_threshold: 0.6,
94            max_results: 1000,
95            exclude: exclude.map(|s| s.to_vec()),
96            rank: false,
97            cache: false,
98            semantic: false,
99            benchmark: false,
100            vs_grep: false,
101            use_bm25: false,
102            context_filter: None,
103            fixed_string: false,
104            declaration_filter: None,
105            context: 0,
106        };
107        let results = search_code(&pattern, path, &options)?;
108        for r in results {
109            let is_definition = definitions
110                .iter()
111                .any(|d| d.file == r.file && d.line == r.line_number);
112            if !is_definition {
113                references.push(FindResult {
114                    kind: "REFERENCE".to_string(),
115                    file: r.file.clone(),
116                    line: r.line_number,
117                    content: r.content.trim().to_string(),
118                    symbol: symbol.to_string(),
119                });
120            }
121        }
122    }
123
124    if find_type == FindType::Callers || find_type == FindType::All {
125        let graph = build_call_graph(path, extensions, exclude)?;
126        for edge in &graph.edges {
127            if edge.callee == symbol {
128                let file = graph
129                    .nodes
130                    .get(&edge.caller)
131                    .map(|n| n.file_path.clone())
132                    .unwrap_or_default();
133                if !file.is_empty() {
134                    let content = std::fs::read_to_string(&file).unwrap_or_default();
135                    let line_content = content
136                        .lines()
137                        .nth(edge.call_site_line.saturating_sub(1))
138                        .unwrap_or("")
139                        .trim();
140                    callers.push(FindResult {
141                        kind: "CALL".to_string(),
142                        file,
143                        line: edge.call_site_line,
144                        content: line_content.to_string(),
145                        symbol: format!("{} calls {}", edge.caller, symbol),
146                    });
147                }
148            }
149        }
150    }
151
152    Ok(FindReport {
153        symbol: symbol.to_string(),
154        definitions,
155        references,
156        callers,
157    })
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum FindType {
162    Definition,
163    References,
164    Callers,
165    All,
166}
167
168impl FindType {
169    pub fn parse(s: &str) -> Self {
170        match s.to_lowercase().as_str() {
171            "definition" | "def" | "definitions" => FindType::Definition,
172            "references" | "ref" | "refs" => FindType::References,
173            "callers" | "calls" => FindType::Callers,
174            _ => FindType::All,
175        }
176    }
177}
178
179/// Print find report to stdout
180pub fn print_find_report(report: &FindReport, find_type: FindType) {
181    use colored::*;
182
183    println!("{}", format!("find '{}'", report.symbol).cyan().bold());
184    println!("{}", "─".repeat(40).cyan());
185    println!();
186
187    if (find_type == FindType::Definition || find_type == FindType::All)
188        && !report.definitions.is_empty()
189    {
190        println!("{}", "DEFINITIONS".green().bold());
191        for d in &report.definitions {
192            println!(
193                "  {} {}:{}",
194                d.file.cyan(),
195                d.line.to_string().yellow(),
196                d.content
197            );
198        }
199        println!();
200    }
201
202    if (find_type == FindType::Callers || find_type == FindType::All) && !report.callers.is_empty()
203    {
204        println!("{}", "CALLERS".green().bold());
205        for c in &report.callers {
206            println!(
207                "  {} {}:{}",
208                c.file.cyan(),
209                c.line.to_string().yellow(),
210                c.content
211            );
212        }
213        println!();
214    }
215
216    if (find_type == FindType::References || find_type == FindType::All)
217        && !report.references.is_empty()
218    {
219        println!("{}", "REFERENCES".green().bold());
220        for r in &report.references {
221            println!(
222                "  {} {}:{}",
223                r.file.cyan(),
224                r.line.to_string().yellow(),
225                r.content
226            );
227        }
228        println!();
229    }
230
231    if report.definitions.is_empty() && report.callers.is_empty() && report.references.is_empty() {
232        println!("{}", "No matches found.".dimmed());
233    }
234}