1pub mod candidate;
6pub mod grep;
7pub use grep::GrepRun;
8mod index;
9pub mod query;
10mod search;
11
12pub use candidate::Candidate;
13
14pub use search::{
15 BinaryMode, CandidateFilter, CandidateFilterConfig, CaseMode, ColorChoice, ColumnLimit,
16 ColumnOverflow, FilenameMode, GlobConfig, HiddenMode, IgnoreConfig, IgnoreSources,
17 LineStyleFlags, LinkTraversal, Match, MatchEmissionMode, OutputEmission, PassthruMode,
18 PathDisplay, PatternCompiler, RecordTerminator, SearchCollection, SearchError, SearchLineStyle,
19 SearchMatchFlags, SearchMode, SearchOptions, SearchOutcome, SearchOutput, SearchOutputFormat,
20 SearchQuery, SearchRecordStyle, SearchSeparators, SearchStats, TypeDef, VisibilityConfig,
21 WalkOptions, ZeroCountMode,
22};
23
24pub use ignore::{Walk, WalkBuilder};
25
26pub use index::config::WalkOptions as IndexWalkOptions;
27pub use index::meta::StoreMeta;
28pub use index::store::IndexStore;
29pub use index::trigram::{TrigramIndex, TrigramIndexError};
30pub use index::{
31 CorpusKind, CorpusMeta, CorpusSpec, FileId, FilterMeta, Index, IndexConfig, IndexError,
32 IndexId, IndexKind, Indexes, PlanMode, QueryPlanOutput, WalkMeta,
33};
34
35pub use query::{CandidateRequirement, QueryFlags, QueryPlanner, QuerySpec};
36
37use thiserror::Error;
38
39pub const SIFT_DIR: &str = ".sift";
40pub const FILES_BIN: &str = "files.bin";
41pub const LEXICON_BIN: &str = "lexicon.bin";
42pub const POSTINGS_BIN: &str = "postings.bin";
43pub const TRIGRAMS_BIN: &str = "trigrams.bin";
44
45#[derive(Debug, Error)]
47pub enum Error {
48 #[error(transparent)]
49 Index(#[from] IndexError),
50
51 #[error(transparent)]
52 Search(#[from] SearchError),
53
54 #[error("IO error: {0}")]
55 Io(#[from] std::io::Error),
56
57 #[error("ignore walk error: {0}")]
58 Ignore(#[from] ignore::Error),
59
60 #[error("regex error: {0}")]
61 Regex(#[from] Box<regex_automata::meta::BuildError>),
62}
63
64impl From<crate::index::trigram::TrigramIndexError> for Error {
65 fn from(e: crate::index::trigram::TrigramIndexError) -> Self {
66 Self::Index(IndexError::Trigram(e))
67 }
68}
69
70pub type Result<T> = std::result::Result<T, Error>;
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use std::fs;
76 use tempfile::TempDir;
77
78 fn build_trigram_in_tmp(tmp: &TempDir, corpus_path: &std::path::Path) -> TrigramIndex {
79 let sift_dir = tmp.path().join(".sift");
80 let trigram_dir = sift_dir.join("trigram");
81 let config = IndexConfig {
82 corpus: CorpusSpec {
83 root: corpus_path,
84 kind: CorpusKind::Directory,
85 follow_links: false,
86 include_paths: &[],
87 exclude_paths: &[],
88 },
89 walk: IndexWalkOptions::new(false),
90 visibility: VisibilityConfig::default(),
91 };
92 TrigramIndex::build(&config, &trigram_dir, &[]).expect("build index")
93 }
94
95 fn build_index_in_tmp(tmp: &TempDir, corpus_path: &std::path::Path) -> Index {
96 Index::Trigram(build_trigram_in_tmp(tmp, corpus_path))
97 }
98
99 #[test]
100 fn build_open_search_finds_line() {
101 let tmp = TempDir::new().expect("create temp dir");
102 let corpus = tmp.path().join("corpus");
103 fs::create_dir_all(corpus.join("src")).expect("create src dir");
104 fs::write(corpus.join("src/lib.rs"), "fn hello() {\n let x = 1;\n}\n")
105 .expect("write test file");
106
107 let tri = build_trigram_in_tmp(&tmp, &corpus);
108 assert!(
109 tri.file_path(FileId::new(0)).is_some(),
110 "should have indexed files"
111 );
112 let index = Index::Trigram(tri);
113
114 let pat = vec![r"let\s+x".to_string()];
115 let q = SearchQuery::new(&pat, SearchOptions::default()).expect("compile search");
116 let hits = q.collect_index_matches(&index).expect("search");
117 assert_eq!(hits.len(), 1);
118 assert!(hits[0].file.ends_with("src/lib.rs"));
119 assert_eq!(hits[0].line, 2);
120 }
121
122 #[test]
123 fn indexed_search_matches_walk_search_for_literal() {
124 let tmp = TempDir::new().expect("create temp dir");
125 let corpus = tmp.path().join("corpus");
126 fs::create_dir_all(corpus.join("a")).expect("create dir a");
127 fs::create_dir_all(corpus.join("b")).expect("create dir b");
128 fs::write(corpus.join("a/x.txt"), "alpha beta\n").expect("write file a");
129 fs::write(corpus.join("b/y.txt"), "gamma delta\n").expect("write file b");
130
131 let index = build_index_in_tmp(&tmp, &corpus);
132
133 let pat = vec!["beta".to_string()];
134 let opts = SearchOptions::default();
135 let q = SearchQuery::new(&pat, opts).expect("compile search");
136 let naive = q.collect_walk_matches(&corpus).expect("walk search");
137 let hits = q.collect_index_matches(&index).expect("index search");
138 assert_eq!(hits, naive);
139 }
140
141 #[test]
142 fn full_scan_parallel_candidate_path_finds_all_files() {
143 let tmp = TempDir::new().expect("create temp dir");
144 let corpus = tmp.path().join("corpus");
145 fs::create_dir_all(corpus.join("d")).expect("create dir");
146 let n_files = 8;
147 for i in 0..n_files {
148 fs::write(
149 corpus.join("d").join(format!("f{i}.txt")),
150 format!("line {i} needle\n"),
151 )
152 .expect("write file");
153 }
154
155 let tri = build_trigram_in_tmp(&tmp, &corpus);
156 for i in 0..n_files {
157 assert!(
158 tri.file_path(FileId::new(i)).is_some(),
159 "file {i} should be indexed"
160 );
161 }
162 let index = Index::Trigram(tri);
163
164 let pat = vec!["needle".to_string()];
165 let q = SearchQuery::new(&pat, SearchOptions::default()).expect("compile search");
166 let hits = q.collect_index_matches(&index).expect("search");
167 assert_eq!(hits.len(), n_files);
168 }
169
170 #[test]
171 fn index_and_walk_return_same_matches_for_literal_pattern() {
172 let tmp = TempDir::new().expect("create temp dir");
173 let corpus = tmp.path().join("corpus");
174 fs::create_dir_all(corpus.join("keep")).expect("create keep dir");
175 fs::write(corpus.join("keep/a.txt"), "one\ntwo beta\n").expect("write file a");
176 fs::write(corpus.join("keep/b.txt"), "three\n").expect("write file b");
177
178 let index = build_index_in_tmp(&tmp, &corpus);
179
180 let pat = vec!["one".to_string(), "three".to_string()];
181 let q = SearchQuery::new(&pat, SearchOptions::default()).expect("compile search");
182 let mut from_index = q.collect_index_matches(&index).expect("index search");
183 let mut from_walk = q.collect_walk_matches(&corpus).expect("walk search");
184 from_index.sort_by(|a, b| (&a.file, a.line, &a.text).cmp(&(&b.file, b.line, &b.text)));
185 from_walk.sort_by(|a, b| (&a.file, a.line, &a.text).cmp(&(&b.file, b.line, &b.text)));
186 assert_eq!(from_index, from_walk);
187 }
188
189 #[test]
190 fn multi_pattern_search_matches_either() {
191 let tmp = TempDir::new().expect("create temp dir");
192 let corpus = tmp.path().join("corpus");
193 fs::create_dir_all(&corpus).expect("create corpus");
194 fs::write(corpus.join("a.txt"), "hello world\nfoo bar\n").expect("write file");
195
196 let index = build_index_in_tmp(&tmp, &corpus);
197
198 let pat = vec!["hello".to_string(), "foo".to_string()];
199 let q = SearchQuery::new(&pat, SearchOptions::default()).expect("compile search");
200 let hits = q.collect_index_matches(&index).expect("search");
201 assert_eq!(hits.len(), 2);
202 }
203
204 #[test]
205 fn case_insensitive_search_matches_variants() {
206 let tmp = TempDir::new().expect("create temp dir");
207 let corpus = tmp.path().join("corpus");
208 fs::create_dir_all(&corpus).expect("create corpus");
209 fs::write(corpus.join("a.txt"), "Hello WORLD\n").expect("write file");
210
211 let index = build_index_in_tmp(&tmp, &corpus);
212
213 let pat = vec!["hello".to_string()];
214 let opts = SearchOptions {
215 case_mode: CaseMode::Insensitive,
216 ..SearchOptions::default()
217 };
218 let q = SearchQuery::new(&pat, opts).expect("compile search");
219 let hits = q.collect_index_matches(&index).expect("search");
220 assert_eq!(hits.len(), 1);
221 }
222}