Add ignore functionality to file manager

- Introduced `Ignore` actor to manage ignore patterns based on gitignore and user-defined overrides.
- Implemented `IgnoreFilter` to handle ignored paths and patterns.
- Updated `Files` struct to include an ignore filter, allowing files to be filtered based on ignore rules.
- Modified existing actors and managers to apply ignore filters during file operations.
- Updated  `Cargo.toml` and `Cargo.lock` to include new dependencies related to ignore functionality.
This commit is contained in:
Carlos de Paula 2025-10-17 16:27:30 -03:00
parent 46569a1f48
commit 7bfad2436f
13 changed files with 791 additions and 76 deletions

332
Cargo.lock generated
View file

@ -1011,6 +1011,17 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "document-features"
version = "0.2.11"
@ -1275,6 +1286,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
[[package]]
name = "fsevent-sys"
version = "4.1.0"
@ -1441,6 +1461,19 @@ version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
[[package]]
name = "git2"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
dependencies = [
"bitflags 2.9.4",
"libc",
"libgit2-sys",
"log",
"url",
]
[[package]]
name = "globset"
version = "0.4.16"
@ -1574,12 +1607,135 @@ dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
dependencies = [
"displaydoc",
"potential_utf",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locale_core"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_normalizer"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
dependencies = [
"displaydoc",
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
[[package]]
name = "icu_properties"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locale_core",
"icu_properties_data",
"icu_provider",
"potential_utf",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
[[package]]
name = "icu_provider"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
dependencies = [
"displaydoc",
"icu_locale_core",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerotrie",
"zerovec",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
name = "ignore"
version = "0.4.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b"
dependencies = [
"crossbeam-deque",
"globset",
"log",
"memchr",
"regex-automata",
"same-file",
"walkdir",
"winapi-util",
]
[[package]]
name = "image"
version = "0.25.8"
@ -1839,6 +1995,18 @@ dependencies = [
"cc",
]
[[package]]
name = "libgit2-sys"
version = "0.17.0+1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224"
dependencies = [
"cc",
"libc",
"libz-sys",
"pkg-config",
]
[[package]]
name = "libm"
version = "0.2.15"
@ -1855,6 +2023,18 @@ dependencies = [
"libc",
]
[[package]]
name = "libz-sys"
version = "1.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
@ -1867,6 +2047,12 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "litemap"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
[[package]]
name = "litrs"
version = "0.4.2"
@ -2604,6 +2790,15 @@ dependencies = [
"universal-hash",
]
[[package]]
name = "potential_utf"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
dependencies = [
"zerovec",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
@ -3444,6 +3639,12 @@ dependencies = [
"sha2",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "static_assertions"
version = "1.1.0"
@ -3495,6 +3696,17 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syntect"
version = "5.3.0"
@ -3642,6 +3854,16 @@ dependencies = [
"time-core",
]
[[package]]
name = "tinystr"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "tokio"
version = "1.47.1"
@ -3933,12 +4155,30 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
@ -3972,6 +4212,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vergen"
version = "9.0.6"
@ -4554,6 +4800,12 @@ version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "writeable"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]]
name = "wyz"
version = "0.5.1"
@ -4838,7 +5090,9 @@ dependencies = [
"core-foundation-sys",
"dirs",
"foldhash 0.2.0",
"git2",
"hashbrown 0.16.0",
"ignore",
"libc",
"objc",
"parking_lot",
@ -5086,6 +5340,30 @@ dependencies = [
"yazi-term",
]
[[package]]
name = "yoke"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerocopy"
version = "0.8.27"
@ -5106,12 +5384,66 @@ dependencies = [
"syn",
]
[[package]]
name = "zerofrom"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zerotrie"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
]
[[package]]
name = "zerovec"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zune-core"
version = "0.4.12"

View file

@ -30,29 +30,57 @@ crossterm = { version = "0.29.0", features = [ "event-stream" ] }
dirs = "6.0.0"
foldhash = "0.2.0"
futures = "0.3.31"
git2 = { version = "0.19.0", default-features = false, features = [
"vendored-libgit2",
] }
globset = "0.4.16"
hashbrown = { version = "0.16.0", features = ["serde"] }
ignore = "0.4.23"
indexmap = { version = "2.11.4", features = ["serde"] }
libc = "0.2.177"
lru = "0.16.1"
mlua = { version = "0.11.4", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serde" ] }
mlua = { version = "0.11.4", features = [
"anyhow",
"async",
"error-send",
"lua54",
"macros",
"serde",
] }
objc = "0.2.7"
ordered-float = { version = "5.1.0", features = ["serde"] }
parking_lot = "0.12.5"
paste = "1.0.15"
percent-encoding = "2.3.2"
ratatui = { version = "0.29.0", features = [ "unstable-rendered-line-info", "unstable-widget-ref" ] }
ratatui = { version = "0.29.0", features = [
"unstable-rendered-line-info",
"unstable-widget-ref",
] }
regex = "1.12.1"
russh = { version = "0.54.5", default-features = false, features = [ "ring", "rsa" ] }
russh = { version = "0.54.5", default-features = false, features = [
"ring",
"rsa",
] }
scopeguard = "1.2.0"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
syntect = { version = "5.3.0", default-features = false, features = [
"parsing",
"plist-load",
"regex-onig",
] }
tokio = { version = "1.47.1", features = ["full"] }
tokio-stream = "0.1.17"
tokio-util = "0.7.16"
toml = { version = "0.9.8" }
tracing = { version = "0.1.41", features = [ "max_level_debug", "release_max_level_debug" ] }
twox-hash = { version = "2.1.2", default-features = false, features = [ "std", "random", "xxhash3_128" ] }
tracing = { version = "0.1.41", features = [
"max_level_debug",
"release_max_level_debug",
] }
twox-hash = { version = "2.1.2", default-features = false, features = [
"std",
"random",
"xxhash3_128",
] }
unicode-width = { version = "0.2.0", default-features = false }
uzers = "0.12.1"

View file

@ -60,6 +60,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!());

View file

@ -0,0 +1,155 @@
use anyhow::Result;
use yazi_config::YAZI;
use yazi_core::tab::Folder;
use yazi_fs::{FolderStage, IgnoreFilter};
use yazi_macro::{act, render, render_and, succ};
use yazi_parser::VoidOpt;
use yazi_shared::{data::Data, url::UrlLike};
use crate::{Actor, Ctx};
pub struct Ignore;
impl Actor for Ignore {
type Options = VoidOpt;
const NAME: &str = "ignore";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let gitignore_enabled = YAZI.mgr.gitignore_enable;
let override_patterns = &YAZI.mgr.ignore_override;
// If gitignore is disabled but we have override patterns, apply them
if !gitignore_enabled && !override_patterns.is_empty() {
// Load ignore filter from override patterns only
let ignore_filter = if let Some(path) = cx.cwd().as_path() {
IgnoreFilter::from_patterns(path, override_patterns)
} else {
None
};
let hovered = cx.hovered().map(|f| f.urn().to_owned());
let apply = |f: &mut Folder, filter: Option<IgnoreFilter>| {
// Always set the filter, even when loading
let changed = f.files.set_ignore_filter(filter);
if f.stage == FolderStage::Loading {
render!();
false
} else {
render_and!(changed && f.files.catchup_revision())
}
};
// Apply to CWD
if apply(cx.current_mut(), ignore_filter.clone()) {
act!(mgr:hover, cx)?;
act!(mgr:update_paged, cx)?;
}
// Apply to hovered
if let Some(h) = cx.hovered_folder_mut() {
let hovered_filter = if let Some(path) = h.url.as_path() {
IgnoreFilter::from_patterns(path, override_patterns)
} else {
None
};
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()) {
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;
}
}
succ!();
}
// If gitignore is disabled and no override patterns, remove any ignore filter
if !gitignore_enabled {
let hovered = cx.hovered().map(|f| f.urn().to_owned());
let apply = |f: &mut Folder| {
// Always clear the filter, even when loading
let changed = f.files.set_ignore_filter(None);
if f.stage == FolderStage::Loading {
render!();
false
} else {
render_and!(changed && f.files.catchup_revision())
}
};
// Apply to CWD and parent
if let (a, Some(b)) = (apply(cx.current_mut()), cx.parent_mut().map(apply))
&& (a | b)
{
act!(mgr:hover, cx)?;
act!(mgr:update_paged, cx)?;
}
// Apply to hovered
if let Some(h) = cx.hovered_folder_mut()
&& apply(h)
{
render!(h.repos(None));
act!(mgr:peek, cx, true)?;
} else if hovered.as_deref() != cx.hovered().map(|f| f.urn()) {
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;
}
succ!();
}
// Load ignore filter from the current directory
let ignore_filter = if let Some(path) = cx.cwd().as_path() {
IgnoreFilter::from_dir(path, override_patterns)
} else {
None
};
let hovered = cx.hovered().map(|f| f.urn().to_owned());
let apply = |f: &mut Folder, filter: Option<IgnoreFilter>| {
// Always set the filter, even when loading, so files are filtered as they
// arrive
let changed = f.files.set_ignore_filter(filter);
if f.stage == FolderStage::Loading {
render!();
false
} else {
render_and!(changed && f.files.catchup_revision())
}
};
// Apply to CWD
if apply(cx.current_mut(), ignore_filter.clone()) {
act!(mgr:hover, cx)?;
act!(mgr:update_paged, cx)?;
}
// Apply to parent (they should use their own ignore file, so we don't apply the
// same filter) Parent folders will be updated when they become the current
// directory
// Apply to hovered
if let Some(h) = cx.hovered_folder_mut() {
// Load ignore filter for hovered directory if it's a directory
let hovered_filter = if let Some(path) = h.url.as_path() {
IgnoreFilter::from_dir(path, override_patterns)
} else {
None
};
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()) {
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;
}
}
succ!();
}
}

View file

@ -19,6 +19,7 @@ yazi_macro::mod_flat!(
hardlink
hidden
hover
ignore
leave
linemode
link

View file

@ -23,6 +23,9 @@ 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 {

View file

@ -24,6 +24,12 @@ pub struct Mgr {
pub scrolloff: SyncCell<u8>,
pub mouse_events: SyncCell<MouseEvents>,
pub title_format: String,
// Filtering
#[serde(default)]
pub gitignore_enable: bool,
#[serde(default)]
pub ignore_override: Vec<String>,
}
impl Mgr {

View file

@ -32,6 +32,7 @@ pub enum Spark<'a> {
Forward(yazi_parser::VoidOpt),
Hardlink(yazi_parser::mgr::HardlinkOpt),
Hidden(yazi_parser::mgr::HiddenOpt),
Ignore(yazi_parser::VoidOpt),
Hover(yazi_parser::mgr::HoverOpt),
Leave(yazi_parser::VoidOpt),
Linemode(yazi_parser::mgr::LinemodeOpt),
@ -155,6 +156,7 @@ impl<'a> IntoLua for Spark<'a> {
Self::Forward(b) => b.into_lua(lua),
Self::Hardlink(b) => b.into_lua(lua),
Self::Hidden(b) => b.into_lua(lua),
Self::Ignore(b) => b.into_lua(lua),
Self::Hover(b) => b.into_lua(lua),
Self::Leave(b) => b.into_lua(lua),
Self::Linemode(b) => b.into_lua(lua),
@ -253,6 +255,7 @@ try_from_spark!(
mgr:escape_visual,
mgr:follow,
mgr:forward,
mgr:ignore,
mgr:leave,
mgr:refresh,
mgr:search_stop,

View file

@ -111,6 +111,7 @@ impl<'a> Executor<'a> {
on!(copy);
on!(shell);
on!(hidden);
on!(ignore);
on!(linemode);
on!(search);
on!(search_do);

View file

@ -20,7 +20,9 @@ arc-swap = "1.7.1"
bitflags = { workspace = true }
dirs = { workspace = true }
foldhash = { workspace = true }
git2 = { workspace = true }
hashbrown = { workspace = true }
ignore = { workspace = true }
parking_lot = { workspace = true }
percent-encoding = { workspace = true }
regex = { workspace = true }

View file

@ -3,7 +3,7 @@ use std::{mem, ops::{Deref, DerefMut, Not}};
use hashbrown::{HashMap, HashSet};
use yazi_shared::{Id, url::{Urn, UrnBuf}};
use super::{FilesSorter, Filter};
use super::{FilesSorter, Filter, IgnoreFilter};
use crate::{FILES_TICKET, File, SortBy};
#[derive(Default)]
@ -19,6 +19,7 @@ pub struct Files {
sorter: FilesSorter,
filter: Option<Filter>,
show_hidden: bool,
ignore_filter: Option<IgnoreFilter>,
}
impl Deref for Files {
@ -257,9 +258,15 @@ impl Files {
fn split_files(&self, files: impl IntoIterator<Item = File>) -> (Vec<File>, Vec<File>) {
if let Some(filter) = &self.filter {
files.into_iter().partition(|f| {
(f.is_hidden() && !self.show_hidden)
|| !filter.matches(f.urn())
|| self.ignore_filter.as_ref().is_some_and(|ig| ig.matches_url(&f.url))
})
} else if let Some(ignore_filter) = &self.ignore_filter {
files
.into_iter()
.partition(|f| (f.is_hidden() && !self.show_hidden) || !filter.matches(f.urn()))
.partition(|f| (f.is_hidden() && !self.show_hidden) || ignore_filter.matches_url(&f.url))
} else if self.show_hidden {
(vec![], files.into_iter().collect())
} else {
@ -316,6 +323,24 @@ impl Files {
true
}
// --- Ignore filter
#[inline]
pub fn ignore_filter(&self) -> Option<&IgnoreFilter> { self.ignore_filter.as_ref() }
pub fn set_ignore_filter(&mut self, ignore_filter: Option<IgnoreFilter>) -> bool {
if self.ignore_filter.is_none() && ignore_filter.is_none() {
return false;
}
self.ignore_filter = ignore_filter;
// Re-split files with the new ignore filter
let it = mem::take(&mut self.items).into_iter().chain(mem::take(&mut self.hidden));
(self.hidden, self.items) = self.split_files(it);
self.sorter.sort(&mut self.items, &self.sizes);
true
}
// --- Show hidden
pub fn set_show_hidden(&mut self, state: bool) {
if self.show_hidden == state {

158
yazi-fs/src/ignore.rs Normal file
View file

@ -0,0 +1,158 @@
use std::{collections::HashSet, path::{Path, PathBuf}};
use yazi_shared::url::AsUrl;
#[derive(Clone, Debug)]
pub struct IgnoreFilter {
ignored_paths: HashSet<PathBuf>,
gitignore: Option<ignore::gitignore::Gitignore>,
}
impl IgnoreFilter {
/// Create a new IgnoreFilter by checking git ignore status for files in the
/// given directory
pub fn from_dir(dir: impl AsRef<Path>, override_patterns: &[String]) -> Option<Self> {
let dir = dir.as_ref();
// Try to open the git repository for this directory
let repo = git2::Repository::discover(dir).ok()?;
// Get the workdir (root of the git repository)
let workdir = repo.workdir()?;
// Get git statuses for the repository
let statuses = match repo.statuses(None) {
Ok(s) => s,
Err(_) => return None,
};
// Build a set of ALL ignored paths from git status
let mut ignored_paths = HashSet::new();
// Manually add .git directory as ignored (like eza does)
ignored_paths.insert(workdir.join(".git"));
// Add all ignored files from git status
for status in statuses.iter() {
if status.status() == git2::Status::IGNORED {
if let Some(path) = status.path() {
ignored_paths.insert(workdir.join(path));
}
}
}
// Build custom gitignore from override patterns if provided
let gitignore = if !override_patterns.is_empty() {
let mut builder = ignore::gitignore::GitignoreBuilder::new(workdir);
// Add each override pattern
for pattern in override_patterns {
let _ = builder.add_line(None, pattern);
}
builder.build().ok()
} else {
None
};
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 override patterns, still create
// the filter
if gitignore.is_some() {
return Some(Self { ignored_paths, gitignore });
}
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 })
}
/// Create a new IgnoreFilter from only override patterns (no git integration)
pub fn from_patterns(dir: impl AsRef<Path>, patterns: &[String]) -> Option<Self> {
if patterns.is_empty() {
return None;
}
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);
}
let gitignore = builder.build().ok()?;
Some(Self { ignored_paths: HashSet::new(), gitignore: Some(gitignore) })
}
/// Check if a file should be ignored based on its URL
pub fn matches_url(&self, url: impl AsUrl) -> bool {
let url = url.as_url();
let path = url.loc.as_path();
// First 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());
match matched {
ignore::Match::None => {
// No override pattern matched, fall through to git ignore check
}
ignore::Match::Ignore(_) => {
// Override pattern says to ignore
return true;
}
ignore::Match::Whitelist(_) => {
// Override pattern says NOT to ignore (negation pattern like !target/)
// This takes precedence over git ignore
return false;
}
}
}
// Check exact match first
if self.ignored_paths.contains(path) {
return true;
}
// Check if any parent directory is ignored
// BUT also check if parent is whitelisted by override patterns
let mut current = path;
while let Some(parent) = current.parent() {
if self.ignored_paths.contains(parent) {
// Parent is ignored by git, but check if override patterns whitelist it
if let Some(ref gitignore) = self.gitignore {
let matched = gitignore.matched(parent, true); // parent is always a directory
if matches!(matched, ignore::Match::Whitelist(_)) {
// Parent is whitelisted, so children should not be ignored
return false;
}
}
return true;
}
current = parent;
}
false
}
/// Check if a path should be ignored
pub fn matches_path(&self, path: &Path) -> bool { self.ignored_paths.contains(path) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_git_repo_discovery() {
// This test assumes we're running in the yazi git repo
let current_dir = std::env::current_dir().unwrap();
assert!(git2::Repository::discover(&current_dir).is_ok());
}
}

View file

@ -2,7 +2,7 @@
yazi_macro::mod_pub!(cha error mounts path provider);
yazi_macro::mod_flat!(cwd file files filter fns hash op sorter sorting splatter stage url xdg);
yazi_macro::mod_flat!(cwd file files filter fns hash ignore op sorter sorting splatter stage url xdg);
pub fn init() {
CWD.init(<_>::default());