11use std:: {
2+ cell:: LazyCell ,
3+ collections:: BTreeSet ,
24 io:: { BufRead , BufReader } ,
35 ops:: Range ,
4- path:: Path ,
6+ path:: { Path , PathBuf } ,
57 pin:: pin,
68 sync:: Arc ,
79} ;
@@ -22,7 +24,7 @@ use smol::{
2224
2325use text:: BufferId ;
2426use util:: { ResultExt , maybe, paths:: compare_rel_paths} ;
25- use worktree:: { Entry , ProjectEntryId , Snapshot , Worktree } ;
27+ use worktree:: { Entry , ProjectEntryId , Snapshot , Worktree , WorktreeSettings } ;
2628
2729use crate :: {
2830 Project , ProjectItem , ProjectPath , RemotelyCreatedModels ,
@@ -178,7 +180,7 @@ impl Search {
178180
179181 let ( find_all_matches_tx, find_all_matches_rx) =
180182 bounded ( MAX_CONCURRENT_BUFFER_OPENS ) ;
181-
183+ let query = Arc :: new ( query ) ;
182184 let ( candidate_searcher, tasks) = match self . kind {
183185 SearchKind :: OpenBuffersOnly => {
184186 let Ok ( open_buffers) = cx. update ( |cx| self . all_loaded_buffers ( & query, cx) )
@@ -207,11 +209,12 @@ impl Search {
207209 let ( sorted_search_results_tx, sorted_search_results_rx) = unbounded ( ) ;
208210
209211 let ( input_paths_tx, input_paths_rx) = unbounded ( ) ;
210-
212+ // glob: src/rust/
213+ // path: src/
211214 let tasks = vec ! [
212215 cx. spawn( Self :: provide_search_paths(
213216 std:: mem:: take( worktrees) ,
214- query. include_ignored ( ) ,
217+ query. clone ( ) ,
215218 input_paths_tx,
216219 sorted_search_results_tx,
217220 ) )
@@ -366,26 +369,30 @@ impl Search {
366369
367370 fn provide_search_paths (
368371 worktrees : Vec < Entity < Worktree > > ,
369- include_ignored : bool ,
372+ query : Arc < SearchQuery > ,
370373 tx : Sender < InputPath > ,
371374 results : Sender < oneshot:: Receiver < ProjectPath > > ,
372375 ) -> impl AsyncFnOnce ( & mut AsyncApp ) {
373376 async move |cx| {
374377 _ = maybe ! ( async move {
378+ let gitignored_tracker = PathInclusionMatcher :: new( query. clone( ) ) ;
375379 for worktree in worktrees {
376380 let ( mut snapshot, worktree_settings) = worktree
377381 . read_with( cx, |this, _| {
378382 Some ( ( this. snapshot( ) , this. as_local( ) ?. settings( ) ) )
379383 } ) ?
380384 . context( "The worktree is not local" ) ?;
381- if include_ignored {
385+ if query . include_ignored( ) {
382386 // Pre-fetch all of the ignored directories as they're going to be searched.
383387 let mut entries_to_refresh = vec![ ] ;
384- for entry in snapshot. entries( include_ignored, 0 ) {
385- if entry. is_ignored && entry. kind. is_unloaded( ) {
386- if !worktree_settings. is_path_excluded( & entry. path) {
387- entries_to_refresh. push( entry. path. clone( ) ) ;
388- }
388+
389+ for entry in snapshot. entries( query. include_ignored( ) , 0 ) {
390+ if gitignored_tracker. should_scan_gitignored_dir(
391+ entry,
392+ & snapshot,
393+ & worktree_settings,
394+ ) {
395+ entries_to_refresh. push( entry. path. clone( ) ) ;
389396 }
390397 }
391398 let barrier = worktree. update( cx, |this, _| {
@@ -404,8 +411,9 @@ impl Search {
404411 cx. background_executor( )
405412 . scoped( |scope| {
406413 scope. spawn( async {
407- for entry in snapshot. files( include_ignored, 0 ) {
414+ for entry in snapshot. files( query . include_ignored( ) , 0 ) {
408415 let ( should_scan_tx, should_scan_rx) = oneshot:: channel( ) ;
416+
409417 let Ok ( _) = tx
410418 . send( InputPath {
411419 entry: entry. clone( ) ,
@@ -788,3 +796,78 @@ struct MatchingEntry {
788796 path : ProjectPath ,
789797 should_scan_tx : oneshot:: Sender < ProjectPath > ,
790798}
799+
800+ /// This struct encapsulates the logic to decide whether a given gitignored directory should be
801+ /// scanned based on include/exclude patterns of a search query (as include/exclude parameters may match paths inside it).
802+ /// It is kind-of doing an inverse of glob. Given a glob pattern like `src/**/` and a parent path like `src`, we need to decide whether the parent
803+ /// may contain glob hits.
804+ struct PathInclusionMatcher {
805+ included : BTreeSet < PathBuf > ,
806+ excluded : BTreeSet < PathBuf > ,
807+ query : Arc < SearchQuery > ,
808+ }
809+
810+ impl PathInclusionMatcher {
811+ fn new ( query : Arc < SearchQuery > ) -> Self {
812+ let mut included = BTreeSet :: new ( ) ;
813+ let mut excluded = BTreeSet :: new ( ) ;
814+ if query. filters_path ( ) {
815+ included. extend (
816+ query
817+ . files_to_include ( )
818+ . sources ( )
819+ . iter ( )
820+ . filter_map ( |glob| Some ( wax:: Glob :: new ( glob) . ok ( ) ?. partition ( ) . 0 ) ) ,
821+ ) ;
822+ excluded. extend (
823+ query
824+ . files_to_exclude ( )
825+ . sources ( )
826+ . iter ( )
827+ . filter_map ( |glob| Some ( wax:: Glob :: new ( glob) . ok ( ) ?. partition ( ) . 0 ) ) ,
828+ ) ;
829+ }
830+ Self {
831+ included,
832+ excluded,
833+ query,
834+ }
835+ }
836+
837+ fn should_scan_gitignored_dir (
838+ & self ,
839+ entry : & Entry ,
840+ snapshot : & Snapshot ,
841+ worktree_settings : & WorktreeSettings ,
842+ ) -> bool {
843+ if !entry. is_ignored || !entry. kind . is_unloaded ( ) {
844+ return false ;
845+ }
846+ if !self . query . include_ignored ( ) {
847+ return false ;
848+ }
849+ if worktree_settings. is_path_excluded ( & entry. path ) {
850+ return false ;
851+ }
852+
853+ let as_abs_path = LazyCell :: new ( move || snapshot. absolutize ( & entry. path ) ) ;
854+ let descendant_might_match_glob = |prefix : & Path | {
855+ if prefix. is_absolute ( ) {
856+ as_abs_path. starts_with ( prefix)
857+ } else {
858+ entry. path . as_std_path ( ) . starts_with ( prefix)
859+ }
860+ } ;
861+ let matched_path = !self
862+ . excluded
863+ . iter ( )
864+ . any ( |prefix| descendant_might_match_glob ( prefix) )
865+ && ( self . included . is_empty ( )
866+ || self
867+ . included
868+ . iter ( )
869+ . any ( |prefix| descendant_might_match_glob ( prefix) ) ) ;
870+
871+ matched_path
872+ }
873+ }
0 commit comments