@@ -2,7 +2,6 @@ use std::ops::Range;
2
2
3
3
use egui:: { Color32 , NumExt as _, Widget as _} ;
4
4
use itertools:: Itertools ;
5
- use nohash_hasher:: IntMap ;
6
5
use smallvec:: SmallVec ;
7
6
8
7
use crate :: { list_item, UiExt as _} ;
@@ -226,11 +225,11 @@ impl FilterMatcher {
226
225
None => Some ( PathRanges :: default ( ) ) ,
227
226
Some ( [ ] ) => None ,
228
227
Some ( keywords) => {
229
- let hierarchy = path. into_iter ( ) . map ( str:: to_lowercase) . collect_vec ( ) ;
228
+ let path = path. into_iter ( ) . map ( str:: to_lowercase) . collect_vec ( ) ;
230
229
231
230
let all_ranges = keywords
232
231
. iter ( )
233
- . map ( |keyword| keyword. match_path ( hierarchy . iter ( ) . map ( String :: as_str) ) )
232
+ . map ( |keyword| keyword. match_path ( path . iter ( ) . map ( String :: as_str) ) )
234
233
. collect_vec ( ) ;
235
234
236
235
// all keywords must match!
@@ -256,13 +255,12 @@ impl FilterMatcher {
256
255
/// `match_from_first_part_start` and/or `match_to_last_part_end`, which have the same behavior as
257
256
/// regex's `^` and `$`.
258
257
///
259
- /// If the keyword has multiple parts, the tested path must have at least one instance of contiguous
258
+ /// If the keyword has multiple parts, e.g. "first/second", the tested path must have at least one instance of contiguous
260
259
/// parts which match the corresponding keyword parts. In that context, the keyword parts have the
261
260
/// following behavior:
262
261
/// - First keyword part: `^part$` if `match_from_first_part_start`, `part$` otherwise
263
262
/// - Last keyword part: `^part$` if `match_to_last_part_end`, `^part` otherwise
264
263
/// - Other keyword parts: `^part$`
265
-
266
264
#[ derive( Debug , Clone , PartialEq ) ]
267
265
struct Keyword {
268
266
/// The parts of a keyword.
@@ -279,6 +277,8 @@ impl Keyword {
279
277
///
280
278
/// The string must not contain any whitespace!
281
279
fn new ( mut keyword : & str ) -> Self {
280
+ // Invariant: keywords are not empty
281
+ debug_assert ! ( !keyword. is_empty( ) ) ;
282
282
debug_assert ! ( !keyword. contains( char :: is_whitespace) ) ;
283
283
284
284
let match_from_first_part_start = if let Some ( k) = keyword. strip_prefix ( '/' ) {
@@ -307,16 +307,25 @@ impl Keyword {
307
307
/// Match the keyword against the provided path.
308
308
///
309
309
/// An empty [`PathRanges`] means that the keyword didn't match the path.
310
- fn match_path < ' a > ( & self , path : impl IntoIterator < Item = & ' a str > ) -> PathRanges {
310
+ ///
311
+ /// Implementation notes:
312
+ /// - This function is akin to a "sliding window" of the keyword parts against the path parts,
313
+ /// trying to find some "alignment" yielding a match.
314
+ /// - We must be thorough as we want to find _all_ match highlights (i.e., we don't early out as
315
+ /// soon as we find a match).
316
+ fn match_path < ' a > ( & self , lowercase_path : impl ExactSizeIterator < Item = & ' a str > ) -> PathRanges {
311
317
let mut state_machines = vec ! [ ] ;
312
318
313
- for ( part_index, part) in path. into_iter ( ) . enumerate ( ) {
314
- let lowercase_part = part. to_lowercase ( ) ;
319
+ let path_length = lowercase_path. len ( ) ;
315
320
316
- state_machines. push ( MatchStateMachine :: new ( self ) ) ;
321
+ for ( path_part_index, path_part) in lowercase_path. into_iter ( ) . enumerate ( ) {
322
+ // Only start a new state machine if it has a chance to be matched entirely.
323
+ if self . parts . len ( ) <= ( path_length - path_part_index) {
324
+ state_machines. push ( MatchStateMachine :: new ( self ) ) ;
325
+ }
317
326
318
327
for state_machine in & mut state_machines {
319
- state_machine. step ( & lowercase_part , part_index ) ;
328
+ state_machine. process_next_path_part ( path_part , path_part_index ) ;
320
329
}
321
330
}
322
331
@@ -342,7 +351,7 @@ impl Keyword {
342
351
/// merged when read, which only happens with [`Self::remove`].
343
352
#[ derive( Debug , Default ) ]
344
353
pub struct PathRanges {
345
- ranges : IntMap < usize , Vec < Range < usize > > > ,
354
+ ranges : ahash :: HashMap < usize , Vec < Range < usize > > > ,
346
355
}
347
356
348
357
impl PathRanges {
@@ -435,7 +444,7 @@ impl<'a> MatchStateMachine<'a> {
435
444
matches ! ( self . state, MatchState :: Match )
436
445
}
437
446
438
- fn step ( & mut self , part : & str , part_index : usize ) {
447
+ fn process_next_path_part ( & mut self , part : & str , part_index : usize ) {
439
448
if matches ! ( self . state, MatchState :: Match | MatchState :: NoMatch ) {
440
449
return ;
441
450
}
@@ -694,17 +703,17 @@ mod test {
694
703
695
704
#[ test]
696
705
fn test_keyword_match_path ( ) {
697
- fn match_and_normalize ( query : & str , path : & [ & str ] ) -> Vec < Vec < Range < usize > > > {
698
- let keyword = Keyword :: new ( query) ;
699
- let path = path . to_vec ( ) ;
700
- keyword . match_path ( path . clone ( ) ) . into_vec ( path . len ( ) )
706
+ fn match_and_normalize ( query : & str , lowercase_path : & [ & str ] ) -> Vec < Vec < Range < usize > > > {
707
+ Keyword :: new ( query)
708
+ . match_path ( lowercase_path . iter ( ) . copied ( ) )
709
+ . into_vec ( lowercase_path . len ( ) )
701
710
}
702
711
703
712
assert_eq ! ( match_and_normalize( "a" , & [ "a" ] ) , vec![ vec![ 0 ..1 ] ] ) ;
704
713
assert_eq ! ( match_and_normalize( "a" , & [ "aaa" ] ) , vec![ vec![ 0 ..3 ] ] ) ;
705
714
706
715
assert_eq ! (
707
- match_and_normalize( "A /" , & [ "aaa" , "aaa" ] ) ,
716
+ match_and_normalize( "a /" , & [ "aaa" , "aaa" ] ) ,
708
717
vec![ vec![ 2 ..3 ] , vec![ 2 ..3 ] ]
709
718
) ;
710
719
@@ -737,31 +746,29 @@ mod test {
737
746
) ;
738
747
739
748
assert ! (
740
- match_and_normalize( "a/B /c/" , & [ "aaa" , "b" , "cccc" ] )
749
+ match_and_normalize( "a/b /c/" , & [ "aaa" , "b" , "cccc" ] )
741
750
. into_iter( )
742
751
. flatten( )
743
752
. count( )
744
753
== 0 ,
745
754
) ;
746
755
747
756
assert_eq ! (
748
- match_and_normalize( "ab/cd" , & [ "xxxAb " , "cDaB " , "Cdxxx " ] ) ,
757
+ match_and_normalize( "ab/cd" , & [ "xxxab " , "cdab " , "cdxxx " ] ) ,
749
758
vec![ vec![ 3 ..5 ] , vec![ 0 ..4 ] , vec![ 0 ..2 ] ]
750
759
) ;
751
760
752
761
assert_eq ! (
753
- match_and_normalize( "ab/ab" , & [ "xxxAb " , "aB " , "aBxxx " ] ) ,
762
+ match_and_normalize( "ab/ab" , & [ "xxxab " , "ab " , "abxxx " ] ) ,
754
763
vec![ vec![ 3 ..5 ] , vec![ 0 ..2 ] , vec![ 0 ..2 ] ]
755
764
) ;
756
765
}
757
766
758
767
#[ test]
759
768
fn test_match_path ( ) {
760
769
fn match_and_normalize ( query : & str , path : & [ & str ] ) -> Option < Vec < Vec < Range < usize > > > > {
761
- let matcher = FilterMatcher :: new ( Some ( query) ) ;
762
- let path = path. to_vec ( ) ;
763
- matcher
764
- . match_path ( path. clone ( ) )
770
+ FilterMatcher :: new ( Some ( query) )
771
+ . match_path ( path. iter ( ) . copied ( ) )
765
772
. map ( |ranges| ranges. into_vec ( path. len ( ) ) )
766
773
}
767
774
0 commit comments