@@ -125,6 +125,7 @@ const SUBCOMMANDS: &[&str] = &[
125125/// Prefix used to search user saved applications.
126126pub const APP_PREFIX : & str = "app" ;
127127const NOTE_SEARCH_DEBOUNCE : Duration = Duration :: from_secs ( 1 ) ;
128+ const COMPLETION_REBUILD_DEBOUNCE : Duration = Duration :: from_millis ( 120 ) ;
128129
129130fn scale_ui < R > ( ui : & mut egui:: Ui , scale : f32 , add_contents : impl FnOnce ( & mut egui:: Ui ) -> R ) -> R {
130131 ui. scope ( |ui| {
@@ -407,6 +408,9 @@ pub struct LauncherApp {
407408 actions_by_id : HashMap < String , Action > ,
408409 command_cache : Vec < Action > ,
409410 completion_index : Option < Map < Vec < u8 > > > ,
411+ action_completion_dirty : bool ,
412+ command_completion_dirty : bool ,
413+ completion_rebuild_after : Option < Instant > ,
410414 suggestions : Vec < String > ,
411415 autocomplete_index : usize ,
412416 pub query : String ,
@@ -618,7 +622,8 @@ impl LauncherApp {
618622 . iter ( )
619623 . map ( |a| ( a. action . clone ( ) , a. clone ( ) ) )
620624 . collect ( ) ;
621- self . update_completion_index ( ) ;
625+ self . action_completion_dirty = true ;
626+ self . schedule_completion_rebuild ( ) ;
622627 }
623628
624629 pub fn update_command_cache ( & mut self ) {
@@ -627,7 +632,37 @@ impl LauncherApp {
627632 . commands_filtered ( self . enabled_plugins . as_ref ( ) ) ;
628633 cmds. sort_by_cached_key ( |a| a. label . to_lowercase ( ) ) ;
629634 self . command_cache = cmds;
630- self . update_completion_index ( ) ;
635+ self . command_completion_dirty = true ;
636+ self . schedule_completion_rebuild ( ) ;
637+ }
638+
639+ fn schedule_completion_rebuild ( & mut self ) {
640+ self . completion_rebuild_after = Some ( Instant :: now ( ) + COMPLETION_REBUILD_DEBOUNCE ) ;
641+ self . completion_index = None ;
642+ self . autocomplete_index = 0 ;
643+ self . suggestions . clear ( ) ;
644+ }
645+
646+ fn maybe_rebuild_completion_index ( & mut self , now : Instant ) {
647+ let should_rebuild = self
648+ . completion_rebuild_after
649+ . is_some_and ( |scheduled| now >= scheduled)
650+ && ( self . action_completion_dirty || self . command_completion_dirty ) ;
651+ if should_rebuild {
652+ self . update_completion_index ( ) ;
653+ self . action_completion_dirty = false ;
654+ self . command_completion_dirty = false ;
655+ self . completion_rebuild_after = None ;
656+ }
657+ }
658+
659+ fn rebuild_completion_index_now ( & mut self ) {
660+ if self . action_completion_dirty || self . command_completion_dirty {
661+ self . update_completion_index ( ) ;
662+ self . action_completion_dirty = false ;
663+ self . command_completion_dirty = false ;
664+ }
665+ self . completion_rebuild_after = None ;
631666 }
632667
633668 pub fn process_watch_events ( & mut self ) {
@@ -735,6 +770,7 @@ impl LauncherApp {
735770 }
736771 }
737772 }
773+ self . maybe_rebuild_completion_index ( Instant :: now ( ) ) ;
738774 }
739775
740776 fn update_completion_index ( & mut self ) {
@@ -1440,6 +1476,9 @@ impl LauncherApp {
14401476 actions_by_id,
14411477 command_cache : Vec :: new ( ) ,
14421478 completion_index : None ,
1479+ action_completion_dirty : false ,
1480+ command_completion_dirty : false ,
1481+ completion_rebuild_after : None ,
14431482 suggestions : Vec :: new ( ) ,
14441483 autocomplete_index : 0 ,
14451484 vim_mode : false ,
@@ -1476,6 +1515,7 @@ impl LauncherApp {
14761515
14771516 app. update_action_cache ( ) ;
14781517 app. update_command_cache ( ) ;
1518+ app. rebuild_completion_index_now ( ) ;
14791519 app. search ( ) ;
14801520 crate :: plugins:: mouse_gestures:: sync_enabled_plugins ( app. enabled_plugins . as_ref ( ) ) ;
14811521 app
@@ -5170,6 +5210,107 @@ mod tests {
51705210 std:: env:: set_current_dir ( original_dir) . unwrap ( ) ;
51715211 }
51725212
5213+ #[ test]
5214+ fn watch_event_bursts_delay_completion_rebuild_until_debounce_window ( ) {
5215+ let _lock = TEST_MUTEX . lock ( ) . unwrap ( ) ;
5216+ let dir = tempdir ( ) . unwrap ( ) ;
5217+ let original_dir = std:: env:: current_dir ( ) . unwrap ( ) ;
5218+ std:: env:: set_current_dir ( dir. path ( ) ) . unwrap ( ) ;
5219+
5220+ std:: fs:: write (
5221+ "actions.json" ,
5222+ serde_json:: to_string_pretty ( & serde_json:: json!( [
5223+ {
5224+ "label" : "Initial App" ,
5225+ "desc" : "demo" ,
5226+ "action" : "initial:app" ,
5227+ "args" : null
5228+ }
5229+ ] ) )
5230+ . unwrap ( ) ,
5231+ )
5232+ . unwrap ( ) ;
5233+
5234+ let ctx = egui:: Context :: default ( ) ;
5235+ let mut app = new_app ( & ctx) ;
5236+ app. rebuild_completion_index_now ( ) ;
5237+ assert ! ( app. completion_index. is_some( ) ) ;
5238+
5239+ std:: fs:: write (
5240+ "actions.json" ,
5241+ serde_json:: to_string_pretty ( & serde_json:: json!( [
5242+ {
5243+ "label" : "Updated App" ,
5244+ "desc" : "demo" ,
5245+ "action" : "updated:app" ,
5246+ "args" : null
5247+ }
5248+ ] ) )
5249+ . unwrap ( ) ,
5250+ )
5251+ . unwrap ( ) ;
5252+
5253+ send_event ( WatchEvent :: Actions ) ;
5254+ send_event ( WatchEvent :: Actions ) ;
5255+ app. process_watch_events ( ) ;
5256+
5257+ assert ! ( app. completion_index. is_none( ) ) ;
5258+ assert ! ( app. action_completion_dirty) ;
5259+
5260+ let scheduled = app
5261+ . completion_rebuild_after
5262+ . expect ( "rebuild should be scheduled" ) ;
5263+ app. maybe_rebuild_completion_index ( scheduled - Duration :: from_millis ( 1 ) ) ;
5264+ assert ! ( app. completion_index. is_none( ) ) ;
5265+
5266+ app. maybe_rebuild_completion_index ( scheduled + Duration :: from_millis ( 1 ) ) ;
5267+ assert ! ( app. completion_index. is_some( ) ) ;
5268+ assert ! ( !app. action_completion_dirty) ;
5269+ assert ! ( !app. command_completion_dirty) ;
5270+ assert ! ( app. completion_rebuild_after. is_none( ) ) ;
5271+
5272+ std:: env:: set_current_dir ( original_dir) . unwrap ( ) ;
5273+ }
5274+
5275+ #[ test]
5276+ fn completion_suggestions_clear_until_rebuild_and_match_latest_entries ( ) {
5277+ let _lock = TEST_MUTEX . lock ( ) . unwrap ( ) ;
5278+ let ctx = egui:: Context :: default ( ) ;
5279+ let mut app = new_app ( & ctx) ;
5280+ app. query_autocomplete = true ;
5281+
5282+ app. actions = Arc :: new ( vec ! [ Action {
5283+ label: "Old App" . into( ) ,
5284+ desc: "demo" . into( ) ,
5285+ action: "old:app" . into( ) ,
5286+ args: None ,
5287+ } ] ) ;
5288+ app. update_action_cache ( ) ;
5289+ app. rebuild_completion_index_now ( ) ;
5290+
5291+ app. query = "app " . into ( ) ;
5292+ app. update_suggestions ( ) ;
5293+ assert ! ( app. suggestions. iter( ) . any( |s| s == "app old app" ) ) ;
5294+
5295+ app. actions = Arc :: new ( vec ! [ Action {
5296+ label: "New App" . into( ) ,
5297+ desc: "demo" . into( ) ,
5298+ action: "new:app" . into( ) ,
5299+ args: None ,
5300+ } ] ) ;
5301+ app. update_action_cache ( ) ;
5302+
5303+ assert ! ( app. completion_index. is_none( ) ) ;
5304+ assert ! ( app. suggestions. is_empty( ) ) ;
5305+
5306+ app. maybe_rebuild_completion_index (
5307+ Instant :: now ( ) + COMPLETION_REBUILD_DEBOUNCE + Duration :: from_millis ( 1 ) ,
5308+ ) ;
5309+
5310+ assert ! ( app. suggestions. iter( ) . all( |s| s != "app old app" ) ) ;
5311+ assert ! ( app. suggestions. iter( ) . any( |s| s == "app new app" ) ) ;
5312+ }
5313+
51735314 #[ test]
51745315 fn open_note_panel_reuses_existing_panel_for_same_slug ( ) {
51755316 let _lock = TEST_MUTEX . lock ( ) . unwrap ( ) ;
0 commit comments