diff --git a/Cargo.lock b/Cargo.lock index 5e461a0..9f2a0a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -823,15 +823,6 @@ dependencies = [ "vsimd", ] -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -858,9 +849,6 @@ name = "bitflags" version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" -dependencies = [ - "serde_core", -] [[package]] name = "bitvec" @@ -1261,15 +1249,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" -[[package]] -name = "crossbeam-channel" -version = "0.5.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" version = "0.8.7" @@ -1289,15 +1268,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-queue" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.22" @@ -1510,27 +1480,6 @@ dependencies = [ "crypto-common 0.2.2", ] -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.48.0", -] - [[package]] name = "displaydoc" version = "0.2.7" @@ -1551,15 +1500,6 @@ dependencies = [ "litrs", ] -[[package]] -name = "doxygen-rs" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "415b6ec780d34dcf624666747194393603d0373b7141eef01d12ee58881507d9" -dependencies = [ - "phf", -] - [[package]] name = "dunce" version = "1.0.5" @@ -1694,83 +1634,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "fff-grep" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca0bd4b73cde171bf11165944fe7104af88fb5fc761fb355a469f7e0b18f7aa" -dependencies = [ - "bstr", - "memchr", -] - -[[package]] -name = "fff-notify-debouncer-full" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29a4ebea7b8a2840cd59358bbf396f6f04313ce8eae84ac79703ce80298b8731" -dependencies = [ - "file-id", - "log", - "notify", - "notify-types", - "rustc-hash", - "walkdir", -] - -[[package]] -name = "fff-query-parser" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7e476806b7e5999639b68970054c42d38d7fa430ea60118f67a05c0dd635fbd" - -[[package]] -name = "fff-search" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fbb4c71c1d716fa1ddf7efbb7f62f5ef5a439110a4029c7dc743e54fe1e0417" -dependencies = [ - "ahash", - "aho-corasick", - "blake3", - "dirs", - "dunce", - "fff-grep", - "fff-notify-debouncer-full", - "fff-query-parser", - "git2", - "glidesort", - "globset", - "heed", - "ignore", - "libc", - "memchr", - "memmap2", - "neo_frizbee", - "notify", - "parking_lot", - "pathdiff", - "rayon", - "regex", - "regex-syntax", - "serde", - "signal-hook-registry", - "smallvec", - "thiserror 2.0.18", - "tracing", - "tracing-appender", - "tracing-subscriber", -] - -[[package]] -name = "file-id" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fc6a637b6dc58414714eddd9170ff187ecb0933d4c7024d1abbd23a3cc26e9" -dependencies = [ - "windows-sys 0.60.2", -] - [[package]] name = "filetime" version = "0.2.29" @@ -2039,24 +1902,6 @@ dependencies = [ "weezl", ] -[[package]] -name = "git2" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddddbf932745a6be37109b6112d3ee09696106f848449069d3a57bba937ab82e" -dependencies = [ - "bitflags 2.13.1", - "libc", - "libgit2-sys", - "log", -] - -[[package]] -name = "glidesort" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2e102e6eb644d3e0b186fc161e4460417880a0a0b87d235f2e5b8fb30f2e9e0" - [[package]] name = "globset" version = "0.4.20" @@ -2123,44 +1968,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "heed" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad82d6598ccf1dac15c8b758a1bd282b755b6776be600429176757190a1b0202" -dependencies = [ - "bitflags 2.13.1", - "byteorder", - "heed-traits", - "heed-types", - "libc", - "lmdb-master-sys", - "once_cell", - "page_size", - "serde", - "synchronoise", - "url", -] - -[[package]] -name = "heed-traits" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3130048d404c57ce5a1ac61a903696e8fcde7e8c2991e9fcfc1f27c3ef74ff" - -[[package]] -name = "heed-types" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c255bdf46e07fb840d120a36dcc81f385140d7191c76a7391672675c01a55d" -dependencies = [ - "bincode", - "byteorder", - "heed-traits", - "serde", - "serde_json", -] - [[package]] name = "hermit-abi" version = "0.5.2" @@ -2539,26 +2346,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "inotify" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592" -dependencies = [ - "bitflags 2.13.1", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" -dependencies = [ - "libc", -] - [[package]] name = "inout" version = "0.1.4" @@ -2867,17 +2654,18 @@ dependencies = [ "bytes", "clap", "crossterm", - "fff-search", "flate2", "futures-util", "getrandom 0.4.3", "hyper", "hyper-util", + "ignore", "image", "jsonschema", "jsonwebtoken", "keyring", "libc", + "neo_frizbee", "opentelemetry 0.32.0", "opentelemetry-otlp", "opentelemetry_sdk", @@ -2909,26 +2697,6 @@ dependencies = [ "zip", ] -[[package]] -name = "kqueue" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" -dependencies = [ - "bitflags 2.13.1", - "libc", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -2941,45 +2709,12 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" -[[package]] -name = "libgit2-sys" -version = "0.18.8+1.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7c568b25d7489bc3fb2988ed69ab111d2944d2f5fec3d5c987fe545ea97b50" -dependencies = [ - "cc", - "libc", - "libz-sys", - "pkg-config", -] - [[package]] name = "libm" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" -[[package]] -name = "libredox" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d8f1ea3f21fd3405dcaf6c9b5c1630af9afc422d9073ea39c5f6d6c772e08ed" -dependencies = [ - "libc", -] - -[[package]] -name = "libz-sys" -version = "1.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "line-clipping" version = "0.3.8" @@ -3013,17 +2748,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" -[[package]] -name = "lmdb-master-sys" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaeb9bd22e73bd1babffff614994b341e9b2008de7bb73bf1f7e9154f1978f8b" -dependencies = [ - "cc", - "doxygen-rs", - "libc", -] - [[package]] name = "lock_api" version = "0.4.14" @@ -3054,15 +2778,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - [[package]] name = "matchit" version = "0.8.4" @@ -3075,15 +2790,6 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" -[[package]] -name = "memmap2" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" -dependencies = [ - "libc", -] - [[package]] name = "memoffset" version = "0.9.1" @@ -3171,44 +2877,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" -[[package]] -name = "notify" -version = "9.0.0-rc.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7768eb7201a1964d8eb10224e850df046ab584ee3c3d26e340a993d81f748148" -dependencies = [ - "bitflags 2.13.1", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "notify-types", - "objc2-core-foundation", - "objc2-core-services", - "walkdir", - "windows-sys 0.61.2", - "xxhash-rust", -] - -[[package]] -name = "notify-types" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "num" version = "0.4.3" @@ -3322,25 +2990,6 @@ dependencies = [ "url", ] -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "objc2-core-services" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583300ad934cba24ff5292aee751ecc070f7ca6b39a574cc21b7b5e588e06a0b" -dependencies = [ - "libc", - "objc2-core-foundation", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -3452,12 +3101,6 @@ dependencies = [ "tokio-stream", ] -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - [[package]] name = "ordered-float" version = "5.5.0" @@ -3483,16 +3126,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" -[[package]] -name = "page_size" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "palette" version = "0.7.7" @@ -3556,12 +3189,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - [[package]] name = "pem" version = "3.0.6" @@ -3578,48 +3205,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared", - "rand 0.8.8", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - [[package]] name = "pin-project" version = "1.1.13" @@ -4109,26 +3694,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "rayon" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - [[package]] name = "redox_syscall" version = "0.5.18" @@ -4138,17 +3703,6 @@ dependencies = [ "bitflags 2.13.1", ] -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 1.0.69", -] - [[package]] name = "ref-cast" version = "1.0.27" @@ -4864,12 +4418,6 @@ dependencies = [ "time", ] -[[package]] -name = "siphasher" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" - [[package]] name = "slab" version = "0.4.12" @@ -4950,12 +4498,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "symlink" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" - [[package]] name = "syn" version = "2.0.119" @@ -4987,15 +4529,6 @@ dependencies = [ "futures-core", ] -[[package]] -name = "synchronoise" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dbc01390fc626ce8d1cffe3376ded2b72a11bb70e1c75f404a210e4daa4def2" -dependencies = [ - "crossbeam-queue", -] - [[package]] name = "synstructure" version = "0.13.2" @@ -5395,19 +4928,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-appender" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" -dependencies = [ - "crossbeam-channel", - "symlink", - "thiserror 2.0.18", - "time", - "tracing-subscriber", -] - [[package]] name = "tracing-attributes" version = "0.1.31" @@ -5429,17 +4949,6 @@ dependencies = [ "valuable", ] -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - [[package]] name = "tracing-opentelemetry" version = "0.32.1" @@ -5474,16 +4983,9 @@ version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", "sharded-slab", - "smallvec", "thread_local", - "tracing", "tracing-core", - "tracing-log", ] [[package]] @@ -5644,12 +5146,6 @@ 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 = "version_check" version = "0.9.5" @@ -6297,12 +5793,6 @@ dependencies = [ "tap", ] -[[package]] -name = "xxhash-rust" -version = "0.8.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6" - [[package]] name = "yoke" version = "0.8.3" diff --git a/Cargo.toml b/Cargo.toml index 5db04b9..b057ca1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ bytes = "=1.12.1" clap = { version = "=4.6.6", features = ["derive"] } crossterm = { version = "=0.29.0", features = ["event-stream"] } flate2 = "=1.1.10" -fff-search = { version = "=0.10.6", default-features = false, features = ["ripgrep"] } +ignore = "=0.4.33" futures-util = "=0.3.34" getrandom = "=0.4.3" hyper = "=1.11.1" @@ -45,6 +45,7 @@ image = { version = "=0.25.10", default-features = false, features = ["gif", "jp jsonwebtoken = { version = "=11.0.0", default-features = false, features = ["aws_lc_rs"] } keyring = { version = "=4.1.6", default-features = false, features = ["v1"] } jsonschema = { version = "=0.52.0", default-features = false, features = ["idna"] } +neo_frizbee = "=0.11.0" opentelemetry = { version = "=0.32.0", default-features = false, features = ["trace"] } opentelemetry-otlp = { version = "=0.32.0", default-features = false, features = ["grpc-tonic", "http-proto", "http-json", "reqwest-client", "tls-webpki-roots", "trace"] } opentelemetry_sdk = { version = "=0.32.1", default-features = false, features = ["experimental_trace_batch_span_processor_with_async_runtime", "rt-tokio", "trace"] } diff --git a/src/file_search.rs b/src/file_search.rs index 215440b..9f5e3fc 100644 --- a/src/file_search.rs +++ b/src/file_search.rs @@ -4,14 +4,75 @@ use std::{ sync::{Arc, Mutex}, }; -use fff_search::{ - FFFMode, FilePicker, FilePickerOptions, FuzzySearchOptions, PaginationArgs, ParserConfig, - QueryParser, -}; +use ignore::{WalkBuilder, WalkState}; +use neo_frizbee::{Config, match_list, match_list_indices}; use serde::{Deserialize, Serialize}; const MAX_RESULTS: usize = 100; +// Preserve fff-search’s non-Git safeguards for home and other broad roots. +const NON_GIT_IGNORED_DIRS: &[&str] = &[ + // various dev tools that can be meet in the developer app + "node_modules", + "__pycache__", + "venv", + ".venv", + "target/debug", + "target/release", + "target/rust-analyzer", + "target/criterion", + // Language package caches in non-git roots. + "go/pkg/mod", + ".cargo/registry", + ".rustup/toolchains", + ".gradle/caches", + ".m2/repository", + ".npm/_cacache", + ".pub-cache", + #[cfg(not(target_os = "windows"))] + ".local/state", // this contains tons of logs which generate too much watcher noise + #[cfg(target_os = "macos")] + "Library/Application Support", + #[cfg(target_os = "macos")] + "Library/Caches", + #[cfg(target_os = "macos")] + "Library/Containers", // sandboxed apps data + #[cfg(target_os = "macos")] + "Library/Group Containers", // random application data and networking + #[cfg(target_os = "macos")] + "Library/pnpm", + #[cfg(target_os = "macos")] + "Library/Metadata", + #[cfg(target_os = "macos")] + "Library/Developer/CoreSimulator", + #[cfg(target_os = "macos")] + "Library/Android", + #[cfg(target_os = "macos")] + "Library/Logs", + #[cfg(target_os = "macos")] + "Library/Daemon Containers", + #[cfg(target_os = "macos")] + "Library/Trial", + #[cfg(target_os = "macos")] + "Library/Preferences", + #[cfg(target_os = "macos")] + "Library/Messages", + #[cfg(target_os = "macos")] + "Library/IdentityServices", + #[cfg(target_os = "windows")] + "bin/Debug", + #[cfg(target_os = "windows")] + "bin/Release", + #[cfg(target_os = "windows")] + "Program Files", + #[cfg(target_os = "windows")] + "Program Files (x86)", + #[cfg(target_os = "windows")] + "AppData/Local", + #[cfg(target_os = "windows")] + "AppData/Roaming", +]; + #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] pub struct FileMatch { pub relative_path: String, @@ -19,7 +80,7 @@ pub struct FileMatch { } pub struct WorkspaceFileSearch { - picker: FilePicker, + files: Vec, } pub struct WorkspaceFileSearchState { @@ -27,39 +88,6 @@ pub struct WorkspaceFileSearchState { search: WorkspaceFileSearch, } -#[derive(Debug, Clone, Copy)] -struct LiteralPathConfig; - -impl ParserConfig for LiteralPathConfig { - fn enable_glob(&self) -> bool { - false - } - - fn enable_extension(&self) -> bool { - false - } - - fn enable_exclude(&self) -> bool { - false - } - - fn enable_path_segments(&self) -> bool { - false - } - - fn enable_type_filter(&self) -> bool { - false - } - - fn enable_git_status(&self) -> bool { - false - } - - fn enable_location(&self) -> bool { - false - } -} - impl WorkspaceFileSearch { pub fn start(root: PathBuf) -> Result { if !root.is_dir() { @@ -68,72 +96,153 @@ impl WorkspaceFileSearch { root.display() )); } - let base_path = root - .into_os_string() - .into_string() - .map_err(|_| "workspace root is not valid UTF-8".to_string())?; - let mut picker = FilePicker::new(FilePickerOptions { - base_path, - mode: FFFMode::Ai, - watch: false, - enable_home_dir_scanning: true, - ..FilePickerOptions::default() - }) - .map_err(|error| format!("could not start workspace file index: {error}"))?; - // The synchronous path-only scan avoids fff-search's background - // content-classification pass and cannot overlap a replacement scan. - picker - .collect_files() - .map_err(|error| format!("could not scan workspace files: {error}"))?; - Ok(Self { picker }) + if root.parent().is_none() { + return Err(format!( + "refusing to index filesystem root: {}", + root.display() + )); + } + if root.to_str().is_none() { + return Err("workspace root is not valid UTF-8".to_string()); + } + + let is_git_repo = root + .ancestors() + .any(|ancestor| ancestor.join(".git").exists()); + let mut builder = WalkBuilder::new(&root); + builder + .hidden(!is_git_repo) + .git_ignore(true) + .git_exclude(true) + .git_global(true) + .ignore(true) + .follow_links(false) + .filter_entry(|entry| entry.file_name() != ".git"); + + if !is_git_repo { + let mut overrides = ignore::overrides::OverrideBuilder::new(&root); + for dir in NON_GIT_IGNORED_DIRS { + overrides + .add(&format!("!**/{dir}/")) + .map_err(|error| format!("invalid non-Git exclusion: {error}"))?; + } + builder.overrides( + overrides + .build() + .map_err(|error| format!("could not build non-Git exclusions: {error}"))?, + ); + } + + let files = Mutex::new(Vec::new()); + let scan_error = Mutex::new(None); + builder.build_parallel().run(|| { + let files = &files; + let scan_error = &scan_error; + let root = &root; + Box::new(move |result| { + let entry = match result { + Ok(entry) if entry.error().is_none() => entry, + result => { + let error = match result { + Ok(entry) => entry.error().expect("entry has an error").to_string(), + Err(error) => error.to_string(), + }; + scan_error + .lock() + .expect("scan error mutex poisoned") + .get_or_insert(error); + return WalkState::Quit; + } + }; + if !entry.file_type().is_some_and(|kind| kind.is_file()) { + return WalkState::Continue; + } + let Ok(relative) = entry.path().strip_prefix(root) else { + return WalkState::Continue; + }; + let relative_path = relative + .iter() + .map(|part| part.to_string_lossy()) + .collect::>() + .join("/"); + files + .lock() + .expect("file list mutex poisoned") + .push(relative_path); + WalkState::Continue + }) + }); + if let Some(error) = scan_error + .into_inner() + .map_err(|error| format!("could not collect workspace scan error: {error}"))? + { + return Err(format!("could not scan workspace files: {error}")); + } + let mut files = files + .into_inner() + .map_err(|error| format!("could not collect workspace files: {error}"))?; + files.sort_unstable(); + Ok(Self { files }) } pub fn search(&self, query: &str) -> Result, String> { - let picker = &self.picker; - let parsed = QueryParser::new(LiteralPathConfig).parse(query); - let results = picker.fuzzy_search( - &parsed, - None, - FuzzySearchOptions { - pagination: PaginationArgs { - offset: 0, - limit: MAX_RESULTS, - }, - ..FuzzySearchOptions::default() - }, - ); + if query.is_empty() { + return Ok(self + .files + .iter() + .take(MAX_RESULTS) + .cloned() + .map(|relative_path| FileMatch { + relative_path, + match_byte_offsets: Vec::new(), + }) + .collect()); + } + + let config = Config { + max_typos: Some((query.len() as u16 / 4).clamp(2, 6)), + ..Config::default() + }; + let candidates: Vec<_> = match_list(query, &self.files, &config) + .into_iter() + .take(MAX_RESULTS) + .filter_map(|matched| self.files.get(matched.index as usize)) + .map(String::as_str) + .collect(); - Ok(results - .items + Ok(match_list_indices(query, &candidates, &config) .into_iter() - .zip(results.match_byte_offsets) - .map(|(item, offsets)| { - let relative_path = item.relative_path(picker); - let mut match_byte_offsets: Vec<_> = offsets - .into_iter() - .filter_map(|(start, end)| { - let range = start as usize..end as usize; - (range.start < range.end - && relative_path.is_char_boundary(range.start) - && relative_path.is_char_boundary(range.end)) - .then_some(range) - }) - .collect(); - match_byte_offsets.sort_by_key(|range| (range.start, range.end)); - let mut normalized: Vec> = Vec::new(); - for range in match_byte_offsets { - if let Some(previous) = normalized.last_mut() - && range.start <= previous.end + .filter_map(|matched| { + let relative_path = candidates.get(matched.index as usize)?.to_string(); + let mut offsets = matched.indices; + offsets.sort_unstable(); + let mut match_byte_offsets: Vec> = Vec::new(); + for byte_index in offsets { + if byte_index >= relative_path.len() { + continue; + } + // neo_frizbee returns byte offsets, including continuation bytes. + // Expand each one to the containing character before merging. + let mut start = byte_index; + while !relative_path.is_char_boundary(start) { + start -= 1; + } + let mut end = byte_index + 1; + while !relative_path.is_char_boundary(end) { + end += 1; + } + if let Some(previous) = match_byte_offsets.last_mut() + && start <= previous.end { - previous.end = previous.end.max(range.end); + previous.end = previous.end.max(end); } else { - normalized.push(range); + match_byte_offsets.push(start..end); } } - FileMatch { + Some(FileMatch { relative_path, - match_byte_offsets: normalized, - } + match_byte_offsets, + }) }) .collect()) } @@ -249,10 +358,59 @@ mod tests { let unicode = search.search("caf\u{e9}").expect("Unicode query"); assert_eq!(unicode[0].relative_path, "src/caf\u{e9}.rs"); - assert!(unicode[0].match_byte_offsets.iter().all(|range| { - unicode[0].relative_path.is_char_boundary(range.start) - && unicode[0].relative_path.is_char_boundary(range.end) - })); + assert_eq!(unicode[0].match_byte_offsets, vec![4..9]); + } + + #[test] + fn unicode_highlights_use_byte_offsets() { + let search = WorkspaceFileSearch { + files: vec!["é.rs".into()], + }; + for (query, expected) in [("é", 0..2), ("rs", 3..5), ("é.rs", 0..5)] { + let results = search.search(query).expect("Unicode search"); + assert_eq!(results.len(), 1, "query: {query}"); + assert_eq!(results[0].match_byte_offsets.len(), 1, "query: {query}"); + assert_eq!(results[0].match_byte_offsets[0], expected, "query: {query}"); + } + } + + #[test] + fn non_git_roots_exclude_machine_state_but_keep_source() { + let workspace = tempfile::tempdir().expect("temporary non-Git workspace"); + for prefix in ["", "nested/"] { + for dir in NON_GIT_IGNORED_DIRS { + let directory = workspace.path().join(format!("{prefix}{dir}")); + fs::create_dir_all(&directory).expect("create excluded directory"); + fs::write(directory.join("excluded.txt"), "state").expect("write state"); + } + } + let sources = [ + "dev/project/src/main.rs", + "dev/myproj/pkg/mod/thing.go", + "Documents/notes/todo.md", + "target/source.rs", + "node_modules_backup/source.js", + ]; + for source in sources { + let path = workspace.path().join(source); + fs::create_dir_all(path.parent().unwrap()).expect("create source directory"); + fs::write(path, "source").expect("write source"); + } + let search = WorkspaceFileSearch::start(workspace.path().to_path_buf()) + .expect("start non-Git search"); + let mut expected: Vec<_> = sources.into_iter().map(String::from).collect(); + expected.sort(); + assert_eq!(search.files, expected); + } + + #[test] + fn git_roots_do_not_apply_non_git_exclusions() { + let workspace = workspace(); + fs::create_dir(workspace.path().join("node_modules")).expect("create directory"); + fs::write(workspace.path().join("node_modules/source.js"), "source").expect("write source"); + let search = + WorkspaceFileSearch::start(workspace.path().to_path_buf()).expect("start Git search"); + assert!(search.files.contains(&"node_modules/source.js".to_string())); } #[tokio::test] @@ -285,6 +443,27 @@ mod tests { ); } + #[test] + fn prunes_git_metadata_before_scanning() { + let workspace = workspace(); + // An invalid ignore rule would fail the scan if metadata were traversed. + fs::write(workspace.path().join(".git/.ignore"), "[z-a]\n") + .expect("write invalid metadata ignore rule"); + let search = WorkspaceFileSearch::start(workspace.path().to_path_buf()) + .expect("metadata must not be scanned"); + assert!(search.files.iter().all(|path| !path.starts_with(".git/"))); + } + + #[test] + fn reports_invalid_ignore_rules() { + let workspace = workspace(); + fs::write(workspace.path().join(".ignore"), "[z-a]\n").expect("write invalid ignore rule"); + let error = WorkspaceFileSearch::start(workspace.path().to_path_buf()) + .err() + .expect("invalid ignore rule must fail the scan"); + assert!(error.contains("could not scan workspace files"), "{error}"); + } + #[test] fn rejects_invalid_roots() { let workspace = tempfile::tempdir().expect("temporary workspace"); @@ -293,5 +472,6 @@ mod tests { assert!(WorkspaceFileSearch::start(file).is_err()); assert!(WorkspaceFileSearch::start(workspace.path().join("missing")).is_err()); + assert!(WorkspaceFileSearch::start(PathBuf::from(std::path::MAIN_SEPARATOR_STR)).is_err()); } }