@@ -4,6 +4,7 @@ use crate::order::PriorityConfig;
44use crate :: utils:: tree_arena:: JsonTreeArena as TreeArena ;
55
66use crate :: InputKind ;
7+ use crate :: grep:: GrepConfig ;
78
89pub mod fileset;
910pub mod format;
@@ -25,48 +26,104 @@ pub(crate) struct IngestOutput {
2526 pub warnings : Vec < String > ,
2627}
2728
29+ /// Return a copy of `cfg` with array sampling disabled when strong grep is
30+ /// active. Non-JSONL formats need this to avoid sampling away matches;
31+ /// JSONL handles it via `merge_required` in the sampler instead.
32+ pub ( crate ) fn grep_adjusted_cfg (
33+ cfg : & PriorityConfig ,
34+ grep : & GrepConfig ,
35+ ) -> PriorityConfig {
36+ if grep. has_strong ( ) {
37+ let mut c = * cfg;
38+ c. array_max_items = usize:: MAX ;
39+ c
40+ } else {
41+ * cfg
42+ }
43+ }
44+
45+ /// Build a predicate that returns true for JSONL line indices matching the
46+ /// strong grep pattern. When no grep is active, returns a no-op.
47+ ///
48+ /// Uses a single regex scan over the entire text and maps match positions
49+ /// back to line indices, avoiding per-line regex overhead.
50+ pub ( crate ) fn jsonl_grep_predicate (
51+ bytes : & [ u8 ] ,
52+ grep : & GrepConfig ,
53+ ) -> Box < dyn Fn ( usize ) -> bool > {
54+ let Some ( re) = grep. patterns . strong ( ) else {
55+ return Box :: new ( |_| false ) ;
56+ } ;
57+ let Ok ( text) = std:: str:: from_utf8 ( bytes) else {
58+ return Box :: new ( |_| false ) ;
59+ } ;
60+ let offsets = formats:: json:: jsonl_line_offsets ( text) ;
61+ if offsets. is_empty ( ) {
62+ return Box :: new ( |_| false ) ;
63+ }
64+ // Single regex pass: find all match positions and map to line indices.
65+ let mut matching = vec ! [ false ; offsets. len( ) ] ;
66+ for m in re. find_iter ( text) {
67+ let pos = m. start ( ) ;
68+ // Binary search for the line containing this byte position.
69+ let idx = offsets. partition_point ( |& ( start, _) | start <= pos) ;
70+ if idx > 0 {
71+ matching[ idx - 1 ] = true ;
72+ }
73+ }
74+ Box :: new ( move |i : usize | matching. get ( i) . copied ( ) . unwrap_or ( false ) )
75+ }
76+
2877/// Dispatch the appropriate ingest path for any supported input kind.
2978pub ( crate ) fn ingest_into_arena (
3079 input : InputKind ,
3180 priority_cfg : & PriorityConfig ,
81+ grep : & GrepConfig ,
3282) -> Result < IngestOutput > {
3383 match input {
3484 InputKind :: Json ( bytes) => {
35- parse_json_one ( bytes, priority_cfg) . map ( |arena| IngestOutput {
85+ let cfg = grep_adjusted_cfg ( priority_cfg, grep) ;
86+ parse_json_one ( bytes, & cfg) . map ( |arena| IngestOutput {
3687 arena,
3788 warnings : Vec :: new ( ) ,
3889 } )
3990 }
4091 InputKind :: Jsonl ( bytes) => {
41- parse_jsonl_one ( & bytes, priority_cfg) . map ( |arena| IngestOutput {
42- arena,
43- warnings : Vec :: new ( ) ,
44- } )
92+ let must_include = jsonl_grep_predicate ( & bytes, grep) ;
93+ parse_jsonl_one ( & bytes, priority_cfg, & * must_include) . map (
94+ |arena| IngestOutput {
95+ arena,
96+ warnings : Vec :: new ( ) ,
97+ } ,
98+ )
4599 }
46100 InputKind :: Yaml ( bytes) => {
47- parse_yaml_one ( & bytes, priority_cfg) . map ( |arena| IngestOutput {
101+ let cfg = grep_adjusted_cfg ( priority_cfg, grep) ;
102+ parse_yaml_one ( & bytes, & cfg) . map ( |arena| IngestOutput {
48103 arena,
49104 warnings : Vec :: new ( ) ,
50105 } )
51106 }
52107 InputKind :: Text { bytes, mode } => {
108+ let cfg = grep_adjusted_cfg ( priority_cfg, grep) ;
53109 let atomic = matches ! ( mode, crate :: TextMode :: CodeLike ) ;
54- parse_text_one_with_mode ( bytes, priority_cfg , atomic) . map (
55- |arena| IngestOutput {
110+ parse_text_one_with_mode ( bytes, & cfg , atomic) . map ( |arena| {
111+ IngestOutput {
56112 arena,
57113 warnings : Vec :: new ( ) ,
58- } ,
59- )
114+ }
115+ } )
60116 }
61117 InputKind :: Fileset ( inputs) => {
62- Ok ( fileset:: parse_fileset_multi ( inputs, priority_cfg) )
118+ Ok ( fileset:: parse_fileset_multi ( inputs, priority_cfg, grep ) )
63119 }
64120 }
65121}
66122
67123#[ cfg( test) ]
68124mod tests {
69125 use super :: * ;
126+ use crate :: grep:: { GrepConfig , GrepPatterns , GrepShow } ;
70127 use crate :: order:: NodeKind ;
71128
72129 #[ test]
@@ -108,6 +165,85 @@ mod tests {
108165 assert_eq ! ( arena. nodes[ root] . object_len. unwrap_or( 0 ) , 2 ) ;
109166 }
110167
168+ fn grep_with_strong ( pattern : & str ) -> GrepConfig {
169+ GrepConfig {
170+ patterns : GrepPatterns :: StrongOnly (
171+ regex:: Regex :: new ( pattern) . unwrap ( ) ,
172+ ) ,
173+ show : GrepShow :: Matching ,
174+ }
175+ }
176+
177+ #[ test]
178+ fn jsonl_grep_predicate_marks_matching_lines ( ) {
179+ let input = b"{\" a\" :1}\n {\" b\" :2}\n {\" c\" :3}\n " ;
180+ let grep = grep_with_strong ( "b" ) ;
181+ let pred = jsonl_grep_predicate ( input, & grep) ;
182+ assert ! ( !pred( 0 ) , "line 0 should not match" ) ;
183+ assert ! ( pred( 1 ) , "line 1 should match 'b'" ) ;
184+ assert ! ( !pred( 2 ) , "line 2 should not match" ) ;
185+ }
186+
187+ #[ test]
188+ fn jsonl_grep_predicate_multiple_matches ( ) {
189+ let input = b"{\" x\" :1}\n {\" x\" :2}\n {\" y\" :3}\n {\" x\" :4}\n " ;
190+ let grep = grep_with_strong ( "x" ) ;
191+ let pred = jsonl_grep_predicate ( input, & grep) ;
192+ assert ! ( pred( 0 ) ) ;
193+ assert ! ( pred( 1 ) ) ;
194+ assert ! ( !pred( 2 ) ) ;
195+ assert ! ( pred( 3 ) ) ;
196+ }
197+
198+ #[ test]
199+ fn jsonl_grep_predicate_no_strong_pattern_returns_noop ( ) {
200+ let input = b"{\" a\" :1}\n {\" b\" :2}\n " ;
201+ let grep = GrepConfig :: default ( ) ; // no patterns
202+ let pred = jsonl_grep_predicate ( input, & grep) ;
203+ assert ! ( !pred( 0 ) ) ;
204+ assert ! ( !pred( 1 ) ) ;
205+ }
206+
207+ #[ test]
208+ fn jsonl_grep_predicate_skips_empty_lines ( ) {
209+ // Empty lines are excluded from offsets, so indices are dense
210+ let input = b"{\" a\" :1}\n \n {\" b\" :2}\n " ;
211+ let grep = grep_with_strong ( "b" ) ;
212+ let pred = jsonl_grep_predicate ( input, & grep) ;
213+ // Only 2 non-empty lines: index 0 = {"a":1}, index 1 = {"b":2}
214+ assert ! ( !pred( 0 ) ) ;
215+ assert ! ( pred( 1 ) ) ;
216+ }
217+
218+ #[ test]
219+ fn jsonl_grep_predicate_match_on_first_line ( ) {
220+ let input = b"{\" needle\" :true}\n {\" other\" :false}\n " ;
221+ let grep = grep_with_strong ( "needle" ) ;
222+ let pred = jsonl_grep_predicate ( input, & grep) ;
223+ assert ! ( pred( 0 ) , "match on first line should work" ) ;
224+ assert ! ( !pred( 1 ) ) ;
225+ }
226+
227+ #[ test]
228+ fn jsonl_grep_predicate_match_on_last_line ( ) {
229+ let input = b"{\" a\" :1}\n {\" needle\" :true}" ;
230+ let grep = grep_with_strong ( "needle" ) ;
231+ let pred = jsonl_grep_predicate ( input, & grep) ;
232+ assert ! ( !pred( 0 ) ) ;
233+ assert ! (
234+ pred( 1 ) ,
235+ "match on last line (no trailing newline) should work"
236+ ) ;
237+ }
238+
239+ #[ test]
240+ fn jsonl_grep_predicate_out_of_bounds_returns_false ( ) {
241+ let input = b"{\" a\" :1}\n {\" b\" :2}\n " ;
242+ let grep = grep_with_strong ( "a" ) ;
243+ let pred = jsonl_grep_predicate ( input, & grep) ;
244+ assert ! ( !pred( 99 ) , "out of bounds index should return false" ) ;
245+ }
246+
111247 #[ test]
112248 fn fileset_ingest_surfaces_parse_warnings ( ) {
113249 let inputs = vec ! [ fileset:: FilesetInput {
@@ -118,6 +254,7 @@ mod tests {
118254 let IngestOutput { arena, warnings } = ingest_into_arena (
119255 InputKind :: Fileset ( inputs) ,
120256 & PriorityConfig :: new ( usize:: MAX , usize:: MAX ) ,
257+ & GrepConfig :: default ( ) ,
121258 )
122259 . unwrap ( ) ;
123260 assert ! ( arena. is_fileset, "fileset input should mark arena" ) ;
0 commit comments