Skip to content

Commit e5f39e8

Browse files
committed
Enhance 'cd' action to include ignore functionality and refactor excluded state handling
1 parent 6d9d525 commit e5f39e8

File tree

3 files changed

+185
-39
lines changed

3 files changed

+185
-39
lines changed

yazi-actor/src/mgr/cd.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ impl Actor for Cd {
7676
err!(Pubsub::pub_after_cd(tab.id, tab.cwd()));
7777
act!(mgr:hidden, cx)?;
7878
act!(mgr:sort, cx)?;
79+
act!(mgr:ignore, cx)?;
7980
act!(mgr:hover, cx)?;
8081
act!(mgr:refresh, cx)?;
8182
succ!(render!());

yazi-actor/src/mgr/excluded.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ impl Actor for Excluded {
1515
const NAME: &str = "excluded";
1616

1717
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
18-
let state = opt.state.bool(cx.tab().current.files.show_excluded());
19-
let hovered = cx.hovered().map(|f| f.urn().to_owned());
18+
let current_state = cx.tab().current.files.show_excluded();
19+
let state = opt.state.bool(current_state);
2020

21+
let hovered = cx.hovered().map(|f| f.urn().to_owned());
2122
let apply = |f: &mut Folder| {
2223
if f.stage == FolderStage::Loading {
2324
render!();

yazi-config/src/files/exclude.rs

Lines changed: 181 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,36 @@ impl Exclude {
5454
for pattern in &self.urn {
5555
if let Some(negated) = pattern.strip_prefix('!') {
5656
// Negation pattern - add to whitelist
57-
let glob = Glob::new(negated)?;
57+
// Transform simple filename patterns to match anywhere in the tree
58+
let transformed = if negated.contains('/') || negated.starts_with("**/") {
59+
negated.to_string()
60+
} else {
61+
// Match the item itself and everything inside it
62+
format!("**/{}", negated)
63+
};
64+
let glob = Glob::new(&transformed)?;
5865
whitelist_builder.add(glob);
66+
// Also match everything inside directories with this name
67+
if !negated.contains('/') && !negated.starts_with("**/") {
68+
let glob_inner = Glob::new(&format!("**/{}/**", negated))?;
69+
whitelist_builder.add(glob_inner);
70+
}
5971
} else {
6072
// Regular pattern - add to ignore list
61-
let glob = Glob::new(pattern)?;
73+
// Transform simple filename patterns to match anywhere in the tree
74+
let transformed = if pattern.contains('/') || pattern.starts_with("**/") {
75+
pattern.to_string()
76+
} else {
77+
// Match the item itself: **/.git
78+
format!("**/{}", pattern)
79+
};
80+
let glob = Glob::new(&transformed)?;
6281
ignore_builder.add(glob);
82+
// Also match everything inside directories with this name: **/.git/**
83+
if !pattern.contains('/') && !pattern.starts_with("**/") {
84+
let glob_inner = Glob::new(&format!("**/{}/**", pattern))?;
85+
ignore_builder.add(glob_inner);
86+
}
6387
}
6488
}
6589

@@ -77,39 +101,14 @@ impl Exclude {
77101
pub fn matches_path(&self, path: &Path) -> Option<bool> {
78102
let compiled = self.compiled.as_ref()?;
79103

80-
// For absolute paths, try both the full path and relative components
81-
// This helps patterns like **/.git/** match /home/user/project/.git
82-
let paths_to_check: Vec<&Path> = if path.is_absolute() {
83-
// Also check each component as if it's relative
84-
// This allows **/.git/** to match /home/user/.git/config
85-
let mut paths = vec![path];
86-
87-
// Check if any path component matches by checking relative sub-paths
88-
// For /home/user/.git/config, we want to match against .git/config too
89-
if let Some(components) = path.to_str() {
90-
for (i, _) in components.match_indices('/').skip(1) {
91-
if let Some(subpath) = components.get(i + 1..) {
92-
paths.push(Path::new(subpath));
93-
}
94-
}
95-
}
96-
paths
97-
} else {
98-
vec![path]
99-
};
100-
101104
// Check whitelist first (negation takes precedence)
102-
for p in &paths_to_check {
103-
if compiled.whitelists.is_match(p) {
104-
return Some(false); // Explicitly NOT ignored
105-
}
105+
if compiled.whitelists.is_match(path) {
106+
return Some(false); // Explicitly NOT ignored
106107
}
107108

108109
// Check ignore patterns
109-
for p in &paths_to_check {
110-
if compiled.ignores.is_match(p) {
111-
return Some(true); // Should be ignored
112-
}
110+
if compiled.ignores.is_match(path) {
111+
return Some(true); // Should be ignored
113112
}
114113

115114
None // No match
@@ -127,13 +126,10 @@ impl Exclude {
127126
let pattern = &self.r#in[3..]; // Remove leading "**/", e.g., "target" or "target/**"
128127

129128
// Strip trailing /** if present
130-
let pattern = if let Some(p) = pattern.strip_suffix("/**") {
131-
p
132-
} else {
133-
pattern
134-
};
129+
let pattern = if let Some(p) = pattern.strip_suffix("/**") { p } else { pattern };
135130

136-
// Check if path ends with the pattern (e.g., /home/user/project/target matches **/target)
131+
// Check if path ends with the pattern (e.g., /home/user/project/target matches
132+
// **/target)
137133
if path.ends_with(&format!("/{}", pattern)) || path.ends_with(pattern) {
138134
return true;
139135
}
@@ -174,3 +170,151 @@ impl Exclude {
174170
path == self.r#in || path.starts_with(&format!("{}/", self.r#in))
175171
}
176172
}
173+
174+
#[cfg(test)]
175+
mod tests {
176+
use std::path::Path;
177+
178+
use super::*;
179+
180+
fn create_exclude(in_pattern: &str, urn_patterns: Vec<&str>) -> Exclude {
181+
let mut exclude = Exclude {
182+
r#in: in_pattern.to_string(),
183+
urn: urn_patterns.into_iter().map(String::from).collect(),
184+
compiled: None,
185+
};
186+
exclude.compile().unwrap();
187+
exclude
188+
}
189+
190+
#[test]
191+
fn test_context_matching_with_dots() {
192+
let exclude = create_exclude("*", vec![".git"]);
193+
194+
// Test various path contexts - all should match "*"
195+
let test_cases = vec![
196+
("/home/carlosedp/projects/yazi/yazi", true),
197+
("/home/carlosedp/projects/yazi/yazi-rs.github.io", true),
198+
("/home/carlosedp/projects/yazi/command-palette.yazi", true),
199+
("/home/carlosedp/projects/yazi/gitignore.yazi", true),
200+
];
201+
202+
for (context, should_match) in test_cases {
203+
let matches = exclude.matches_context(context);
204+
assert_eq!(
205+
matches, should_match,
206+
"Context matching failed for {}: expected {}, got {}",
207+
context, should_match, matches
208+
);
209+
}
210+
}
211+
212+
#[test]
213+
fn test_simple_pattern_behavior() {
214+
// Test how globset matches simple patterns like ".git"
215+
use globset::Glob;
216+
217+
let patterns_and_paths = vec![
218+
// Pattern ".git" gets transformed to "**/.git" in our compile() method
219+
// So let's test the transformed version
220+
("**/.git", "/home/user/.git", true),
221+
("**/.git", "/home/user/proj/.git", true),
222+
("**/.git", "/home/user/command-palette.yazi/.git", true),
223+
("**/.git", ".git", true),
224+
// **/.git/** matches files INSIDE .git, not the .git directory itself
225+
("**/.git/**", "/home/user/.git", false),
226+
("**/.git/**", "/home/user/.git/config", true),
227+
];
228+
229+
for (pattern, path, expected) in patterns_and_paths {
230+
let glob = Glob::new(pattern).unwrap().compile_matcher();
231+
let matches = glob.is_match(Path::new(path));
232+
assert_eq!(
233+
matches, expected,
234+
"Pattern '{}' with path '{}': expected {}, got {}",
235+
pattern, path, expected, matches
236+
);
237+
}
238+
}
239+
240+
#[test]
241+
fn test_path_matching_with_git() {
242+
let exclude = create_exclude("*", vec![".git"]);
243+
244+
// Test various .git paths
245+
// Pattern ".git" is transformed to "**/.git" which matches any component named
246+
// .git
247+
let test_paths = vec![
248+
("/home/carlosedp/projects/yazi/yazi/.git", true),
249+
("/home/carlosedp/projects/yazi/yazi-rs.github.io/.git", true),
250+
("/home/carlosedp/projects/yazi/command-palette.yazi/.git", true),
251+
("/home/carlosedp/projects/yazi/gitignore.yazi/.git", true),
252+
// Files inside .git now also match "**/.git" since .git is the component name
253+
("/home/carlosedp/projects/yazi/yazi/.git/config", true),
254+
("/home/carlosedp/projects/yazi/command-palette.yazi/.git/config", true),
255+
];
256+
257+
for (path_str, should_match) in test_paths {
258+
let path = Path::new(path_str);
259+
let result = exclude.matches_path(path);
260+
// None means no match, which is equivalent to false (not ignored)
261+
let actual = result.unwrap_or(false);
262+
assert_eq!(
263+
actual, should_match,
264+
"Path matching failed for {}: expected {}, got {}",
265+
path_str, should_match, actual
266+
);
267+
}
268+
}
269+
270+
#[test]
271+
fn test_glob_pattern_matching() {
272+
// User provides "**/.git" explicitly - matches only things NAMED .git
273+
let exclude = create_exclude("*", vec!["**/.git"]);
274+
275+
let test_paths = vec![
276+
// Should match .git directory itself
277+
("/home/carlosedp/projects/yazi/yazi/.git", true),
278+
("/home/carlosedp/projects/yazi/command-palette.yazi/.git", true),
279+
// Should NOT match files inside .git when using **/.git alone
280+
("/home/carlosedp/projects/yazi/yazi/.git/config", false),
281+
];
282+
283+
for (path_str, should_match) in test_paths {
284+
let path = Path::new(path_str);
285+
let result = exclude.matches_path(path);
286+
let actual = result.unwrap_or(false);
287+
assert_eq!(
288+
actual, should_match,
289+
"Glob pattern matching failed for {}: expected {}, got {}",
290+
path_str, should_match, actual
291+
);
292+
}
293+
}
294+
295+
#[test]
296+
fn test_glob_pattern_with_trailing_slash() {
297+
// Pattern **/.git/** means match everything inside any .git directory
298+
let exclude = create_exclude("*", vec!["**/.git/**"]);
299+
300+
let test_paths = vec![
301+
// Should NOT match .git directory itself with /**
302+
("/home/carlosedp/projects/yazi/yazi/.git", false),
303+
// Should match all files inside .git
304+
("/home/carlosedp/projects/yazi/yazi/.git/config", true),
305+
("/home/carlosedp/projects/yazi/command-palette.yazi/.git", false),
306+
("/home/carlosedp/projects/yazi/command-palette.yazi/.git/config", true),
307+
];
308+
309+
for (path_str, should_match) in test_paths {
310+
let path = Path::new(path_str);
311+
let result = exclude.matches_path(path);
312+
let actual = result.unwrap_or(false);
313+
assert_eq!(
314+
actual, should_match,
315+
"Glob pattern with /** matching failed for {}: expected {}, got {}",
316+
path_str, should_match, actual
317+
);
318+
}
319+
}
320+
}

0 commit comments

Comments
 (0)