Enhance ignore functionality with glob matcher support for exclude patterns in search

This commit is contained in:
Carlos de Paula 2025-10-20 11:31:31 -03:00
parent 2b00cae8e1
commit 9fc3d9fa10
No known key found for this signature in database
3 changed files with 91 additions and 32 deletions

View file

@ -1,3 +1,5 @@
use std::sync::Arc;
use anyhow::Result;
use yazi_config::YAZI;
use yazi_core::tab::Folder;
@ -18,16 +20,28 @@ impl Actor for Ignore {
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let gitignores = YAZI.files.gitignores;
// Get exclude patterns for the current context
// Use path string for context matching
let cwd_str = cx.cwd().as_path().map(|p| p.display().to_string()).unwrap_or_default();
// Get the appropriate context string for matching exclude rules
// For search directories, use "search://**" as the context
let cwd = cx.cwd();
let cwd_str = if cwd.is_search() {
"search://**".to_string()
} else {
cwd.as_path().map(|p| p.display().to_string()).unwrap_or_default()
};
let exclude_patterns = YAZI.files.excludes_for_context(&cwd_str);
// Create glob matcher function for compiled patterns
let glob_matcher = {
let context = cwd_str.clone();
Arc::new(move |path: &std::path::Path| YAZI.files.matches_path(path, &context))
};
// If gitignores is disabled but we have exclude patterns, apply them
if !gitignores && !exclude_patterns.is_empty() {
// Load ignore filter from exclude patterns only
let ignore_filter = if let Some(path) = cx.cwd().as_path() {
IgnoreFilter::from_patterns(path, &exclude_patterns)
IgnoreFilter::from_patterns(path, &exclude_patterns, Some(glob_matcher.clone()))
} else {
None
};
@ -52,10 +66,18 @@ impl Actor for Ignore {
// Apply to hovered
if let Some(h) = cx.hovered_folder_mut() {
let hovered_str = h.url.as_path().map(|p| p.display().to_string()).unwrap_or_default();
let hovered_str = if h.url.is_search() {
"search://**".to_string()
} else {
h.url.as_path().map(|p| p.display().to_string()).unwrap_or_default()
};
let hovered_excludes = YAZI.files.excludes_for_context(&hovered_str);
let hovered_matcher = {
let context = hovered_str.clone();
Arc::new(move |path: &std::path::Path| YAZI.files.matches_path(path, &context))
};
let hovered_filter = if let Some(path) = h.url.as_path() {
IgnoreFilter::from_patterns(path, &hovered_excludes)
IgnoreFilter::from_patterns(path, &hovered_excludes, Some(hovered_matcher))
} else {
None
};
@ -110,7 +132,7 @@ impl Actor for Ignore {
// Load ignore filter from the current directory
let ignore_filter = if let Some(path) = cx.cwd().as_path() {
IgnoreFilter::from_dir(path, &exclude_patterns, gitignores)
IgnoreFilter::from_dir(path, &exclude_patterns, gitignores, Some(glob_matcher.clone()))
} else {
None
};
@ -141,10 +163,18 @@ impl Actor for Ignore {
// Apply to hovered
if let Some(h) = cx.hovered_folder_mut() {
// Load ignore filter for hovered directory if it's a directory
let hovered_str = h.url.as_path().map(|p| p.display().to_string()).unwrap_or_default();
let hovered_str = if h.url.is_search() {
"search://**".to_string()
} else {
h.url.as_path().map(|p| p.display().to_string()).unwrap_or_default()
};
let hovered_excludes = YAZI.files.excludes_for_context(&hovered_str);
let hovered_matcher = {
let context = hovered_str.clone();
Arc::new(move |path: &std::path::Path| YAZI.files.matches_path(path, &context))
};
let hovered_filter = if let Some(path) = h.url.as_path() {
IgnoreFilter::from_dir(path, &hovered_excludes, gitignores)
IgnoreFilter::from_dir(path, &hovered_excludes, gitignores, Some(hovered_matcher))
} else {
None
};

View file

@ -101,12 +101,12 @@ impl Exclude {
// Handle glob patterns with wildcard
if self.context.ends_with("/**") {
let prefix = &self.context[..self.context.len() - 3];
// Check if path starts with prefix (absolute path match)
if path.starts_with(prefix) {
return true;
}
// Check if path contains the pattern anywhere (for relative patterns like "/target/**")
// This allows "/target/**" to match "/home/user/project/target/debug"
if prefix.starts_with('/') && !prefix.starts_with("//") {
@ -116,10 +116,10 @@ impl Exclude {
return true;
}
}
return false;
}
// Exact match or prefix match for non-wildcard patterns
path == self.context || path.starts_with(&format!("{}/", self.context))
}

View file

@ -1,4 +1,4 @@
use std::{collections::HashSet, path::{Path, PathBuf}};
use std::{collections::HashSet, path::{Path, PathBuf}, sync::Arc};
use yazi_shared::url::AsUrl;
@ -9,12 +9,25 @@ use yazi_shared::url::AsUrl;
/// patterns that follow gitignore syntax. Exclude patterns can be
/// context-specific and take precedence over git's ignore rules using the `!`
/// prefix for negation.
#[derive(Clone, Debug)]
#[derive(Clone)]
pub struct IgnoreFilter {
/// Set of paths ignored by git (from git status)
ignored_paths: HashSet<PathBuf>,
ignored_paths: HashSet<PathBuf>,
/// Custom gitignore matcher for exclude patterns
gitignore: Option<ignore::gitignore::Gitignore>,
gitignore: Option<ignore::gitignore::Gitignore>,
/// Custom glob-based matcher function for advanced pattern matching
/// Returns Some(true) if should be ignored, Some(false) if whitelisted, None if no match
glob_matcher: Option<Arc<dyn Fn(&Path) -> Option<bool> + Send + Sync>>,
}
impl std::fmt::Debug for IgnoreFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IgnoreFilter")
.field("ignored_paths", &self.ignored_paths)
.field("gitignore", &self.gitignore)
.field("glob_matcher", &self.glob_matcher.as_ref().map(|_| "Some(...)"))
.finish()
}
}
impl IgnoreFilter {
@ -46,6 +59,7 @@ impl IgnoreFilter {
dir: impl AsRef<Path>,
exclude_patterns: &[String],
use_git: bool,
glob_matcher: Option<Arc<dyn Fn(&Path) -> Option<bool> + Send + Sync>>,
) -> Option<Self> {
let dir = dir.as_ref();
@ -100,17 +114,17 @@ impl IgnoreFilter {
if ignored_paths.is_empty()
|| (ignored_paths.len() == 1 && ignored_paths.contains(&workdir.join(".git")))
{
// If we have no git-ignored paths but we have exclude patterns, still create
// If we have no git-ignored paths but we have exclude patterns or glob matcher, still create
// the filter
if gitignore.is_some() {
return Some(Self { ignored_paths, gitignore });
if gitignore.is_some() || glob_matcher.is_some() {
return Some(Self { ignored_paths, gitignore, glob_matcher });
}
return None;
}
// Store ALL ignored paths, not just the ones in the current directory
// This way, when files are loaded later, we can check them against the full set
Some(Self { ignored_paths, gitignore })
Some(Self { ignored_paths, gitignore, glob_matcher })
}
/// Creates a new `IgnoreFilter` from only exclude patterns without git
@ -128,22 +142,30 @@ impl IgnoreFilter {
///
/// This is useful when `gitignores = false` but custom exclude patterns
/// are still desired.
pub fn from_patterns(dir: impl AsRef<Path>, patterns: &[String]) -> Option<Self> {
if patterns.is_empty() {
pub fn from_patterns(
dir: impl AsRef<Path>,
patterns: &[String],
glob_matcher: Option<Arc<dyn Fn(&Path) -> Option<bool> + Send + Sync>>,
) -> Option<Self> {
if patterns.is_empty() && glob_matcher.is_none() {
return None;
}
let dir = dir.as_ref();
let mut builder = ignore::gitignore::GitignoreBuilder::new(dir);
let gitignore = if !patterns.is_empty() {
let dir = dir.as_ref();
let mut builder = ignore::gitignore::GitignoreBuilder::new(dir);
// Add each pattern
for pattern in patterns {
let _ = builder.add_line(None, pattern);
}
// Add each pattern
for pattern in patterns {
let _ = builder.add_line(None, pattern);
}
let gitignore = builder.build().ok()?;
builder.build().ok()
} else {
None
};
Some(Self { ignored_paths: HashSet::new(), gitignore: Some(gitignore) })
Some(Self { ignored_paths: HashSet::new(), gitignore, glob_matcher })
}
/// Checks if a file should be ignored based on its URL.
@ -172,7 +194,14 @@ impl IgnoreFilter {
let url = url.as_url();
let path = url.loc.as_path();
// First check if override patterns apply (they can negate ignores)
// First check glob matcher (highest priority)
if let Some(ref matcher) = self.glob_matcher {
if let Some(should_ignore) = matcher(path) {
return should_ignore;
}
}
// Then check if override patterns apply (they can negate ignores)
// Override patterns take absolute priority
if let Some(ref gitignore) = self.gitignore {
let matched = gitignore.matched(path, path.is_dir());