Skip to content

Commit

Permalink
Add file_scan_inclusions setting to customize Zed file indexing (ze…
Browse files Browse the repository at this point in the history
…d-industries#16852)

Closes zed-industries#4745

Release Notes:

- Added a new `file_scan_inclusions` setting to force Zed to index files
that match the provided globs, even if they're gitignored.

---------

Co-authored-by: Mikayla Maki <[email protected]>
  • Loading branch information
2 people authored and Anthony-Eid committed Nov 22, 2024
1 parent 9fa37d5 commit 6314401
Show file tree
Hide file tree
Showing 6 changed files with 350 additions and 25 deletions.
10 changes: 9 additions & 1 deletion assets/settings/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@
},
// Add files or globs of files that will be excluded by Zed entirely:
// they will be skipped during FS scan(s), file tree and file search
// will lack the corresponding file entries.
// will lack the corresponding file entries. Overrides `file_scan_inclusions`.
"file_scan_exclusions": [
"**/.git",
"**/.svn",
Expand All @@ -679,6 +679,14 @@
"**/.classpath",
"**/.settings"
],
// Add files or globs of files that will be included by Zed, even when
// ignored by git. This is useful for files that are not tracked by git,
// but are still important to your project. Note that globs that are
// overly broad can slow down Zed's file scanning. Overridden by `file_scan_exclusions`.
"file_scan_inclusions": [
".env*",
"docker-compose.*.yml"
],
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
Expand Down
1 change: 1 addition & 0 deletions crates/project_panel/src/project_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2252,6 +2252,7 @@ impl ProjectPanel {
is_ignored: entry.is_ignored,
is_external: false,
is_private: false,
is_always_included: entry.is_always_included,
git_status: entry.git_status,
canonical_path: entry.canonical_path.clone(),
char_bag: entry.char_bag,
Expand Down
2 changes: 1 addition & 1 deletion crates/worktree/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ log.workspace = true
parking_lot.workspace = true
paths.workspace = true
postage.workspace = true
rpc.workspace = true
rpc = { workspace = true, features = ["gpui"] }
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
Expand Down
85 changes: 70 additions & 15 deletions crates/worktree/src/worktree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ use std::{
};
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
use text::{LineEnding, Rope};
use util::{paths::home_dir, ResultExt};
use util::{
paths::{home_dir, PathMatcher},
ResultExt,
};
pub use worktree_settings::WorktreeSettings;

#[cfg(feature = "test-support")]
Expand Down Expand Up @@ -134,6 +137,7 @@ pub struct RemoteWorktree {
background_snapshot: Arc<Mutex<(Snapshot, Vec<proto::UpdateWorktree>)>>,
project_id: u64,
client: AnyProtoClient,
file_scan_inclusions: PathMatcher,
updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>,
update_observer: Option<mpsc::UnboundedSender<proto::UpdateWorktree>>,
snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>,
Expand All @@ -150,6 +154,7 @@ pub struct Snapshot {
root_char_bag: CharBag,
entries_by_path: SumTree<Entry>,
entries_by_id: SumTree<PathEntry>,
always_included_entries: Vec<Arc<Path>>,
repository_entries: TreeMap<RepositoryWorkDirectory, RepositoryEntry>,

/// A number that increases every time the worktree begins scanning
Expand Down Expand Up @@ -433,7 +438,7 @@ impl Worktree {
cx.observe_global::<SettingsStore>(move |this, cx| {
if let Self::Local(this) = this {
let settings = WorktreeSettings::get(settings_location, cx).clone();
if settings != this.settings {
if this.settings != settings {
this.settings = settings;
this.restart_background_scanners(cx);
}
Expand Down Expand Up @@ -480,11 +485,19 @@ impl Worktree {
let (background_updates_tx, mut background_updates_rx) = mpsc::unbounded();
let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel();

let worktree_id = snapshot.id();
let settings_location = Some(SettingsLocation {
worktree_id,
path: Path::new(EMPTY_PATH),
});

let settings = WorktreeSettings::get(settings_location, cx).clone();
let worktree = RemoteWorktree {
client,
project_id,
replica_id,
snapshot,
file_scan_inclusions: settings.file_scan_inclusions.clone(),
background_snapshot: background_snapshot.clone(),
updates_tx: Some(background_updates_tx),
update_observer: None,
Expand All @@ -500,7 +513,10 @@ impl Worktree {
while let Some(update) = background_updates_rx.next().await {
{
let mut lock = background_snapshot.lock();
if let Err(error) = lock.0.apply_remote_update(update.clone()) {
if let Err(error) = lock
.0
.apply_remote_update(update.clone(), &settings.file_scan_inclusions)
{
log::error!("error applying worktree update: {}", error);
}
lock.1.push(update);
Expand Down Expand Up @@ -1022,7 +1038,17 @@ impl LocalWorktree {
let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded();
self.scan_requests_tx = scan_requests_tx;
self.path_prefixes_to_scan_tx = path_prefixes_to_scan_tx;

self.start_background_scanner(scan_requests_rx, path_prefixes_to_scan_rx, cx);
let always_included_entries = mem::take(&mut self.snapshot.always_included_entries);
log::debug!(
"refreshing entries for the following always included paths: {:?}",
always_included_entries
);

// Cleans up old always included entries to ensure they get updated properly. Otherwise,
// nested always included entries may not get updated and will result in out-of-date info.
self.refresh_entries_for_paths(always_included_entries);
}

fn start_background_scanner(
Expand Down Expand Up @@ -1971,7 +1997,7 @@ impl RemoteWorktree {
this.update(&mut cx, |worktree, _| {
let worktree = worktree.as_remote_mut().unwrap();
let snapshot = &mut worktree.background_snapshot.lock().0;
let entry = snapshot.insert_entry(entry);
let entry = snapshot.insert_entry(entry, &worktree.file_scan_inclusions);
worktree.snapshot = snapshot.clone();
entry
})?
Expand Down Expand Up @@ -2052,6 +2078,7 @@ impl Snapshot {
abs_path,
root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
root_name,
always_included_entries: Default::default(),
entries_by_path: Default::default(),
entries_by_id: Default::default(),
repository_entries: Default::default(),
Expand Down Expand Up @@ -2115,8 +2142,12 @@ impl Snapshot {
self.entries_by_id.get(&entry_id, &()).is_some()
}

fn insert_entry(&mut self, entry: proto::Entry) -> Result<Entry> {
let entry = Entry::try_from((&self.root_char_bag, entry))?;
fn insert_entry(
&mut self,
entry: proto::Entry,
always_included_paths: &PathMatcher,
) -> Result<Entry> {
let entry = Entry::try_from((&self.root_char_bag, always_included_paths, entry))?;
let old_entry = self.entries_by_id.insert_or_replace(
PathEntry {
id: entry.id,
Expand Down Expand Up @@ -2170,7 +2201,11 @@ impl Snapshot {
}
}

pub(crate) fn apply_remote_update(&mut self, mut update: proto::UpdateWorktree) -> Result<()> {
pub(crate) fn apply_remote_update(
&mut self,
mut update: proto::UpdateWorktree,
always_included_paths: &PathMatcher,
) -> Result<()> {
log::trace!(
"applying remote worktree update. {} entries updated, {} removed",
update.updated_entries.len(),
Expand All @@ -2193,7 +2228,7 @@ impl Snapshot {
}

for entry in update.updated_entries {
let entry = Entry::try_from((&self.root_char_bag, entry))?;
let entry = Entry::try_from((&self.root_char_bag, always_included_paths, entry))?;
if let Some(PathEntry { path, .. }) = self.entries_by_id.get(&entry.id, &()) {
entries_by_path_edits.push(Edit::Remove(PathKey(path.clone())));
}
Expand Down Expand Up @@ -2713,7 +2748,7 @@ impl LocalSnapshot {
for entry in self.entries_by_path.cursor::<()>(&()) {
if entry.is_file() {
assert_eq!(files.next().unwrap().inode, entry.inode);
if !entry.is_ignored && !entry.is_external {
if (!entry.is_ignored && !entry.is_external) || entry.is_always_included {
assert_eq!(visible_files.next().unwrap().inode, entry.inode);
}
}
Expand Down Expand Up @@ -2796,7 +2831,7 @@ impl LocalSnapshot {

impl BackgroundScannerState {
fn should_scan_directory(&self, entry: &Entry) -> bool {
(!entry.is_external && !entry.is_ignored)
(!entry.is_external && (!entry.is_ignored || entry.is_always_included))
|| entry.path.file_name() == Some(*DOT_GIT)
|| entry.path.file_name() == Some(local_settings_folder_relative_path().as_os_str())
|| self.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning
Expand Down Expand Up @@ -3369,6 +3404,12 @@ pub struct Entry {
/// exclude them from searches.
pub is_ignored: bool,

/// Whether this entry is always included in searches.
///
/// This is used for entries that are always included in searches, even
/// if they are ignored by git. Overridden by file_scan_exclusions.
pub is_always_included: bool,

/// Whether this entry's canonical path is outside of the worktree.
/// This means the entry is only accessible from the worktree root via a
/// symlink.
Expand Down Expand Up @@ -3440,6 +3481,7 @@ impl Entry {
size: metadata.len,
canonical_path,
is_ignored: false,
is_always_included: false,
is_external: false,
is_private: false,
git_status: None,
Expand Down Expand Up @@ -3486,7 +3528,8 @@ impl sum_tree::Item for Entry {
type Summary = EntrySummary;

fn summary(&self, _cx: &()) -> Self::Summary {
let non_ignored_count = if self.is_ignored || self.is_external {
let non_ignored_count = if (self.is_ignored || self.is_external) && !self.is_always_included
{
0
} else {
1
Expand Down Expand Up @@ -4254,6 +4297,7 @@ 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);

// Avoid recursing until crash in the case of a recursive symlink
if job.ancestor_inodes.contains(&child_entry.inode) {
Expand All @@ -4278,6 +4322,7 @@ 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);
if !child_entry.is_ignored {
if let Some(repo) = &containing_repository {
if let Ok(repo_path) = child_entry.path.strip_prefix(&repo.work_directory) {
Expand Down Expand Up @@ -4314,6 +4359,12 @@ impl BackgroundScanner {
new_jobs.remove(job_ix);
}
}
if entry.is_always_included {
state
.snapshot
.always_included_entries
.push(entry.path.clone());
}
}

state.populate_dir(&job.path, new_entries, new_ignore);
Expand Down Expand Up @@ -4430,6 +4481,7 @@ 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);

if let (Some(scan_queue_tx), true) = (&scan_queue_tx, is_dir) {
if state.should_scan_directory(&fs_entry)
Expand Down Expand Up @@ -5317,7 +5369,7 @@ impl<'a> Traversal<'a> {
if let Some(entry) = self.cursor.item() {
if (self.include_files || !entry.is_file())
&& (self.include_dirs || !entry.is_dir())
&& (self.include_ignored || !entry.is_ignored)
&& (self.include_ignored || !entry.is_ignored || entry.is_always_included)
{
return true;
}
Expand Down Expand Up @@ -5448,10 +5500,12 @@ impl<'a> From<&'a Entry> for proto::Entry {
}
}

impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
impl<'a> TryFrom<(&'a CharBag, &PathMatcher, proto::Entry)> for Entry {
type Error = anyhow::Error;

fn try_from((root_char_bag, entry): (&'a CharBag, proto::Entry)) -> Result<Self> {
fn try_from(
(root_char_bag, always_included, entry): (&'a CharBag, &PathMatcher, proto::Entry),
) -> Result<Self> {
let kind = if entry.is_dir {
EntryKind::Dir
} else {
Expand All @@ -5462,14 +5516,15 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
Ok(Entry {
id: ProjectEntryId::from_proto(entry.id),
kind,
path,
path: path.clone(),
inode: entry.inode,
mtime: entry.mtime.map(|time| time.into()),
size: entry.size.unwrap_or(0),
canonical_path: entry
.canonical_path
.map(|path_string| Box::from(Path::new(&path_string))),
is_ignored: entry.is_ignored,
is_always_included: always_included.is_match(path.as_ref()),
is_external: entry.is_external,
git_status: git_status_from_proto(entry.git_status),
is_private: false,
Expand Down
36 changes: 34 additions & 2 deletions crates/worktree/src/worktree_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use util::paths::PathMatcher;

#[derive(Clone, PartialEq, Eq)]
pub struct WorktreeSettings {
pub file_scan_inclusions: PathMatcher,
pub file_scan_exclusions: PathMatcher,
pub private_files: PathMatcher,
}
Expand All @@ -21,13 +22,19 @@ impl WorktreeSettings {

pub fn is_path_excluded(&self, path: &Path) -> bool {
path.ancestors()
.any(|ancestor| self.file_scan_exclusions.is_match(ancestor))
.any(|ancestor| self.file_scan_exclusions.is_match(&ancestor))
}

pub fn is_path_always_included(&self, path: &Path) -> bool {
path.ancestors()
.any(|ancestor| self.file_scan_inclusions.is_match(&ancestor))
}
}

#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct WorktreeSettingsContent {
/// Completely ignore files matching globs from `file_scan_exclusions`
/// Completely ignore files matching globs from `file_scan_exclusions`. Overrides
/// `file_scan_inclusions`.
///
/// Default: [
/// "**/.git",
Expand All @@ -42,6 +49,15 @@ pub struct WorktreeSettingsContent {
#[serde(default)]
pub file_scan_exclusions: Option<Vec<String>>,

/// Always include files that match these globs when scanning for files, even if they're
/// ignored by git. This setting is overridden by `file_scan_exclusions`.
/// Default: [
/// ".env*",
/// "docker-compose.*.yml",
/// ]
#[serde(default)]
pub file_scan_inclusions: Option<Vec<String>>,

/// Treat the files matching these globs as `.env` files.
/// Default: [ "**/.env*" ]
pub private_files: Option<Vec<String>>,
Expand All @@ -59,11 +75,27 @@ impl Settings for WorktreeSettings {
let result: WorktreeSettingsContent = sources.json_merge()?;
let mut file_scan_exclusions = result.file_scan_exclusions.unwrap_or_default();
let mut private_files = result.private_files.unwrap_or_default();
let mut parsed_file_scan_inclusions: Vec<String> = result
.file_scan_inclusions
.unwrap_or_default()
.iter()
.flat_map(|glob| {
Path::new(glob)
.ancestors()
.map(|a| a.to_string_lossy().into())
})
.filter(|p| p != "")
.collect();
file_scan_exclusions.sort();
private_files.sort();
parsed_file_scan_inclusions.sort();
Ok(Self {
file_scan_exclusions: path_matchers(&file_scan_exclusions, "file_scan_exclusions")?,
private_files: path_matchers(&private_files, "private_files")?,
file_scan_inclusions: path_matchers(
&parsed_file_scan_inclusions,
"file_scan_inclusions",
)?,
})
}
}
Expand Down
Loading

0 comments on commit 6314401

Please sign in to comment.