@@ -314,22 +314,30 @@ impl HomeView {
314314 KeyCode :: Esc => {
315315 self . search_active = false ;
316316 self . search_query = Input :: default ( ) ;
317- self . filtered_items = None ;
317+ self . search_matches . clear ( ) ;
318+ self . search_match_index = 0 ;
318319 }
319320 KeyCode :: Enter => {
320321 self . search_active = false ;
321322 }
322323 _ => {
323324 self . search_query
324325 . handle_event ( & crossterm:: event:: Event :: Key ( key) ) ;
325- self . update_filter ( ) ;
326+ self . update_search ( ) ;
326327 }
327328 }
328329 return None ;
329330 }
330331
331332 // Normal mode keybindings
332333 match key. code {
334+ KeyCode :: Esc => {
335+ if !self . search_matches . is_empty ( ) {
336+ self . search_matches . clear ( ) ;
337+ self . search_match_index = 0 ;
338+ self . search_query = Input :: default ( ) ;
339+ }
340+ }
333341 KeyCode :: Char ( 'q' ) => return Some ( Action :: Quit ) ,
334342 KeyCode :: Char ( '?' ) => {
335343 self . show_help = true ;
@@ -368,20 +376,38 @@ impl HomeView {
368376 self . search_query = Input :: default ( ) ;
369377 }
370378 KeyCode :: Char ( 'n' ) => {
371- let existing_titles: Vec < String > =
372- self . instances . iter ( ) . map ( |i| i. title . clone ( ) ) . collect ( ) ;
373- let existing_groups: Vec < String > = self
374- . group_tree
375- . get_all_groups ( )
376- . iter ( )
377- . map ( |g| g. path . clone ( ) )
378- . collect ( ) ;
379- self . new_dialog = Some ( NewSessionDialog :: new (
380- self . available_tools . clone ( ) ,
381- existing_titles,
382- existing_groups,
383- self . storage . profile ( ) ,
384- ) ) ;
379+ if !self . search_matches . is_empty ( ) {
380+ self . search_match_index =
381+ ( self . search_match_index + 1 ) % self . search_matches . len ( ) ;
382+ self . cursor = self . search_matches [ self . search_match_index ] ;
383+ self . update_selected ( ) ;
384+ } else {
385+ let existing_titles: Vec < String > =
386+ self . instances . iter ( ) . map ( |i| i. title . clone ( ) ) . collect ( ) ;
387+ let existing_groups: Vec < String > = self
388+ . group_tree
389+ . get_all_groups ( )
390+ . iter ( )
391+ . map ( |g| g. path . clone ( ) )
392+ . collect ( ) ;
393+ self . new_dialog = Some ( NewSessionDialog :: new (
394+ self . available_tools . clone ( ) ,
395+ existing_titles,
396+ existing_groups,
397+ self . storage . profile ( ) ,
398+ ) ) ;
399+ }
400+ }
401+ KeyCode :: Char ( 'N' ) => {
402+ if !self . search_matches . is_empty ( ) {
403+ self . search_match_index = if self . search_match_index == 0 {
404+ self . search_matches . len ( ) - 1
405+ } else {
406+ self . search_match_index - 1
407+ } ;
408+ self . cursor = self . search_matches [ self . search_match_index ] ;
409+ self . update_selected ( ) ;
410+ }
385411 }
386412 KeyCode :: Char ( 's' ) => {
387413 // Open settings view with selected session's project path (if any)
@@ -604,44 +630,30 @@ impl HomeView {
604630 }
605631
606632 pub ( super ) fn move_cursor ( & mut self , delta : i32 ) {
607- let items = if let Some ( ref filtered) = self . filtered_items {
608- filtered. len ( )
609- } else {
610- self . flat_items . len ( )
611- } ;
612-
613- if items == 0 {
633+ if self . flat_items . is_empty ( ) {
614634 return ;
615635 }
616636
617637 let new_cursor = if delta < 0 {
618638 self . cursor . saturating_sub ( ( -delta) as usize )
619639 } else {
620- ( self . cursor + delta as usize ) . min ( items - 1 )
640+ ( self . cursor + delta as usize ) . min ( self . flat_items . len ( ) - 1 )
621641 } ;
622642
623643 self . cursor = new_cursor;
624644 self . update_selected ( ) ;
625645 }
626646
627647 pub ( super ) fn update_selected ( & mut self ) {
628- let item_idx = if let Some ( ref filtered) = self . filtered_items {
629- filtered. get ( self . cursor ) . copied ( )
630- } else {
631- Some ( self . cursor )
632- } ;
633-
634- if let Some ( idx) = item_idx {
635- if let Some ( item) = self . flat_items . get ( idx) {
636- match item {
637- Item :: Session { id, .. } => {
638- self . selected_session = Some ( id. clone ( ) ) ;
639- self . selected_group = None ;
640- }
641- Item :: Group { path, .. } => {
642- self . selected_session = None ;
643- self . selected_group = Some ( path. clone ( ) ) ;
644- }
648+ if let Some ( item) = self . flat_items . get ( self . cursor ) {
649+ match item {
650+ Item :: Session { id, .. } => {
651+ self . selected_session = Some ( id. clone ( ) ) ;
652+ self . selected_group = None ;
653+ }
654+ Item :: Group { path, .. } => {
655+ self . selected_session = None ;
656+ self . selected_group = Some ( path. clone ( ) ) ;
645657 }
646658 }
647659 }
@@ -658,38 +670,57 @@ impl HomeView {
658670 }
659671 }
660672
661- pub ( super ) fn update_filter ( & mut self ) {
662- if self . search_query . value ( ) . is_empty ( ) {
663- self . filtered_items = None ;
673+ pub ( super ) fn update_search ( & mut self ) {
674+ self . search_matches . clear ( ) ;
675+ self . search_match_index = 0 ;
676+
677+ let query = self . search_query . value ( ) ;
678+ if query. is_empty ( ) {
664679 return ;
665680 }
666681
667- let query = self . search_query . value ( ) . to_lowercase ( ) ;
668- let mut matches = Vec :: new ( ) ;
682+ use nucleo_matcher:: pattern:: { Atom , AtomKind , CaseMatching , Normalization } ;
683+ use nucleo_matcher:: { Config , Matcher , Utf32Str } ;
684+
685+ let mut matcher = Matcher :: new ( Config :: DEFAULT . match_paths ( ) ) ;
686+ let atom = Atom :: new (
687+ query,
688+ CaseMatching :: Ignore ,
689+ Normalization :: Smart ,
690+ AtomKind :: Fuzzy ,
691+ false ,
692+ ) ;
693+
694+ let mut scored: Vec < ( usize , u16 ) > = Vec :: new ( ) ;
695+ let mut buf = Vec :: new ( ) ;
669696
670697 for ( idx, item) in self . flat_items . iter ( ) . enumerate ( ) {
671- match item {
698+ let haystack = match item {
672699 Item :: Session { id, .. } => {
673700 if let Some ( inst) = self . instance_map . get ( id) {
674- if inst. title_lower . contains ( & query)
675- || inst. project_path_lower . contains ( & query)
676- {
677- matches. push ( idx) ;
678- }
701+ format ! ( "{} {}" , inst. title, inst. project_path)
702+ } else {
703+ continue ;
679704 }
680705 }
681706 Item :: Group { name, path, .. } => {
682- if name. to_lowercase ( ) . contains ( & query) || path. to_lowercase ( ) . contains ( & query)
683- {
684- matches. push ( idx) ;
685- }
707+ format ! ( "{} {}" , name, path)
686708 }
709+ } ;
710+
711+ let haystack_utf32 = Utf32Str :: new ( & haystack, & mut buf) ;
712+ if let Some ( score) = atom. score ( haystack_utf32, & mut matcher) {
713+ scored. push ( ( idx, score) ) ;
687714 }
688715 }
689716
690- self . filtered_items = Some ( matches) ;
691- self . cursor = 0 ;
692- self . update_selected ( ) ;
717+ scored. sort_by ( |a, b| b. 1 . cmp ( & a. 1 ) ) ;
718+ self . search_matches = scored. into_iter ( ) . map ( |( idx, _) | idx) . collect ( ) ;
719+
720+ if let Some ( & best) = self . search_matches . first ( ) {
721+ self . cursor = best;
722+ self . update_selected ( ) ;
723+ }
693724 }
694725
695726 /// Create a session with optional hooks. Delegates to the background
0 commit comments