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