1use std::path::Path;
10
11use anyhow::Result;
12use grep::printer::StandardBuilder;
13use grep::regex::{RegexMatcher, RegexMatcherBuilder};
14use grep::searcher::{BinaryDetection, Searcher, SearcherBuilder};
15use ignore::WalkState;
16use rayon::prelude::*;
17use termcolor::NoColor;
18
19#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
22pub struct SearchOptions {
23 pub case_insensitive: bool,
24 pub multi_line: bool,
25 pub dot_matches_new_line: bool,
26 pub word: bool,
28 pub fixed_strings: bool,
30 pub before_context: usize,
32 pub after_context: usize,
34}
35
36const BATCH: usize = 512;
39
40pub(crate) fn build_matcher(pattern: &str, opts: SearchOptions) -> Result<RegexMatcher> {
41 Ok(RegexMatcherBuilder::new()
42 .case_insensitive(opts.case_insensitive)
43 .multi_line(opts.multi_line)
44 .dot_matches_new_line(opts.dot_matches_new_line)
45 .word(opts.word)
46 .build(pattern)?)
47}
48
49fn build_searcher(opts: SearchOptions) -> Searcher {
50 SearcherBuilder::new()
51 .line_number(true)
52 .binary_detection(BinaryDetection::quit(0))
53 .multi_line(opts.multi_line)
54 .before_context(opts.before_context)
55 .after_context(opts.after_context)
56 .build()
57}
58
59fn display_path<'a>(path: &'a Path, root: &Path) -> &'a Path {
63 path.strip_prefix(root).unwrap_or(path)
64}
65
66fn search_one(
69 searcher: &mut Searcher,
70 matcher: &RegexMatcher,
71 path: &Path,
72 root: &Path,
73 buf: &mut Vec<u8>,
74) {
75 buf.clear();
76 let mut printer = StandardBuilder::new().build(NoColor::new(&mut *buf));
77 let shown = display_path(path, root);
78 let _ = searcher.search_path(matcher, path, printer.sink_with_path(matcher, shown));
79}
80
81pub fn search_streaming(
86 pattern: &str,
87 paths: &[&Path],
88 root: &Path,
89 opts: SearchOptions,
90 mut emit: impl FnMut(&[u8]) -> Result<()>,
91) -> Result<()> {
92 let matcher = build_matcher(pattern, opts)?;
93 for batch in paths.chunks(BATCH) {
94 let chunks: Vec<Vec<u8>> = batch
95 .par_iter()
96 .map_init(
97 || (build_searcher(opts), Vec::new()),
98 |(searcher, buf), path| {
99 search_one(searcher, &matcher, path, root, buf);
100 std::mem::take(buf)
101 },
102 )
103 .collect();
104 for c in &chunks {
105 emit(c)?;
106 }
107 }
108 Ok(())
109}
110
111pub fn full_scan(
117 root: &Path,
118 pattern: &str,
119 opts: SearchOptions,
120 sink: impl Fn(&[u8]) + Sync,
121) -> Result<()> {
122 let matcher = build_matcher(pattern, opts)?;
123 let matcher = &matcher;
124 let sink = &sink;
125 crate::index::walk_builder(root).build_parallel().run(|| {
126 let mut searcher = build_searcher(opts);
129 let mut printer = StandardBuilder::new().build(NoColor::new(Vec::<u8>::new()));
130 Box::new(move |res| {
131 if let Ok(entry) = res
132 && entry.file_type().is_some_and(|t| t.is_file())
133 {
134 let path = entry.path();
135 let shown = display_path(path, root);
136 let _ = searcher.search_path(matcher, path, printer.sink_with_path(matcher, shown));
137 let buf = printer.get_mut().get_mut();
138 if !buf.is_empty() {
139 sink(buf);
140 buf.clear();
141 }
142 }
143 WalkState::Continue
144 })
145 });
146 Ok(())
147}
148
149pub fn search(pattern: &str, paths: &[&Path], root: &Path, opts: SearchOptions) -> Result<Vec<u8>> {
151 let mut out = Vec::new();
152 search_streaming(pattern, paths, root, opts, |c| {
153 out.extend_from_slice(c);
154 Ok(())
155 })?;
156 Ok(out)
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn emits_path_line_text() {
165 let tmp = std::env::temp_dir().join(format!("rgx_confirm_{}", std::process::id()));
166 let _ = std::fs::remove_dir_all(&tmp);
167 std::fs::create_dir_all(&tmp).unwrap();
168 let p = tmp.join("f.txt");
169 std::fs::write(&p, b"alpha\nbeta NEEDLE gamma\ndelta\n").unwrap();
170
171 let out = search("NEEDLE", &[p.as_path()], &tmp, SearchOptions::default()).unwrap();
172 let text = String::from_utf8(out).unwrap();
173 assert!(
174 text.starts_with("f.txt:2:beta NEEDLE gamma"),
175 "got: {text:?}"
176 );
177 assert!(!text.contains("alpha"));
178 assert!(
179 !text.contains(tmp.to_str().unwrap()),
180 "path should be relative: {text:?}"
181 );
182 let _ = std::fs::remove_dir_all(&tmp);
183 }
184}