@@ -579,6 +579,13 @@ fn handle_debounced_events(
579579
580580 files_to_update_git_status. reserve ( paths_to_add_or_modify. len ( ) ) ;
581581 for path in & paths_to_add_or_modify {
582+ // Double-check existence: the debouncer may deliver a Modify event
583+ // for a path that was deleted between event emission and processing
584+ // (Create+Remove merged into just Create by the debouncer).
585+ if !path. exists ( ) {
586+ picker. remove_file_by_path ( path) ;
587+ continue ;
588+ }
582589 if picker. handle_create_or_modify ( path) . is_some ( ) {
583590 files_to_update_git_status. push ( path. to_path_buf ( ) ) ;
584591 } else {
@@ -587,13 +594,40 @@ fn handle_debounced_events(
587594 }
588595
589596 overflow_count = picker. get_overflow_files ( ) . len ( ) ;
597+
598+ // Reconcile overflow files: stat-check each live overflow file and
599+ // tombstone any that no longer exist on disk. This handles deletions
600+ // where the debouncer swallowed the Remove event.
601+ if overflow_count > 0 {
602+ let base_path = picker. base_path ( ) . to_path_buf ( ) ;
603+ let stale: Vec < PathBuf > = picker
604+ . get_overflow_files ( )
605+ . iter ( )
606+ . filter ( |f| !f. is_deleted ( ) )
607+ . filter_map ( |f| {
608+ let abs = f. absolute_path ( & * picker, & base_path) ;
609+ ( !abs. exists ( ) ) . then_some ( abs)
610+ } )
611+ . collect ( ) ;
612+
613+ for path in & stale {
614+ picker. remove_file_by_path ( path) ;
615+ }
616+ if !stale. is_empty ( ) {
617+ overflow_count = picker. get_overflow_files ( ) . len ( ) ;
618+ }
619+ }
590620 }
591621
592622 info ! (
593623 files_updated = files_to_update_git_status. len( ) ,
594624 overflow_count, "File index changes applied" ,
595625 ) ;
596626
627+ // Reconcile overflow files unconditionally on every batch: stat-check
628+ // each live overflow file and tombstone any that no longer exist on disk.
629+ // This is the only reliable way to detect deletions when the debouncer
630+ // coalesces Create+Remove for the same path into nothing.
597631 if need_full_rescan || overflow_count > MAX_OVERFLOW_FILES {
598632 info ! ( "Watcher faced limit of index overflow. Triggering rescan" ) ;
599633 if let Err ( e) = shared_picker. trigger_full_rescan_async ( shared_frecency) {
@@ -645,46 +679,57 @@ fn handle_debounced_events(
645679 }
646680 }
647681
648- // Git status updates require a repository.
649- let Some ( repo) = repo. as_ref ( ) else {
650- debug ! ( "No git repo available, skipping git status updates" ) ;
651- return new_dirs_to_watch;
652- } ;
653-
654- if need_full_git_rescan && !need_full_rescan {
655- info ! ( "Triggering full git rescan" ) ;
656-
657- if let Err ( e) = shared_picker. refresh_git_status ( shared_frecency) {
658- error ! ( "Failed to refresh git status: {:?}" , e) ;
659- }
660- }
661-
662- // do not update the git status if the
663- if !files_to_update_git_status. is_empty ( ) && !need_full_git_rescan {
664- info ! (
665- "Fetching git status for {} files" ,
666- files_to_update_git_status. len( )
667- ) ;
682+ // Git status updates are IO-heavy (reads .git/index, stats files) and
683+ // not on the critical path for search correctness. Spawn them on the
684+ // background pool so the debouncer handler returns immediately and can
685+ // process the next event batch without waiting for git.
686+ if need_full_git_rescan || !files_to_update_git_status. is_empty ( ) {
687+ let sp = shared_picker. clone ( ) ;
688+ let sf = shared_frecency. clone ( ) ;
689+ let git_workdir = repo. as_ref ( ) . map ( |r| {
690+ r. workdir ( )
691+ . unwrap_or_else ( || r. path ( ) )
692+ . to_path_buf ( )
693+ } ) ;
694+ let full_rescan = need_full_git_rescan;
695+ let need_picker_rescan = need_full_rescan;
696+ let files = files_to_update_git_status;
697+
698+ crate :: file_picker:: BACKGROUND_THREAD_POOL . spawn ( move || {
699+ let Some ( git_path) = git_workdir else { return } ;
700+ let Ok ( repo) = Repository :: open ( & git_path) else {
701+ error ! ( "Failed to open git repo for async status update" ) ;
702+ return ;
703+ } ;
668704
669- let status = match GitStatusCache :: git_status_for_paths ( repo , & files_to_update_git_status ) {
670- Ok ( status ) => status ,
671- Err ( e) => {
672- tracing :: error!( ?e , "Failed to query git status" ) ;
673- return new_dirs_to_watch ;
705+ if full_rescan && !need_picker_rescan {
706+ info ! ( "Async: triggering full git rescan" ) ;
707+ if let Err ( e) = sp . refresh_git_status ( & sf ) {
708+ error ! ( "Failed to refresh git status: {:?}" , e ) ;
709+ }
674710 }
675- } ;
676711
677- if let Ok ( mut guard) = shared_picker. write ( )
678- && let Some ( ref mut picker) = * guard
679- {
680- if let Err ( e) = picker. update_git_statuses ( status, shared_frecency) {
681- error ! ( "Failed to update git statuses: {:?}" , e) ;
682- } else {
683- info ! ( "Successfully updated git statuses in picker" ) ;
712+ if !files. is_empty ( ) && !full_rescan {
713+ info ! ( "Async: fetching git status for {} files" , files. len( ) ) ;
714+ let status = match GitStatusCache :: git_status_for_paths ( & repo, & files) {
715+ Ok ( s) => s,
716+ Err ( e) => {
717+ error ! ( "Failed to query git status: {:?}" , e) ;
718+ return ;
719+ }
720+ } ;
721+
722+ if let Ok ( mut guard) = sp. write ( )
723+ && let Some ( ref mut picker) = * guard
724+ {
725+ if let Err ( e) = picker. update_git_statuses ( status, & sf) {
726+ error ! ( "Failed to update git statuses: {:?}" , e) ;
727+ } else {
728+ info ! ( "Async: git statuses updated" ) ;
729+ }
730+ }
684731 }
685- } else {
686- error ! ( "Failed to acquire picker lock for git status update" ) ;
687- }
732+ } ) ;
688733 }
689734
690735 new_dirs_to_watch
0 commit comments