1use crate::client::SearchfoxClient;
2use crate::types::{File, SearchfoxResponse};
3use anyhow::Result;
4use log::{debug, warn};
5use reqwest::Url;
6
7fn is_constructor_pattern(symbol: &str) -> bool {
8 if let Some(colon_pos) = symbol.rfind("::") {
9 let class_part = &symbol[..colon_pos];
10 let method_part = &symbol[colon_pos + 2..];
11 let class_name = class_part.split("::").last().unwrap_or(class_part);
12 class_name == method_part
13 } else {
14 false
15 }
16}
17
18fn extract_class_name_from_constructor(symbol: &str) -> String {
19 if let Some(colon_pos) = symbol.rfind("::") {
20 symbol[..colon_pos].to_string()
21 } else {
22 symbol.to_string()
23 }
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum CategoryFilter {
28 All,
29 ExcludeTests,
30 ExcludeGenerated,
31 ExcludeTestsAndGenerated,
32 OnlyTests,
33 OnlyGenerated,
34 OnlyNormal,
35}
36
37impl CategoryFilter {
38 pub fn should_include(&self, category: &str) -> bool {
39 match self {
40 CategoryFilter::All => true,
41 CategoryFilter::ExcludeTests => category != "test",
42 CategoryFilter::ExcludeGenerated => category != "generated",
43 CategoryFilter::ExcludeTestsAndGenerated => {
44 category != "test" && category != "generated"
45 }
46 CategoryFilter::OnlyTests => category == "test",
47 CategoryFilter::OnlyGenerated => category == "generated",
48 CategoryFilter::OnlyNormal => category == "normal",
49 }
50 }
51}
52
53#[derive(Debug, Clone)]
54pub struct SearchOptions {
55 pub query: Option<String>,
56 pub path: Option<String>,
57 pub case: bool,
58 pub regexp: bool,
59 pub limit: usize,
60 pub context: Option<usize>,
61 pub symbol: Option<String>,
62 pub id: Option<String>,
63 pub cpp: bool,
64 pub c_lang: bool,
65 pub webidl: bool,
66 pub js: bool,
67 pub java: bool,
68 pub category_filter: CategoryFilter,
69}
70
71impl Default for SearchOptions {
72 fn default() -> Self {
73 Self {
74 query: None,
75 path: None,
76 case: false,
77 regexp: false,
78 limit: 50,
79 context: None,
80 symbol: None,
81 id: None,
82 cpp: false,
83 c_lang: false,
84 webidl: false,
85 js: false,
86 java: false,
87 category_filter: CategoryFilter::All,
88 }
89 }
90}
91
92impl SearchOptions {
93 pub fn matches_language_filter(&self, path: &str) -> bool {
94 if !self.cpp && !self.c_lang && !self.webidl && !self.js && !self.java {
95 return true;
96 }
97
98 let path_lower = path.to_lowercase();
99
100 if self.cpp
101 && (path_lower.ends_with(".cc")
102 || path_lower.ends_with(".cpp")
103 || path_lower.ends_with(".h")
104 || path_lower.ends_with(".hh")
105 || path_lower.ends_with(".hpp"))
106 {
107 return true;
108 }
109
110 if self.c_lang && (path_lower.ends_with(".c") || path_lower.ends_with(".h")) {
111 return true;
112 }
113
114 if self.webidl && path_lower.ends_with(".webidl") {
115 return true;
116 }
117
118 if self.js
119 && (path_lower.ends_with(".js")
120 || path_lower.ends_with(".mjs")
121 || path_lower.ends_with(".ts")
122 || path_lower.ends_with(".cjs")
123 || path_lower.ends_with(".jsx")
124 || path_lower.ends_with(".tsx"))
125 {
126 return true;
127 }
128
129 if self.java && (path_lower.ends_with(".java") || path_lower.ends_with(".kt")) {
130 return true;
131 }
132
133 false
134 }
135
136 pub fn build_query(&self) -> String {
137 if let Some(symbol) = &self.symbol {
138 format!("symbol:{symbol}")
139 } else if let Some(id) = &self.id {
140 format!("id:{id}")
141 } else if let Some(q) = &self.query {
142 if q.contains("path:")
143 || q.contains("pathre:")
144 || q.contains("symbol:")
145 || q.contains("id:")
146 || q.contains("text:")
147 || q.contains("re:")
148 {
149 q.clone()
150 } else if let Some(context) = self.context {
151 format!("context:{context} text:{q}")
152 } else {
153 q.clone()
154 }
155 } else {
156 String::new()
157 }
158 }
159}
160
161pub struct SearchResult {
162 pub path: String,
163 pub line_number: usize,
164 pub line: String,
165}
166
167impl SearchfoxClient {
168 pub async fn search(&self, options: &SearchOptions) -> Result<Vec<SearchResult>> {
169 let query = options.build_query();
170
171 let mut url = Url::parse(&format!("https://searchfox.org/{}/search", self.repo))?;
172 url.query_pairs_mut()
173 .append_pair("q", &query)
174 .append_pair("case", if options.case { "true" } else { "false" })
175 .append_pair("regexp", if options.regexp { "true" } else { "false" });
176 if let Some(path) = &options.path {
177 url.query_pairs_mut().append_pair("path", path);
178 }
179
180 let response = self.get(url).await?;
181
182 if !response.status().is_success() {
183 anyhow::bail!("Request failed: {}", response.status());
184 }
185
186 let response_text = response.text().await?;
187 let json: SearchfoxResponse = serde_json::from_str(&response_text)?;
188
189 let mut results = Vec::new();
190 let mut count = 0;
191
192 for (key, value) in &json {
193 if key.starts_with('*') {
194 continue;
195 }
196
197 if !options.category_filter.should_include(key) {
198 continue;
199 }
200
201 if let Some(files_array) = value.as_array() {
202 for file in files_array {
203 let file: File = match serde_json::from_value(file.clone()) {
204 Ok(f) => f,
205 Err(e) => {
206 warn!("Failed to parse file JSON: {e}");
207 continue;
208 }
209 };
210
211 if !options.matches_language_filter(&file.path) {
212 continue;
213 }
214
215 if options.path.is_some()
216 && options.query.is_none()
217 && options.symbol.is_none()
218 && options.id.is_none()
219 {
220 if count >= options.limit {
221 break;
222 }
223 results.push(SearchResult {
224 path: file.path.clone(),
225 line_number: 0,
226 line: String::new(),
227 });
228 count += 1;
229 } else {
230 for line in file.lines {
231 if count >= options.limit {
232 break;
233 }
234 results.push(SearchResult {
235 path: file.path.clone(),
236 line_number: line.lno,
237 line: line.line.trim_end().to_string(),
238 });
239 count += 1;
240 }
241 }
242 }
243 } else if let Some(obj) = value.as_object() {
244 for (_category, file_list) in obj {
245 if let Some(files) = file_list.as_array() {
246 for file in files {
247 let file: File = match serde_json::from_value(file.clone()) {
248 Ok(f) => f,
249 Err(_) => continue,
250 };
251
252 if !options.matches_language_filter(&file.path) {
253 continue;
254 }
255
256 if options.path.is_some()
257 && options.query.is_none()
258 && options.symbol.is_none()
259 && options.id.is_none()
260 {
261 if count >= options.limit {
262 break;
263 }
264 results.push(SearchResult {
265 path: file.path.clone(),
266 line_number: 0,
267 line: String::new(),
268 });
269 count += 1;
270 } else {
271 for line in file.lines {
272 if count >= options.limit {
273 break;
274 }
275 results.push(SearchResult {
276 path: file.path.clone(),
277 line_number: line.lno,
278 line: line.line.trim_end().to_string(),
279 });
280 count += 1;
281 }
282 }
283 }
284 }
285 }
286 }
287
288 if count >= options.limit {
289 break;
290 }
291 }
292
293 Ok(results)
294 }
295
296 pub async fn find_symbol_locations(
297 &self,
298 symbol: &str,
299 path_filter: Option<&str>,
300 options: &SearchOptions,
301 ) -> Result<Vec<(String, usize)>> {
302 let is_ctor = is_constructor_pattern(symbol);
303 let search_symbol = if is_ctor {
304 extract_class_name_from_constructor(symbol)
305 } else {
306 symbol.to_string()
307 };
308 let query = format!("id:{search_symbol}");
309 let mut url = Url::parse(&format!("https://searchfox.org/{}/search", self.repo))?;
310 url.query_pairs_mut().append_pair("q", &query);
311 if let Some(path) = path_filter {
312 url.query_pairs_mut().append_pair("path", path);
313 }
314
315 let response = self.get(url).await?;
316
317 if !response.status().is_success() {
318 anyhow::bail!("Request failed: {}", response.status());
319 }
320
321 let response_text = response.text().await?;
322 let json: SearchfoxResponse = serde_json::from_str(&response_text)?;
323 let mut file_locations = Vec::new();
324
325 debug!("Analyzing search results...");
326
327 for (key, value) in &json {
328 if key.starts_with('*') {
329 continue;
330 }
331
332 if let Some(files_array) = value.as_array() {
333 debug!("Found {} files in array for key {}", files_array.len(), key);
334 for file in files_array {
335 match serde_json::from_value::<File>(file.clone()) {
336 Ok(file) => {
337 if !options.matches_language_filter(&file.path) {
338 continue;
339 }
340
341 debug!(
342 "Processing file: {} with {} lines",
343 file.path,
344 file.lines.len()
345 );
346 for line in file.lines {
347 if crate::utils::is_potential_definition(&line, symbol) {
348 debug!(
349 "Found potential definition: {}:{} - {}",
350 file.path,
351 line.lno,
352 line.line.trim()
353 );
354 file_locations.push((file.path.clone(), line.lno));
355 }
356 }
357 }
358 Err(e) => {
359 warn!("Failed to parse file JSON: {e}");
360 }
361 }
362 }
363 } else if let Some(categories) = value.as_object() {
364 let symbol_name = symbol.strip_prefix("id:").unwrap_or(symbol);
365 let is_method_search = symbol_name.contains("::") && !is_ctor;
366
367 if !is_method_search && !is_ctor {
368 for (category_name, category_value) in categories {
369 let is_class_def_category = category_name.starts_with("Definitions (")
370 && (category_name.ends_with(&format!("::{symbol_name})"))
371 || category_name.ends_with(&format!("({symbol_name})")));
372 let is_not_constructor =
373 !category_name.contains(&format!("::{symbol_name}::{symbol_name})"));
374
375 if !is_class_def_category || !is_not_constructor {
376 continue;
377 }
378
379 debug!("Found class definition category: {}", category_name);
380 let Some(files_array) = category_value.as_array() else {
381 continue;
382 };
383
384 for file in files_array {
385 let Ok(file) = serde_json::from_value::<File>(file.clone()) else {
386 continue;
387 };
388
389 if !options.matches_language_filter(&file.path) {
390 continue;
391 }
392
393 let mut class_lines = Vec::new();
394 for line in file.lines {
395 if line.line.contains("class ") || line.line.contains("struct ") {
396 debug!(
397 "Found class/struct definition: {}:{} - {}",
398 file.path,
399 line.lno,
400 line.line.trim()
401 );
402 class_lines.push((
403 file.path.clone(),
404 line.lno,
405 line.line.clone(),
406 ));
407 }
408 }
409
410 if class_lines.is_empty() {
411 continue;
412 }
413
414 for (path, lno, line_text) in &class_lines {
415 if !line_text.contains("{}") {
416 return Ok(vec![(path.clone(), *lno)]);
417 }
418 }
419 let (path, lno, _) = &class_lines[0];
420 return Ok(vec![(path.clone(), *lno)]);
421 }
422 }
423 }
424
425 if is_ctor {
426 let ctor_method_part = if let Some(colon_pos) = symbol.rfind("::") {
427 &symbol[colon_pos + 2..]
428 } else {
429 symbol
430 };
431
432 let mut all_ctor_lines = Vec::new();
433 for (category_name, category_value) in categories {
434 if category_name.contains("Definitions")
435 && category_name.ends_with(&format!("::{ctor_method_part})"))
436 {
437 debug!("Found constructor category: {}", category_name);
438 if let Some(files_array) = category_value.as_array() {
439 for file in files_array {
440 match serde_json::from_value::<File>(file.clone()) {
441 Ok(file) => {
442 if !options.matches_language_filter(&file.path) {
443 continue;
444 }
445
446 for line in file.lines {
447 if crate::utils::is_potential_definition(
448 &line, symbol,
449 ) {
450 debug!(
451 "Found constructor definition: {}:{} - {}",
452 file.path,
453 line.lno,
454 line.line.trim()
455 );
456 all_ctor_lines
457 .push((file.path.clone(), line.lno));
458 }
459 }
460 }
461 Err(_) => continue,
462 }
463 }
464 }
465 }
466 }
467
468 if !all_ctor_lines.is_empty() {
469 return Ok(all_ctor_lines);
470 }
471 }
472
473 let search_order = if is_method_search || is_ctor {
474 vec!["Definitions", "Declarations"]
475 } else {
476 vec!["Declarations", "Definitions"]
477 };
478
479 for search_type in search_order {
480 for (category_name, category_value) in categories {
481 if category_name.contains(search_type)
482 && (category_name.contains(symbol_name)
483 || category_name
484 .to_lowercase()
485 .contains(&symbol_name.to_lowercase()))
486 {
487 if let Some(files_array) = category_value.as_array() {
488 for file in files_array {
489 match serde_json::from_value::<File>(file.clone()) {
490 Ok(file) => {
491 if !options.matches_language_filter(&file.path) {
492 continue;
493 }
494
495 for line in file.lines {
496 if let Some(upsearch) = &line.upsearch {
497 if upsearch.starts_with("symbol:_Z") {
498 return Ok(vec![(
499 file.path.clone(),
500 line.lno,
501 )]);
502 }
503 }
504 file_locations.push((file.path.clone(), line.lno));
505 }
506 }
507 Err(_) => continue,
508 }
509 }
510 }
511 }
512 }
513
514 if !file_locations.is_empty() {
515 break;
516 }
517 }
518 }
519 }
520
521 Ok(file_locations)
522 }
523}