1use 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#[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#[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
33pub 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
179pub 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}