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