diff --git a/yazi-actor/src/mgr/ignore.rs b/yazi-actor/src/mgr/ignore.rs index 8add8a99..3ba1344e 100644 --- a/yazi-actor/src/mgr/ignore.rs +++ b/yazi-actor/src/mgr/ignore.rs @@ -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 { 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 }; diff --git a/yazi-config/src/files/exclude.rs b/yazi-config/src/files/exclude.rs index c5818820..ca198d79 100644 --- a/yazi-config/src/files/exclude.rs +++ b/yazi-config/src/files/exclude.rs @@ -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)) } diff --git a/yazi-fs/src/ignore.rs b/yazi-fs/src/ignore.rs index 00ecc35d..0b18ee55 100644 --- a/yazi-fs/src/ignore.rs +++ b/yazi-fs/src/ignore.rs @@ -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, + ignored_paths: HashSet, /// Custom gitignore matcher for exclude patterns - gitignore: Option, + gitignore: Option, + /// 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 Option + 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, exclude_patterns: &[String], use_git: bool, + glob_matcher: Option Option + Send + Sync>>, ) -> Option { 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, patterns: &[String]) -> Option { - if patterns.is_empty() { + pub fn from_patterns( + dir: impl AsRef, + patterns: &[String], + glob_matcher: Option Option + Send + Sync>>, + ) -> Option { + 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());