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,
)
| 151 | /// (capped to `max_results`) and the `total_matches` count so callers |
| 152 | /// know how many were found before truncation. |
| 153 | pub fn search_content( |
| 154 | repo_root: &Path, |
| 155 | pattern: &str, |
| 156 | opts: ContentSearchOptions, |
| 157 | ) -> Result<ContentSearchResult, ContentSearchError> { |
| 158 | let config = content_config(repo_root); |
| 159 | let index = Index::open(config)?; |
| 160 | |
| 161 | let display_limit = opts.max_results.unwrap_or(50); |
| 162 | |
| 163 | // Fetch a large candidate set so we can rank before truncating. |
| 164 | // We ask for 10x the display limit (capped at 2000) to get good |
| 165 | // coverage across the repo before applying our own ranking. |
| 166 | let fetch_limit = (display_limit * 10).min(2000); |
| 167 | |
| 168 | let search_opts = SearchOptions { |
| 169 | path_filter: opts.path_filter, |
| 170 | file_type: opts.file_type, |
| 171 | exclude_type: opts.exclude_type, |
| 172 | max_results: Some(fetch_limit), |
| 173 | case_insensitive: opts.case_insensitive, |
| 174 | }; |
| 175 | |
| 176 | let raw_matches = index.search(pattern, &search_opts)?; |
| 177 | |
| 178 | // Drop matches in ignored or Atomic-internal paths. syntext's index walk |
| 179 | // respects `.gitignore`/`.ignore` but NOT `.atomicignore`, and it descends |
| 180 | // into hidden dirs like `.atomic`/`.vault`, so build artifacts and |
| 181 | // dependencies excluded only by `.atomicignore` (plus Atomic internals) |
| 182 | // would otherwise surface here and, via the KG search's content-only file |
| 183 | // promotion, as `file:` results. Filter at this shared consumption point |
| 184 | // so both `query search` and `query code` stay clean regardless of what |
| 185 | // the index contains. |
| 186 | let ignore_rules = crate::ignore::IgnoreRules::load_for_enrichment(repo_root); |
| 187 | let raw_matches: Vec<syntext::SearchMatch> = raw_matches |
| 188 | .into_iter() |
| 189 | .filter(|m| { |
| 190 | !crate::ignore::is_enrichment_internal(&m.path) |
| 191 | && !ignore_rules.is_ignored(&m.path, false) |
| 192 | }) |
| 193 | .collect(); |
| 194 | let total_matches = raw_matches.len(); |
| 195 | |
| 196 | // Build per-directory match counts for the facets summary. |
| 197 | let mut dir_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new(); |
| 198 | for m in &raw_matches { |
| 199 | let path_str = m.path.to_string_lossy(); |
| 200 | // Use the first two path components as the directory bucket. |
| 201 | let dir = path_str |
| 202 | .splitn(3, '/') |
| 203 | .take(2) |
| 204 | .collect::<Vec<_>>() |
| 205 | .join("/"); |
| 206 | *dir_counts.entry(dir).or_insert(0) += 1; |
| 207 | } |
| 208 | |
| 209 | // Sort directory facets by count (descending). |
| 210 | let mut dir_facets: Vec<(String, usize)> = dir_counts.into_iter().collect(); |