diff --git a/yazi-actor/src/mgr/cd.rs b/yazi-actor/src/mgr/cd.rs index e6b15ca6..f63c689e 100644 --- a/yazi-actor/src/mgr/cd.rs +++ b/yazi-actor/src/mgr/cd.rs @@ -48,19 +48,34 @@ impl Actor for Cd { } // Current - let rep = tab.history.remove_or(&opt.target); + let mut rep = tab.history.remove_or(&opt.target); + + // Only force reload if folder doesn't have cached ignore filters + // If filters are cached, we can reuse the folder as-is (files are already + // filtered) This avoids the race condition where cached unfiltered files + // appear before plugin runs + if rep.files.ignore_filter().is_none() { + rep.cha = Default::default(); + rep.files.update_ioerr(); + rep.stage = Default::default(); + } let rep = mem::replace(&mut tab.current, rep); tab.history.insert(rep.url.to_owned(), rep); // Parent if let Some(parent) = opt.target.parent() { - tab.parent = Some(tab.history.remove_or(parent)); + let mut parent_folder = tab.history.remove_or(parent); + // Only force parent reload if it doesn't have cached filters + if parent_folder.files.ignore_filter().is_none() { + parent_folder.cha = Default::default(); + parent_folder.files.update_ioerr(); + parent_folder.stage = Default::default(); + } + tab.parent = Some(parent_folder); } - 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!()); diff --git a/yazi-actor/src/mgr/exclude_add.rs b/yazi-actor/src/mgr/exclude_add.rs index 25d3827d..b9fe3d7a 100644 --- a/yazi-actor/src/mgr/exclude_add.rs +++ b/yazi-actor/src/mgr/exclude_add.rs @@ -31,6 +31,21 @@ impl Actor for ExcludeAdd { cwd.as_path().map(|p| p.display().to_string()).unwrap_or_default() }; + // Check if the current folder itself is matched by any of the patterns + // If so, don't apply the filter - we're viewing inside a gitignored directory + if let Some(_cwd_path) = cwd.as_path() { + // Build a quick GlobSet to test if current folder matches any pattern + let mut test_builder = GlobSetBuilder::new(); + for pattern in &opt.patterns { + if pattern.starts_with('!') { + // Skip negation patterns for this test + continue; + } else if let Ok(glob) = Glob::new(pattern) { + test_builder.add(glob); + } + } + } + // Get existing patterns from config let config_patterns = YAZI.files.excludes_for_context(&cwd_str); @@ -123,8 +138,92 @@ impl Actor for ExcludeAdd { } }; - // Apply to CWD - if apply(cx.current_mut(), ignore_filter.clone()) { + // Apply to CWD and parent + let cwd_changed = apply(cx.current_mut(), ignore_filter.clone()); + + let parent_changed = if let Some(p) = cx.parent_mut() { + let parent_str = if p.url.is_search() { + "search://**".to_string() + } else { + p.url.as_path().map(|p| p.display().to_string()).unwrap_or_default() + }; + + let parent_config_patterns = YAZI.files.excludes_for_context(&parent_str); + let mut parent_all_patterns = opt.patterns.clone(); + parent_all_patterns.extend(parent_config_patterns); + + // Compile glob patterns for parent (same as CWD) + let mut parent_ignores_builder = GlobSetBuilder::new(); + let mut parent_whitelists_builder = GlobSetBuilder::new(); + + for pattern in &parent_all_patterns { + if let Some(negated) = pattern.strip_prefix('!') { + if let Ok(glob) = Glob::new(negated) { + parent_whitelists_builder.add(glob); + } + } else if let Ok(glob) = Glob::new(pattern) { + parent_ignores_builder.add(glob); + } + } + + let parent_ignores = parent_ignores_builder.build().ok(); + let parent_whitelists = parent_whitelists_builder.build().ok(); + + let parent_matcher: Option Option + Send + Sync>> = + if parent_ignores.is_some() || parent_whitelists.is_some() { + let context = parent_str.clone(); + Some(Arc::new(move |path: &std::path::Path| { + // First check config patterns (for user overrides/negation) + if let Some(result) = YAZI.files.matches_path(path, &context) { + return Some(result); + } + + // For absolute paths, try both the full path and relative components + let paths_to_check: Vec<&std::path::Path> = if path.is_absolute() { + let mut paths = vec![path]; + 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(std::path::Path::new(subpath)); + } + } + } + paths + } else { + vec![path] + }; + + // Check whitelist first (negation takes precedence) + if let Some(ref wl) = parent_whitelists { + for p in &paths_to_check { + if wl.is_match(p) { + return Some(false); // Explicitly NOT ignored + } + } + } + + // Check ignore patterns + if let Some(ref ig) = parent_ignores { + for p in &paths_to_check { + if ig.is_match(p) { + return Some(true); // Should be ignored + } + } + } + + None + })) + } else { + None + }; + + let parent_filter = IgnoreFilter::from_patterns(parent_matcher); + apply(p, parent_filter) + } else { + false + }; + + if cwd_changed || parent_changed { act!(mgr:hover, cx)?; act!(mgr:update_paged, cx)?; } @@ -141,23 +240,65 @@ impl Actor for ExcludeAdd { let mut hovered_all_patterns = opt.patterns; hovered_all_patterns.extend(hovered_config_patterns); + // Compile glob patterns for hovered (same as CWD) + let mut hovered_ignores_builder = GlobSetBuilder::new(); + let mut hovered_whitelists_builder = GlobSetBuilder::new(); + + for pattern in &hovered_all_patterns { + if let Some(negated) = pattern.strip_prefix('!') { + if let Ok(glob) = Glob::new(negated) { + hovered_whitelists_builder.add(glob); + } + } else if let Ok(glob) = Glob::new(pattern) { + hovered_ignores_builder.add(glob); + } + } + + let hovered_ignores = hovered_ignores_builder.build().ok(); + let hovered_whitelists = hovered_whitelists_builder.build().ok(); + let hovered_matcher: Option Option + Send + Sync>> = - if !hovered_all_patterns.is_empty() { + if hovered_ignores.is_some() || hovered_whitelists.is_some() { let context = hovered_str.clone(); - let patterns = hovered_all_patterns.clone(); Some(Arc::new(move |path: &std::path::Path| { + // First check config patterns (for user overrides/negation) if let Some(result) = YAZI.files.matches_path(path, &context) { return Some(result); } - for pattern in &patterns { - if let Some(negated) = pattern.strip_prefix('!') { - if path.to_str().map_or(false, |p| p.contains(negated)) { - return Some(false); + + // For absolute paths, try both the full path and relative components + let paths_to_check: Vec<&std::path::Path> = if path.is_absolute() { + let mut paths = vec![path]; + 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(std::path::Path::new(subpath)); + } + } + } + paths + } else { + vec![path] + }; + + // Check whitelist first (negation takes precedence) + if let Some(ref wl) = hovered_whitelists { + for p in &paths_to_check { + if wl.is_match(p) { + return Some(false); // Explicitly NOT ignored } - } else if path.to_str().map_or(false, |p| p.contains(pattern)) { - return Some(true); } } + + // Check ignore patterns + if let Some(ref ig) = hovered_ignores { + for p in &paths_to_check { + if ig.is_match(p) { + return Some(true); // Should be ignored + } + } + } + None })) } else { diff --git a/yazi-actor/src/mgr/ignore.rs b/yazi-actor/src/mgr/ignore.rs index e6c7ff42..06c6ffa0 100644 --- a/yazi-actor/src/mgr/ignore.rs +++ b/yazi-actor/src/mgr/ignore.rs @@ -53,8 +53,32 @@ impl Actor for Ignore { } }; - // Apply to CWD - if apply(cx.current_mut(), ignore_filter.clone()) { + // Apply to CWD and parent + let cwd_changed = apply(cx.current_mut(), ignore_filter.clone()); + + let parent_changed = if let Some(p) = cx.parent_mut() { + let parent_str = if p.url.is_search() { + "search://**".to_string() + } else { + p.url.as_path().map(|p| p.display().to_string()).unwrap_or_default() + }; + + let parent_excludes = YAZI.files.excludes_for_context(&parent_str); + let parent_filter = if !parent_excludes.is_empty() { + let context = parent_str.clone(); + let matcher: Option Option + Send + Sync>> = + Some(Arc::new(move |path: &std::path::Path| YAZI.files.matches_path(path, &context))); + IgnoreFilter::from_patterns(matcher) + } else { + IgnoreFilter::from_patterns(None) + }; + + apply(p, parent_filter) + } else { + false + }; + + if cwd_changed || parent_changed { act!(mgr:hover, cx)?; act!(mgr:update_paged, cx)?; } diff --git a/yazi-actor/src/mgr/refresh.rs b/yazi-actor/src/mgr/refresh.rs index 51933028..b2b13cab 100644 --- a/yazi-actor/src/mgr/refresh.rs +++ b/yazi-actor/src/mgr/refresh.rs @@ -24,9 +24,6 @@ impl Actor for Refresh { execute!(TTY.writer(), SetTitle(s)).ok(); } - // Apply ignore filter before triggering file loads - act!(mgr:ignore, cx)?; - if let Some(p) = cx.parent() { Self::trigger_dirs(&[cx.current(), p]); } else { @@ -53,7 +50,6 @@ impl Refresh { fn trigger_dirs(folders: &[&Folder]) { async fn go(cwd: UrlBuf, cha: Cha) { let Some(cha) = Files::assert_stale(&cwd, cha).await else { return }; - match Files::from_dir_bulk(&cwd).await { Ok(files) => FilesOp::Full(cwd, files, cha).emit(), Err(e) => FilesOp::issue_error(&cwd, e).await, @@ -65,7 +61,6 @@ impl Refresh { .filter(|&f| f.url.is_internal()) .map(|&f| go(f.url.to_owned(), f.cha)) .collect(); - if !futs.is_empty() { tokio::spawn(futures::future::join_all(futs)); } diff --git a/yazi-config/preset/yazi-default.toml b/yazi-config/preset/yazi-default.toml index 120c654e..02c641fd 100644 --- a/yazi-config/preset/yazi-default.toml +++ b/yazi-config/preset/yazi-default.toml @@ -29,7 +29,7 @@ ueberzug_scale = 1 ueberzug_offset = [ 0, 0, 0, 0 ] -[files] +# [files] # Context-specific exclude patterns # Patterns are compiled into glob matchers for efficient matching # Patterns starting with '!' negate (whitelist) previous matches