Skip to content

Commit 6915601

Browse files
osiewiczdinocosta
andcommitted
project search: Skip loading of gitignored paths when their descendants
will never match an inclusion/exclusion query. Co-authored-by: dino <dinojoaocosta@gmail.com>
1 parent 5225a84 commit 6915601

File tree

4 files changed

+143
-13
lines changed

4 files changed

+143
-13
lines changed

Cargo.lock

Lines changed: 45 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,7 @@ wasmtime = { version = "29", default-features = false, features = [
721721
"parallel-compilation",
722722
] }
723723
wasmtime-wasi = "29"
724+
wax = "0.6"
724725
which = "6.0.0"
725726
windows-core = "0.61"
726727
wit-component = "0.221"

crates/project/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ toml.workspace = true
8686
url.workspace = true
8787
util.workspace = true
8888
watch.workspace = true
89+
wax.workspace = true
8990
which.workspace = true
9091
worktree.workspace = true
9192
zeroize.workspace = true

crates/project/src/project_search.rs

Lines changed: 96 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use 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

2325
use text::BufferId;
2426
use util::{ResultExt, maybe, paths::compare_rel_paths};
25-
use worktree::{Entry, ProjectEntryId, Snapshot, Worktree};
27+
use worktree::{Entry, ProjectEntryId, Snapshot, Worktree, WorktreeSettings};
2628

2729
use 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

Comments
 (0)