mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
Enhance ignore functionality with glob matcher support for exclude patterns in search
This commit is contained in:
parent
2b00cae8e1
commit
9fc3d9fa10
3 changed files with 91 additions and 32 deletions
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue