file_finder: Respect .gitignore and file_scan_inclusions with ** in glob (#40654)
Closes #39037 Previously, the code split the `**/.env` glob in `file_scan_inclusions` into two sources for the `PathMatcher`: `["**", "**/.env"]`. This approach works for directories, but including `**` will match all directories and their files. To address this, I now select the appropriate `PathMatcher` using only `**/.env` when specifically targeting a file to determine whether to include it in the file finder. Release Notes: - Fixed: respect `.gitignore` and `file_scan_inclusions` settings with `**` in glob for file finder --------- Signed-off-by: Benjamin <5719034+bnjjj@users.noreply.github.com> Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
This commit is contained in:
co-authored by
Julia Ryan
parent
cc6208b17f
commit
a827f25d00
@@ -7,6 +7,7 @@ use menu::{Confirm, SelectNext, SelectPrevious};
|
||||
use pretty_assertions::{assert_eq, assert_matches};
|
||||
use project::{FS_WATCH_LATENCY, RemoveOptions};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::{path, rel_path::rel_path};
|
||||
use workspace::{AppState, CloseActiveItem, OpenOptions, ToggleFileFinder, Workspace, open_paths};
|
||||
|
||||
@@ -658,6 +659,147 @@ async fn test_matching_cancellation(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ignored_root_with_file_inclusions(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.worktree.file_scan_inclusions = Some(vec![
|
||||
"height_demo/**/hi_bonjour".to_string(),
|
||||
"**/height_1".to_string(),
|
||||
]);
|
||||
});
|
||||
})
|
||||
});
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/ancestor",
|
||||
json!({
|
||||
".gitignore": "ignored-root",
|
||||
"ignored-root": {
|
||||
"happiness": "",
|
||||
"height": "",
|
||||
"hi": "",
|
||||
"hiccup": "",
|
||||
},
|
||||
"tracked-root": {
|
||||
".gitignore": "height*",
|
||||
"happiness": "",
|
||||
"height": "",
|
||||
"heights": {
|
||||
"height_1": "",
|
||||
"height_2": "",
|
||||
},
|
||||
"height_demo": {
|
||||
"test_1": {
|
||||
"hi_bonjour": "hi_bonjour",
|
||||
"hi": "hello",
|
||||
},
|
||||
"hihi": "bye",
|
||||
"test_2": {
|
||||
"hoi": "nl"
|
||||
}
|
||||
},
|
||||
"height_include": {
|
||||
"height_1_include": "",
|
||||
"height_2_include": "",
|
||||
},
|
||||
"hi": "",
|
||||
"hiccup": "",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(
|
||||
app_state.fs.clone(),
|
||||
[
|
||||
Path::new(path!("/ancestor/tracked-root")),
|
||||
Path::new(path!("/ancestor/ignored-root")),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let (picker, _workspace, cx) = build_find_picker(project, cx);
|
||||
|
||||
picker
|
||||
.update_in(cx, |picker, window, cx| {
|
||||
picker
|
||||
.delegate
|
||||
.spawn_search(test_path_position("hi"), window, cx)
|
||||
})
|
||||
.await;
|
||||
picker.update(cx, |picker, _| {
|
||||
let matches = collect_search_matches(picker);
|
||||
assert_eq!(matches.history.len(), 0);
|
||||
assert_eq!(
|
||||
matches.search,
|
||||
vec![
|
||||
rel_path("ignored-root/hi").into(),
|
||||
rel_path("tracked-root/hi").into(),
|
||||
rel_path("ignored-root/hiccup").into(),
|
||||
rel_path("tracked-root/hiccup").into(),
|
||||
rel_path("tracked-root/height_demo/test_1/hi_bonjour").into(),
|
||||
rel_path("ignored-root/height").into(),
|
||||
rel_path("tracked-root/heights/height_1").into(),
|
||||
rel_path("ignored-root/happiness").into(),
|
||||
rel_path("tracked-root/happiness").into(),
|
||||
],
|
||||
"All ignored files that were indexed are found for default ignored mode"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ignored_root_with_file_inclusions_repro(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.worktree.file_scan_inclusions = Some(vec!["**/.env".to_string()]);
|
||||
});
|
||||
})
|
||||
});
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/src",
|
||||
json!({
|
||||
".gitignore": "node_modules",
|
||||
"node_modules": {
|
||||
"package.json": "// package.json",
|
||||
".env": "BAR=FOO"
|
||||
},
|
||||
".env": "FOO=BAR"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [Path::new(path!("/src"))], cx).await;
|
||||
let (picker, _workspace, cx) = build_find_picker(project, cx);
|
||||
|
||||
picker
|
||||
.update_in(cx, |picker, window, cx| {
|
||||
picker
|
||||
.delegate
|
||||
.spawn_search(test_path_position("json"), window, cx)
|
||||
})
|
||||
.await;
|
||||
picker.update(cx, |picker, _| {
|
||||
let matches = collect_search_matches(picker);
|
||||
assert_eq!(matches.history.len(), 0);
|
||||
assert_eq!(
|
||||
matches.search,
|
||||
vec![],
|
||||
"All ignored files that were indexed are found for default ignored mode"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ignored_root(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
@@ -1573,6 +1573,21 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
fn file_in_dirs() {
|
||||
let path = Path::new("/work/.env");
|
||||
let path_matcher = PathMatcher::new(&["**/.env".to_owned()], PathStyle::Posix).unwrap();
|
||||
assert!(
|
||||
path_matcher.is_match(path),
|
||||
"Path matcher should match {path:?}"
|
||||
);
|
||||
let path = Path::new("/work/package.json");
|
||||
assert!(
|
||||
!path_matcher.is_match(path),
|
||||
"Path matcher should not match {path:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[perf]
|
||||
fn project_search() {
|
||||
let path = Path::new("/Users/someonetoignore/work/zed/zed.dev/node_modules");
|
||||
|
||||
@@ -505,7 +505,7 @@ impl Worktree {
|
||||
project_id,
|
||||
replica_id,
|
||||
snapshot,
|
||||
file_scan_inclusions: settings.file_scan_inclusions.clone(),
|
||||
file_scan_inclusions: settings.parent_dir_scan_inclusions.clone(),
|
||||
background_snapshot: background_snapshot.clone(),
|
||||
updates_tx: Some(background_updates_tx),
|
||||
update_observer: None,
|
||||
@@ -520,8 +520,10 @@ impl Worktree {
|
||||
while let Some(update) = background_updates_rx.next().await {
|
||||
{
|
||||
let mut lock = background_snapshot.lock();
|
||||
lock.0
|
||||
.apply_remote_update(update.clone(), &settings.file_scan_inclusions);
|
||||
lock.0.apply_remote_update(
|
||||
update.clone(),
|
||||
&settings.parent_dir_scan_inclusions,
|
||||
);
|
||||
lock.1.push(update);
|
||||
}
|
||||
snapshot_updated_tx.send(()).await.ok();
|
||||
@@ -4290,7 +4292,8 @@ impl BackgroundScanner {
|
||||
|
||||
if child_entry.is_dir() {
|
||||
child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true);
|
||||
child_entry.is_always_included = self.settings.is_path_always_included(&child_path);
|
||||
child_entry.is_always_included =
|
||||
self.settings.is_path_always_included(&child_path, true);
|
||||
|
||||
// Avoid recursing until crash in the case of a recursive symlink
|
||||
if job.ancestor_inodes.contains(&child_entry.inode) {
|
||||
@@ -4315,7 +4318,8 @@ impl BackgroundScanner {
|
||||
}
|
||||
} else {
|
||||
child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false);
|
||||
child_entry.is_always_included = self.settings.is_path_always_included(&child_path);
|
||||
child_entry.is_always_included =
|
||||
self.settings.is_path_always_included(&child_path, false);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -4450,7 +4454,8 @@ impl BackgroundScanner {
|
||||
fs_entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, is_dir);
|
||||
fs_entry.is_external = is_external;
|
||||
fs_entry.is_private = self.is_path_private(path);
|
||||
fs_entry.is_always_included = self.settings.is_path_always_included(path);
|
||||
fs_entry.is_always_included =
|
||||
self.settings.is_path_always_included(path, is_dir);
|
||||
|
||||
let parent_is_hidden = path
|
||||
.parent()
|
||||
|
||||
@@ -13,8 +13,11 @@ pub struct WorktreeSettings {
|
||||
pub project_name: Option<String>,
|
||||
/// Whether to prevent this project from being shared in public channels.
|
||||
pub prevent_sharing_in_public_channels: bool,
|
||||
pub file_scan_inclusions: PathMatcher,
|
||||
pub file_scan_exclusions: PathMatcher,
|
||||
pub file_scan_inclusions: PathMatcher,
|
||||
/// This field contains all ancestors of the `file_scan_inclusions`. It's used to
|
||||
/// determine whether to terminate worktree scanning for a given dir.
|
||||
pub parent_dir_scan_inclusions: PathMatcher,
|
||||
pub private_files: PathMatcher,
|
||||
}
|
||||
|
||||
@@ -29,9 +32,12 @@ impl WorktreeSettings {
|
||||
.any(|ancestor| self.file_scan_exclusions.is_match(ancestor.as_std_path()))
|
||||
}
|
||||
|
||||
pub fn is_path_always_included(&self, path: &RelPath) -> bool {
|
||||
path.ancestors()
|
||||
.any(|ancestor| self.file_scan_inclusions.is_match(ancestor.as_std_path()))
|
||||
pub fn is_path_always_included(&self, path: &RelPath, is_dir: bool) -> bool {
|
||||
if is_dir {
|
||||
self.parent_dir_scan_inclusions.is_match(path.as_std_path())
|
||||
} else {
|
||||
self.file_scan_inclusions.is_match(path.as_std_path())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +52,7 @@ impl Settings for WorktreeSettings {
|
||||
.flat_map(|glob| {
|
||||
Path::new(glob)
|
||||
.ancestors()
|
||||
.skip(1)
|
||||
.map(|a| a.to_string_lossy().into())
|
||||
})
|
||||
.filter(|p: &String| !p.is_empty())
|
||||
@@ -57,11 +64,13 @@ impl Settings for WorktreeSettings {
|
||||
file_scan_exclusions: path_matchers(file_scan_exclusions, "file_scan_exclusions")
|
||||
.log_err()
|
||||
.unwrap_or_default(),
|
||||
file_scan_inclusions: path_matchers(
|
||||
parent_dir_scan_inclusions: path_matchers(
|
||||
parsed_file_scan_inclusions,
|
||||
"file_scan_inclusions",
|
||||
)
|
||||
.unwrap(),
|
||||
file_scan_inclusions: path_matchers(file_scan_inclusions, "file_scan_inclusions")
|
||||
.unwrap(),
|
||||
private_files: path_matchers(private_files, "private_files")
|
||||
.log_err()
|
||||
.unwrap_or_default(),
|
||||
|
||||
@@ -760,6 +760,7 @@ async fn test_file_scan_inclusions(cx: &mut TestAppContext) {
|
||||
"prettier": {
|
||||
"package.json": "{}",
|
||||
},
|
||||
"package.json": "//package.json"
|
||||
},
|
||||
"src": {
|
||||
".DS_Store": "",
|
||||
|
||||
Reference in New Issue
Block a user