mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
Enhance 'cd' action to include ignore functionality and refactor excluded state handling
This commit is contained in:
parent
f068dafec2
commit
397b30b996
6 changed files with 192 additions and 46 deletions
|
|
@ -76,6 +76,7 @@ impl Actor for Cd {
|
|||
err!(Pubsub::pub_after_cd(tab.id, tab.cwd()));
|
||||
act!(mgr:hidden, cx)?;
|
||||
act!(mgr:sort, cx)?;
|
||||
act!(mgr:ignore, cx)?;
|
||||
act!(mgr:hover, cx)?;
|
||||
act!(mgr:refresh, cx)?;
|
||||
succ!(render!());
|
||||
|
|
|
|||
|
|
@ -310,7 +310,7 @@ impl Actor for ExcludeAdd {
|
|||
if apply(h, hovered_filter) {
|
||||
render!(h.repos(None));
|
||||
act!(mgr:peek, cx, true)?;
|
||||
} else if hovered.as_deref() != cx.hovered().map(|f| f.urn()) {
|
||||
} else if hovered != cx.hovered().map(|f| f.urn().to_owned()) {
|
||||
act!(mgr:peek, cx)?;
|
||||
act!(mgr:watch, cx)?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,10 @@ impl Actor for Excluded {
|
|||
const NAME: &str = "excluded";
|
||||
|
||||
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
|
||||
let state = opt.state.bool(cx.tab().current.files.show_excluded());
|
||||
let hovered = cx.hovered().map(|f| f.urn().to_owned());
|
||||
let current_state = cx.tab().current.files.show_excluded();
|
||||
let state = opt.state.bool(current_state);
|
||||
|
||||
let hovered = cx.hovered().map(|f| f.urn().to_owned());
|
||||
let apply = |f: &mut Folder| {
|
||||
if f.stage == FolderStage::Loading {
|
||||
render!();
|
||||
|
|
@ -42,7 +43,7 @@ impl Actor for Excluded {
|
|||
{
|
||||
render!(h.repos(None));
|
||||
act!(mgr:peek, cx, true)?;
|
||||
} else if hovered.as_deref() != cx.hovered().map(|f| f.urn()) {
|
||||
} else if hovered != cx.hovered().map(|f| f.urn().to_owned()) {
|
||||
act!(mgr:peek, cx)?;
|
||||
act!(mgr:watch, cx)?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ impl Actor for Ignore {
|
|||
if apply(h, hovered_filter) {
|
||||
render!(h.repos(None));
|
||||
act!(mgr:peek, cx, true)?;
|
||||
} else if hovered.as_deref() != cx.hovered().map(|f| f.urn()) {
|
||||
} else if hovered != cx.hovered().map(|f| f.urn().to_owned()) {
|
||||
act!(mgr:peek, cx)?;
|
||||
act!(mgr:watch, cx)?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,12 +54,36 @@ impl Exclude {
|
|||
for pattern in &self.urn {
|
||||
if let Some(negated) = pattern.strip_prefix('!') {
|
||||
// Negation pattern - add to whitelist
|
||||
let glob = Glob::new(negated)?;
|
||||
// Transform simple filename patterns to match anywhere in the tree
|
||||
let transformed = if negated.contains('/') || negated.starts_with("**/") {
|
||||
negated.to_string()
|
||||
} else {
|
||||
// Match the item itself and everything inside it
|
||||
format!("**/{}", negated)
|
||||
};
|
||||
let glob = Glob::new(&transformed)?;
|
||||
whitelist_builder.add(glob);
|
||||
// Also match everything inside directories with this name
|
||||
if !negated.contains('/') && !negated.starts_with("**/") {
|
||||
let glob_inner = Glob::new(&format!("**/{}/**", negated))?;
|
||||
whitelist_builder.add(glob_inner);
|
||||
}
|
||||
} else {
|
||||
// Regular pattern - add to ignore list
|
||||
let glob = Glob::new(pattern)?;
|
||||
// Transform simple filename patterns to match anywhere in the tree
|
||||
let transformed = if pattern.contains('/') || pattern.starts_with("**/") {
|
||||
pattern.to_string()
|
||||
} else {
|
||||
// Match the item itself: **/.git
|
||||
format!("**/{}", pattern)
|
||||
};
|
||||
let glob = Glob::new(&transformed)?;
|
||||
ignore_builder.add(glob);
|
||||
// Also match everything inside directories with this name: **/.git/**
|
||||
if !pattern.contains('/') && !pattern.starts_with("**/") {
|
||||
let glob_inner = Glob::new(&format!("**/{}/**", pattern))?;
|
||||
ignore_builder.add(glob_inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -77,39 +101,14 @@ impl Exclude {
|
|||
pub fn matches_path(&self, path: &Path) -> Option<bool> {
|
||||
let compiled = self.compiled.as_ref()?;
|
||||
|
||||
// For absolute paths, try both the full path and relative components
|
||||
// This helps patterns like **/.git/** match /home/user/project/.git
|
||||
let paths_to_check: Vec<&Path> = if path.is_absolute() {
|
||||
// Also check each component as if it's relative
|
||||
// This allows **/.git/** to match /home/user/.git/config
|
||||
let mut paths = vec![path];
|
||||
|
||||
// Check if any path component matches by checking relative sub-paths
|
||||
// For /home/user/.git/config, we want to match against .git/config too
|
||||
if let Some(components) = path.to_str() {
|
||||
for (i, _) in components.match_indices('/').skip(1) {
|
||||
if let Some(subpath) = components.get(i + 1..) {
|
||||
paths.push(Path::new(subpath));
|
||||
}
|
||||
}
|
||||
}
|
||||
paths
|
||||
} else {
|
||||
vec![path]
|
||||
};
|
||||
|
||||
// Check whitelist first (negation takes precedence)
|
||||
for p in &paths_to_check {
|
||||
if compiled.whitelists.is_match(p) {
|
||||
return Some(false); // Explicitly NOT ignored
|
||||
}
|
||||
if compiled.whitelists.is_match(path) {
|
||||
return Some(false); // Explicitly NOT ignored
|
||||
}
|
||||
|
||||
// Check ignore patterns
|
||||
for p in &paths_to_check {
|
||||
if compiled.ignores.is_match(p) {
|
||||
return Some(true); // Should be ignored
|
||||
}
|
||||
if compiled.ignores.is_match(path) {
|
||||
return Some(true); // Should be ignored
|
||||
}
|
||||
|
||||
None // No match
|
||||
|
|
@ -127,13 +126,10 @@ impl Exclude {
|
|||
let pattern = &self.r#in[3..]; // Remove leading "**/", e.g., "target" or "target/**"
|
||||
|
||||
// Strip trailing /** if present
|
||||
let pattern = if let Some(p) = pattern.strip_suffix("/**") {
|
||||
p
|
||||
} else {
|
||||
pattern
|
||||
};
|
||||
let pattern = if let Some(p) = pattern.strip_suffix("/**") { p } else { pattern };
|
||||
|
||||
// Check if path ends with the pattern (e.g., /home/user/project/target matches **/target)
|
||||
// Check if path ends with the pattern (e.g., /home/user/project/target matches
|
||||
// **/target)
|
||||
if path.ends_with(&format!("/{}", pattern)) || path.ends_with(pattern) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -174,3 +170,151 @@ impl Exclude {
|
|||
path == self.r#in || path.starts_with(&format!("{}/", self.r#in))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn create_exclude(in_pattern: &str, urn_patterns: Vec<&str>) -> Exclude {
|
||||
let mut exclude = Exclude {
|
||||
r#in: in_pattern.to_string(),
|
||||
urn: urn_patterns.into_iter().map(String::from).collect(),
|
||||
compiled: None,
|
||||
};
|
||||
exclude.compile().unwrap();
|
||||
exclude
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_matching_with_dots() {
|
||||
let exclude = create_exclude("*", vec![".git"]);
|
||||
|
||||
// Test various path contexts - all should match "*"
|
||||
let test_cases = vec![
|
||||
("/home/user/projects/yazi/yazi", true),
|
||||
("/home/user/projects/yazi/yazi-rs.github.io", true),
|
||||
("/home/user/projects/yazi/command-palette.yazi", true),
|
||||
("/home/user/projects/yazi/gitignore.yazi", true),
|
||||
];
|
||||
|
||||
for (context, should_match) in test_cases {
|
||||
let matches = exclude.matches_context(context);
|
||||
assert_eq!(
|
||||
matches, should_match,
|
||||
"Context matching failed for {}: expected {}, got {}",
|
||||
context, should_match, matches
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_pattern_behavior() {
|
||||
// Test how globset matches simple patterns like ".git"
|
||||
use globset::Glob;
|
||||
|
||||
let patterns_and_paths = vec![
|
||||
// Pattern ".git" gets transformed to "**/.git" in our compile() method
|
||||
// So let's test the transformed version
|
||||
("**/.git", "/home/user/.git", true),
|
||||
("**/.git", "/home/user/proj/.git", true),
|
||||
("**/.git", "/home/user/command-palette.yazi/.git", true),
|
||||
("**/.git", ".git", true),
|
||||
// **/.git/** matches files INSIDE .git, not the .git directory itself
|
||||
("**/.git/**", "/home/user/.git", false),
|
||||
("**/.git/**", "/home/user/.git/config", true),
|
||||
];
|
||||
|
||||
for (pattern, path, expected) in patterns_and_paths {
|
||||
let glob = Glob::new(pattern).unwrap().compile_matcher();
|
||||
let matches = glob.is_match(Path::new(path));
|
||||
assert_eq!(
|
||||
matches, expected,
|
||||
"Pattern '{}' with path '{}': expected {}, got {}",
|
||||
pattern, path, expected, matches
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_path_matching_with_git() {
|
||||
let exclude = create_exclude("*", vec![".git"]);
|
||||
|
||||
// Test various .git paths
|
||||
// Pattern ".git" is transformed to "**/.git" which matches any component named
|
||||
// .git
|
||||
let test_paths = vec![
|
||||
("/home/user/projects/yazi/yazi/.git", true),
|
||||
("/home/user/projects/yazi/yazi-rs.github.io/.git", true),
|
||||
("/home/user/projects/yazi/command-palette.yazi/.git", true),
|
||||
("/home/user/projects/yazi/gitignore.yazi/.git", true),
|
||||
// Files inside .git now also match "**/.git" since .git is the component name
|
||||
("/home/user/projects/yazi/yazi/.git/config", true),
|
||||
("/home/user/projects/yazi/command-palette.yazi/.git/config", true),
|
||||
];
|
||||
|
||||
for (path_str, should_match) in test_paths {
|
||||
let path = Path::new(path_str);
|
||||
let result = exclude.matches_path(path);
|
||||
// None means no match, which is equivalent to false (not ignored)
|
||||
let actual = result.unwrap_or(false);
|
||||
assert_eq!(
|
||||
actual, should_match,
|
||||
"Path matching failed for {}: expected {}, got {}",
|
||||
path_str, should_match, actual
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_glob_pattern_matching() {
|
||||
// User provides "**/.git" explicitly - matches only things NAMED .git
|
||||
let exclude = create_exclude("*", vec!["**/.git"]);
|
||||
|
||||
let test_paths = vec![
|
||||
// Should match .git directory itself
|
||||
("/home/user/projects/yazi/yazi/.git", true),
|
||||
("/home/user/projects/yazi/command-palette.yazi/.git", true),
|
||||
// Should NOT match files inside .git when using **/.git alone
|
||||
("/home/user/projects/yazi/yazi/.git/config", false),
|
||||
];
|
||||
|
||||
for (path_str, should_match) in test_paths {
|
||||
let path = Path::new(path_str);
|
||||
let result = exclude.matches_path(path);
|
||||
let actual = result.unwrap_or(false);
|
||||
assert_eq!(
|
||||
actual, should_match,
|
||||
"Glob pattern matching failed for {}: expected {}, got {}",
|
||||
path_str, should_match, actual
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_glob_pattern_with_trailing_slash() {
|
||||
// Pattern **/.git/** means match everything inside any .git directory
|
||||
let exclude = create_exclude("*", vec!["**/.git/**"]);
|
||||
|
||||
let test_paths = vec![
|
||||
// Should NOT match .git directory itself with /**
|
||||
("/home/user/projects/yazi/yazi/.git", false),
|
||||
// Should match all files inside .git
|
||||
("/home/user/projects/yazi/yazi/.git/config", true),
|
||||
("/home/user/projects/yazi/command-palette.yazi/.git", false),
|
||||
("/home/user/projects/yazi/command-palette.yazi/.git/config", true),
|
||||
];
|
||||
|
||||
for (path_str, should_match) in test_paths {
|
||||
let path = Path::new(path_str);
|
||||
let result = exclude.matches_path(path);
|
||||
let actual = result.unwrap_or(false);
|
||||
assert_eq!(
|
||||
actual, should_match,
|
||||
"Glob pattern with /** matching failed for {}: expected {}, got {}",
|
||||
path_str, should_match, actual
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,10 +40,10 @@ impl Files {
|
|||
let mut result = None;
|
||||
|
||||
for exclude in &self.excludes {
|
||||
if exclude.matches_context(context) {
|
||||
if let Some(should_ignore) = exclude.matches_path(path) {
|
||||
result = Some(should_ignore);
|
||||
}
|
||||
if exclude.matches_context(context)
|
||||
&& let Some(should_ignore) = exclude.matches_path(path)
|
||||
{
|
||||
result = Some(should_ignore);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue