Search the content index. Returns matching lines with file paths, line numbers, and content. Supports path filtering, file type filtering, and case-insensitive search. Results are ranked: source files (`src/`, `lib/`, `pkg/`, `internal/`) are prioritized over config, test, build, and generated files. Within each tier, results preserve filesystem order from the index. The returned [`ContentSearc
(
repo_root: &Path,
pattern: &str,
opts: ContentSearchOptions,
)
| 57 | /// (capped to `max_results`) and the `total_matches` count so callers |
| 58 | /// know how many were found before truncation. |
| 59 | pub fn search_content( |
| 60 | repo_root: &Path, |
| 61 | pattern: &str, |
| 62 | opts: ContentSearchOptions, |
| 63 | ) -> Result<ContentSearchResult, ContentSearchError> { |
| 64 | let config = content_config(repo_root); |
| 65 | let index = Index::open(config)?; |
| 66 | |
| 67 | let display_limit = opts.max_results.unwrap_or(50); |
| 68 | |
| 69 | // Fetch a large candidate set so we can rank before truncating. |
| 70 | // We ask for 10x the display limit (capped at 2000) to get good |
| 71 | // coverage across the repo before applying our own ranking. |
| 72 | let fetch_limit = (display_limit * 10).min(2000); |
| 73 | |
| 74 | let search_opts = SearchOptions { |
| 75 | path_filter: opts.path_filter, |
| 76 | file_type: opts.file_type, |
| 77 | exclude_type: opts.exclude_type, |
| 78 | max_results: Some(fetch_limit), |
| 79 | case_insensitive: opts.case_insensitive, |
| 80 | }; |
| 81 | |
| 82 | let raw_matches = index.search(pattern, &search_opts)?; |
| 83 | let total_matches = raw_matches.len(); |
| 84 | |
| 85 | // Build per-directory match counts for the facets summary. |
| 86 | let mut dir_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new(); |
| 87 | for m in &raw_matches { |
| 88 | let path_str = m.path.to_string_lossy(); |
| 89 | // Use the first two path components as the directory bucket. |
| 90 | let dir = path_str |
| 91 | .splitn(3, '/') |
| 92 | .take(2) |
| 93 | .collect::<Vec<_>>() |
| 94 | .join("/"); |
| 95 | *dir_counts.entry(dir).or_insert(0) += 1; |
| 96 | } |
| 97 | |
| 98 | // Sort directory facets by count (descending). |
| 99 | let mut dir_facets: Vec<(String, usize)> = dir_counts.into_iter().collect(); |
| 100 | dir_facets.sort_by_key(|entry| std::cmp::Reverse(entry.1)); |
| 101 | dir_facets.truncate(10); // top 10 directories |
| 102 | |
| 103 | // Rank matches: source code files first, then everything else. |
| 104 | let mut ranked: Vec<(usize, &syntext::SearchMatch)> = raw_matches |
| 105 | .iter() |
| 106 | .map(|m| (path_tier(m.path.to_string_lossy().as_ref()), m)) |
| 107 | .collect(); |
| 108 | ranked.sort_by_key(|(tier, _)| *tier); |
| 109 | |
| 110 | let matches: Vec<ContentMatch> = ranked |
| 111 | .into_iter() |
| 112 | .take(display_limit) |
| 113 | .map(|(_, m)| ContentMatch { |
| 114 | path: m.path.to_string_lossy().to_string(), |
| 115 | line_number: m.line_number, |
| 116 | line_content: String::from_utf8_lossy(&m.line_content).to_string(), |