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