mirror of
https://github.com/junegunn/fzf
synced 2026-06-13 10:33:18 +00:00
Compare commits
1937 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 290b18d9fe | |||
| ccedd064ca | |||
| d4352a013d | |||
| 665bef56ea | |||
| e912cdb3e4 | |||
| de1fca99d5 | |||
| 677e854850 | |||
| 67319aed0b | |||
| 367177d911 | |||
| 5819e5ff2f | |||
| fcc3c6acce | |||
| e0d081906f | |||
| 263eb4732f | |||
| b4a86a9c8a | |||
| 6fefe02546 | |||
| f783582561 | |||
| af42fde089 | |||
| 27dab2422e | |||
| 56be41218c | |||
| 7782da6c00 | |||
| 43f0508dd2 | |||
| 1986d101e0 | |||
| dd7a081b93 | |||
| d247284bc3 | |||
| 6f3e5de150 | |||
| 0142fe9d04 | |||
| 251b08b831 | |||
| 01567b4f06 | |||
| d754cfab87 | |||
| ef6eba1b89 | |||
| faa6a7e9f6 | |||
| 8b66489987 | |||
| 780a624ed7 | |||
| 1ad7c617b1 | |||
| 06e70570e2 | |||
| bd4a18d0a9 | |||
| 1b0d448c6e | |||
| f584cf38f7 | |||
| 0eb2ae9f8b | |||
| 1831500018 | |||
| 62899fd74e | |||
| d1cea64a0e | |||
| d7a132b8bf | |||
| 59b82670f7 | |||
| c516acaa89 | |||
| 11ff4745ad | |||
| 23164c2263 | |||
| 9f8294de62 | |||
| b7138e67a6 | |||
| 0f0751b3b6 | |||
| cc16a97a40 | |||
| 3ccf162cf0 | |||
| fff7eda9d7 | |||
| d198c0c9de | |||
| 55d5b153e6 | |||
| 57695b0309 | |||
| 6f17d49dbb | |||
| 87586ac5eb | |||
| 12be5e7b83 | |||
| 1acf980e95 | |||
| 14e3995a06 | |||
| 2202481705 | |||
| 6153004070 | |||
| 95f186f364 | |||
| 58b2855513 | |||
| a00df93e13 | |||
| 76efddd718 | |||
| b638ff46fb | |||
| 259e841a77 | |||
| f0a2f5ef14 | |||
| 2ae7367e8a | |||
| 6f33df755e | |||
| 2aec7d5201 | |||
| fc60406684 | |||
| cf57950301 | |||
| 48c4913392 | |||
| 17f2aa1a1f | |||
| b5f7221580 | |||
| e6b9a08699 | |||
| 8dbb3b352d | |||
| 9f422851fe | |||
| 7a811f0cb8 | |||
| b80059e21f | |||
| 26de195bbb | |||
| b59f27ef5a | |||
| f3ca0b1365 | |||
| a8e1ef0989 | |||
| 2f27a3ede2 | |||
| 9249ea1739 | |||
| 92bfe68c74 | |||
| 92dc40ea82 | |||
| 12a280ba14 | |||
| 0c6ead6e98 | |||
| 280a011f02 | |||
| d324580840 | |||
| f9830c5a3d | |||
| 95bc5b8f0c | |||
| 0b08f0dea0 | |||
| e7300fe300 | |||
| 260d160973 | |||
| d57ed157ad | |||
| 9226bc605d | |||
| eacef5ea6e | |||
| 96eb68ce63 | |||
| 50be8bc78e | |||
| b4e585779a | |||
| 97ac7794cf | |||
| 4866c34361 | |||
| 3cfee281b4 | |||
| 5887edc6ba | |||
| 3e751c4e87 | |||
| 8452c78cc8 | |||
| 2db14b4308 | |||
| 90c4269d4e | |||
| 6087055305 | |||
| 2f9df91171 | |||
| 12e24d368c | |||
| 55193ee4dc | |||
| ff6a3bbee0 | |||
| dce248ac6d | |||
| 0ff13dcfbe | |||
| 4d6a7757b8 | |||
| b9804f5873 | |||
| 98a3b1fff8 | |||
| 6df5ca17e8 | |||
| 09ca45f7db | |||
| 09fe3a4180 | |||
| b908f7a0ec | |||
| 1a50a3c082 | |||
| fefea8d885 | |||
| 385cccd362 | |||
| 4a684b6c78 | |||
| 4a195e6323 | |||
| 0ecbf3f475 | |||
| 4522868fc0 | |||
| 111a62f1ea | |||
| 33cac3f0e7 | |||
| 74e98cac5c | |||
| c338df02c4 | |||
| 69e9abdab4 | |||
| b6411beaa1 | |||
| b56d614ba2 | |||
| 49ab253555 | |||
| 0e859a18ed | |||
| 880dd20b18 | |||
| 91aa25c863 | |||
| 7e62b34087 | |||
| f9f0014c16 | |||
| ab3b9fef52 | |||
| 7d9724157c | |||
| bc8967632b | |||
| 6360c9261c | |||
| e653628458 | |||
| 28747e5cb6 | |||
| 9725eac314 | |||
| b389616030 | |||
| 25b2248f11 | |||
| 14564e4fc7 | |||
| d01eaa9de3 | |||
| aad805d0d3 | |||
| d1f037059a | |||
| 3f94bcb5bf | |||
| 3c7cbc9d47 | |||
| 28e2a067e5 | |||
| b29e2ee2d1 | |||
| 029b241dbb | |||
| d6ded42026 | |||
| 6eb4b41e34 | |||
| 14b5e1d88c | |||
| 603240122e | |||
| 8d688521fe | |||
| 775129367a | |||
| b3b221854b | |||
| c8cf0992c1 | |||
| 33d8d51c8a | |||
| b473477c22 | |||
| fcc4178bca | |||
| cfc37caabc | |||
| af2a81dc02 | |||
| be5a687281 | |||
| 771e35b972 | |||
| 60a5be1e65 | |||
| 1d5e87f5e4 | |||
| 3db63f5e52 | |||
| 2ab923f3ae | |||
| c3e6d9a8f9 | |||
| 2471edf3ff | |||
| 53a8aeeb72 | |||
| 60b35e748b | |||
| 3f499f055e | |||
| 1df99db0b2 | |||
| 535b610a6b | |||
| 91fab3b3c2 | |||
| b9f2bf64ff | |||
| 07d53cb7e4 | |||
| ead534a1be | |||
| 8a05083503 | |||
| e659b46ff5 | |||
| 991c36453c | |||
| 4d563c6dfa | |||
| 5cb695744f | |||
| c1b259c042 | |||
| 1a0371e2c7 | |||
| aa259fdc19 | |||
| b852dc8a56 | |||
| a0cabe021d | |||
| 8cdfb23df6 | |||
| 4ffde48e2f | |||
| f2b33f038a | |||
| d5913bf86e | |||
| 0e9026b817 | |||
| ab407c4645 | |||
| 91c4bef35f | |||
| bf77206221 | |||
| 0cb1be3f04 | |||
| 01cb38a5fb | |||
| c38c6cad79 | |||
| ba6fc40cfd | |||
| dd46a256c0 | |||
| d19ce0ad8d | |||
| ed7becfb47 | |||
| 9ace1351ff | |||
| e1de29bc40 | |||
| 0df7d10550 | |||
| 91e119a77e | |||
| 3984161f6c | |||
| 91beacf0f4 | |||
| e6ad01fb90 | |||
| ce2200e908 | |||
| 548061dbde | |||
| 8f0c91545d | |||
| 0eefcf348e | |||
| c1f8d18a0c | |||
| 8585969d6d | |||
| 8a943a9b1a | |||
| c87a8eccd4 | |||
| 65df0abf0e | |||
| b51bc6b50e | |||
| febaadbee5 | |||
| 0e67c5aa7a | |||
| 760d1b7c58 | |||
| 9bdacc8df2 | |||
| 8e936ecfa7 | |||
| db2e95b1f2 | |||
| 687074e772 | |||
| 3401c2e0c7 | |||
| e8cb315419 | |||
| f0c4ee4047 | |||
| de0df2422a | |||
| 148b0a94cd | |||
| ca294109c3 | |||
| 9cad2686e9 | |||
| 9a45172232 | |||
| 2a92c7d792 | |||
| f5975cf870 | |||
| a67aa85820 | |||
| c5cabe1691 | |||
| cbed41cd82 | |||
| 6684771cbf | |||
| f5f894ea47 | |||
| a0a334fc8d | |||
| ae12e94b1f | |||
| 9ed971cc90 | |||
| 129cb23078 | |||
| d22812e917 | |||
| 10d712824a | |||
| de4059c8fa | |||
| 416aff86e9 | |||
| 59dc7f178f | |||
| a3c9f8bfee | |||
| 5546c65491 | |||
| f2179f015c | |||
| 9a53d84b9c | |||
| 0a8ff7899c | |||
| f9d7877d8b | |||
| 9fe9976591 | |||
| de1824f71d | |||
| 19a9296c47 | |||
| 49967f3d45 | |||
| 978b6254c7 | |||
| 1afd143810 | |||
| e5cd7f0a3a | |||
| 51d3940c63 | |||
| 179aec1578 | |||
| af0014aba8 | |||
| da3d995709 | |||
| 04c4269db3 | |||
| 78f238294f | |||
| 354d0468c1 | |||
| 4efcc344c3 | |||
| 5818b58350 | |||
| 7941129cc4 | |||
| 069d71a840 | |||
| 08027e7a79 | |||
| ead302981c | |||
| fe0ffa14ff | |||
| 821b8e70a8 | |||
| 8ceda54c7d | |||
| 84e515bd6e | |||
| dea1df6878 | |||
| 0076ec2e8d | |||
| 82c9671f79 | |||
| d364a1122e | |||
| fb570e94e7 | |||
| 6e3c830cd2 | |||
| d7db7fc132 | |||
| ff1550bb38 | |||
| 976001e474 | |||
| 531dd6fb4f | |||
| ba035f2a76 | |||
| d34675d3c9 | |||
| ce95adc66c | |||
| 397fe8e395 | |||
| 111266d832 | |||
| 19d858f9b6 | |||
| 79690724d8 | |||
| 5ed87ffcb9 | |||
| b99cb6323f | |||
| debf3d8a8a | |||
| 4811e52af3 | |||
| 8d81730ec2 | |||
| 330a85c25c | |||
| 3a21116307 | |||
| 247d168af6 | |||
| b2a8a283c7 | |||
| c36ddce36f | |||
| c35d9cff7d | |||
| 549ce3cf6c | |||
| 575bc0768c | |||
| 89334e881e | |||
| dcec6354f5 | |||
| 16d338da84 | |||
| 27258f7207 | |||
| 4d2d6a5ced | |||
| 0c00b203e6 | |||
| 3b68dcdd81 | |||
| 39db026161 | |||
| f6c589c606 | |||
| 2bd29c3172 | |||
| 4a61f53b85 | |||
| adc9ad28da | |||
| 585cfaef8b | |||
| b5cd8880b1 | |||
| 44ddab881e | |||
| bfa287b66d | |||
| 243e52fa11 | |||
| c166eaba6d | |||
| 09194c24f2 | |||
| ec521e47aa | |||
| e3f4a51c18 | |||
| 0a06fd6f63 | |||
| 70eace5290 | |||
| 40f9f254a9 | |||
| 15d6c17390 | |||
| a9d1d42436 | |||
| 1ecfa38eee | |||
| 54fd92b7dd | |||
| 835906d392 | |||
| 1721e6a1ed | |||
| c7ee3b833f | |||
| ffb6e28ca7 | |||
| a4c6846851 | |||
| d18c0bf694 | |||
| 4e3f9854e6 | |||
| b27943423e | |||
| 894a1016bc | |||
| efe6cddd34 | |||
| f1c6bdf3e8 | |||
| 710659bcf5 | |||
| be67775da4 | |||
| 2c6381499c | |||
| 4df842e78c | |||
| b81696fb64 | |||
| d226d841a1 | |||
| c6d83047e5 | |||
| 46dabccdf1 | |||
| cd9517b679 | |||
| cd6677ba1d | |||
| 9c1a47acf7 | |||
| 0c280a3ce1 | |||
| 53e8b6e705 | |||
| ad33165fa7 | |||
| 2055db61c8 | |||
| d2c662e54f | |||
| d24b58ef3f | |||
| 06ae9b0f3b | |||
| 2a9c1c06a4 | |||
| 90ad1b7f22 | |||
| f22fbcd1af | |||
| 1d761684c5 | |||
| e491770f1c | |||
| a41be61506 | |||
| 1a8f633611 | |||
| af8fe918d8 | |||
| 8ef9dfd9a2 | |||
| 66df24040f | |||
| ed4442d9ea | |||
| 0edb5d5ebb | |||
| 9ffc2c7ca3 | |||
| 93cb3758b5 | |||
| d22e75dcdd | |||
| a1b2a6fe2c | |||
| e15cba0c8c | |||
| 31fd207ba2 | |||
| ba6d1b8772 | |||
| 0dce561ec9 | |||
| 376142eb0d | |||
| 664ee1f483 | |||
| dac5b6fde1 | |||
| 998c57442b | |||
| 4a0ab6c926 | |||
| f43e82f17f | |||
| 62238620a5 | |||
| 200745011a | |||
| 82fd88339b | |||
| de0f2efbfb | |||
| 29cf28d845 | |||
| 7e4dbb5f3b | |||
| 923c3a814d | |||
| 779e3cc5b5 | |||
| 3f3d1ef8f5 | |||
| f92f9f137a | |||
| 87f7f436e8 | |||
| 4298c0b1eb | |||
| 6c104d771e | |||
| aefb9a5bc4 | |||
| 8868d7cbb8 | |||
| 10cbac20f9 | |||
| 26bcd0c90d | |||
| fbece2bb67 | |||
| 0012183ede | |||
| 8916cbc6ab | |||
| 21ce70054f | |||
| 3ba82b6d87 | |||
| e771c5d057 | |||
| 4e5e925e39 | |||
| b7248d4115 | |||
| 639253840f | |||
| 710ebdf9c1 | |||
| bb64d84ce4 | |||
| cd1da27ff2 | |||
| c1accc2e5b | |||
| e4489dcbc1 | |||
| c0d407f7ce | |||
| 461115afde | |||
| bae1965231 | |||
| b89c77ec9a | |||
| 1ca5f09d7b | |||
| d79902ae59 | |||
| 77568e114f | |||
| a24d274a3c | |||
| dac81432d6 | |||
| 309b5081ef | |||
| 91bc4f2671 | |||
| 4c9d37d919 | |||
| 7e9566f66a | |||
| 3f7e8a475d | |||
| 1cf7c0f334 | |||
| ff8ee9ee4e | |||
| cbbd939a94 | |||
| f232df2887 | |||
| 16bfb2c80c | |||
| 0ba066123e | |||
| 81c51c26cc | |||
| 6fa8295ac5 | |||
| f975b40236 | |||
| 01d9d9c8c8 | |||
| 1eafc4e5d9 | |||
| 38e4020aa8 | |||
| ac32fbb3b2 | |||
| 7d26eca5cc | |||
| 3347d61591 | |||
| 9abf2c8c9c | |||
| 84e2262ad6 | |||
| 378137d34a | |||
| 66ca16f836 | |||
| 282884ad83 | |||
| 7877ac42f0 | |||
| 19ef8891e3 | |||
| bfea9e53a6 | |||
| a2420026ab | |||
| 1be1991299 | |||
| 67dd7e1923 | |||
| 2b584586ed | |||
| a1994ff0ab | |||
| ca0e858871 | |||
| 06c6615507 | |||
| 818d0be436 | |||
| fcd2baa945 | |||
| 62e0a2824a | |||
| bbe1721a18 | |||
| c1470a51b8 | |||
| 6ee31d5dc5 | |||
| 65d74387e7 | |||
| 7d0ea599c4 | |||
| b7795a3dea | |||
| 323f6f6202 | |||
| 0c61223884 | |||
| 32234be7a2 | |||
| 178b49832e | |||
| 18cbb4a84d | |||
| e84afe196a | |||
| e1e171a3c4 | |||
| d075c00015 | |||
| 6c0ca4a64a | |||
| 6b5d461411 | |||
| 7419e0dde1 | |||
| cf2bb5e40e | |||
| f466e94d65 | |||
| eb0257d48f | |||
| b83dd6c6b4 | |||
| 51c207448d | |||
| a6a558da30 | |||
| 2bf5fa27be | |||
| af7940746f | |||
| a2aa1a156c | |||
| 2f8a72a42a | |||
| 8179ca5eaa | |||
| 4b74f882c7 | |||
| 7cf45af502 | |||
| 46c21158d8 | |||
| 80da0776f8 | |||
| e91f10ab16 | |||
| 2c15cd7923 | |||
| d6584543e9 | |||
| c13228f346 | |||
| 7220d8233e | |||
| 0237bf09bf | |||
| 04017c25bb | |||
| 02199cd609 | |||
| 26b9f5831a | |||
| 243a76002c | |||
| c71e4ddee4 | |||
| 32eb8c1be9 | |||
| c587017830 | |||
| fb885652cc | |||
| afc2f05e5e | |||
| 06547d0cbe | |||
| 578108280e | |||
| 65db7352b7 | |||
| a4db8bd7b5 | |||
| f1c1b02d77 | |||
| 6580f32b43 | |||
| b028cbd8bd | |||
| a1a5418318 | |||
| 5a32634b74 | |||
| c1875af70b | |||
| 0a10d14e19 | |||
| ed8ceec66f | |||
| 03760011d7 | |||
| 0d5aebb806 | |||
| 1313510890 | |||
| b712f2bb6a | |||
| 938c15ec63 | |||
| 3e7f032ec2 | |||
| b42f5bfb19 | |||
| 717562b264 | |||
| 9d6637c1b3 | |||
| 56fef7c8df | |||
| ba0935c71f | |||
| d83eb2800a | |||
| 6f943112a9 | |||
| f422893b8e | |||
| 22b498489c | |||
| 5460517bd2 | |||
| 9a6e557e52 | |||
| 4fdc07927f | |||
| 9030b67e4f | |||
| 43eafdf4b7 | |||
| dfb88edb5e | |||
| bd3e65df4d | |||
| d7b13f3408 | |||
| 14ef8e8051 | |||
| cc1d9f124e | |||
| 93c0299606 | |||
| 55e3c73221 | |||
| 6783417504 | |||
| fa3f706e71 | |||
| 9c2f6cae88 | |||
| a30181e240 | |||
| b4ccf64e62 | |||
| 88d768bf6b | |||
| 6444cc7905 | |||
| 328af1f397 | |||
| 5ae60e2e80 | |||
| 0e0b868342 | |||
| a5beb08ed7 | |||
| 45fc7b903d | |||
| 4f2c274942 | |||
| 93415493b4 | |||
| 8e4d338de9 | |||
| 8a71e091a8 | |||
| 120cd7f25a | |||
| fb3bf6c984 | |||
| d57e1f8baa | |||
| 15ca9ad8eb | |||
| c2e1861747 | |||
| 543d41f3dd | |||
| e5cfc988ec | |||
| ee3916be17 | |||
| fd513f8af8 | |||
| 9a2b7f559c | |||
| b8d2b0df7e | |||
| fe3a9c603e | |||
| 97030d4cb1 | |||
| b2c3e567da | |||
| ca5e633399 | |||
| e60a9a628b | |||
| 0167691941 | |||
| 3b0f976380 | |||
| 7bd298b536 | |||
| 0476a65fca | |||
| 2cb2af115a | |||
| 789226ff6d | |||
| 805efc5bf1 | |||
| cdcab26766 | |||
| ec3acb1932 | |||
| d30e37434e | |||
| 20d5b2e20e | |||
| 6c6be4ab1a | |||
| d004eb1f7c | |||
| 3148b0f3e8 | |||
| 3fc0bd26a5 | |||
| 6c9025ff17 | |||
| 289997e373 | |||
| db44cbdff0 | |||
| da9179335c | |||
| cdf641fa3e | |||
| 66dbee10f5 | |||
| 19e9b620ba | |||
| e4e4700aff | |||
| bb55045596 | |||
| d7e51cdeb5 | |||
| 7f4964b366 | |||
| a6957aba11 | |||
| b5f94f961d | |||
| e182d3db7a | |||
| 3e6e0528a6 | |||
| ac508a1ce4 | |||
| d7fc1e09b1 | |||
| 3b0c86e401 | |||
| 61d10d8ffa | |||
| 7d9548919e | |||
| bee80a730f | |||
| ac3e24c99c | |||
| e7e852bdb3 | |||
| 2b7f168571 | |||
| 5b3da1d878 | |||
| 99f1bc0177 | |||
| ed76f076dd | |||
| 4d357d1063 | |||
| 961ae1541c | |||
| add1aec685 | |||
| 03d6ba7496 | |||
| 71e4d5cc51 | |||
| 215ab48222 | |||
| 0c64c68781 | |||
| 3ec035c68b | |||
| 20c7dcfbca | |||
| c1b8780b9c | |||
| 64c61603e9 | |||
| 57c08d925f | |||
| 51623a5f6a | |||
| ca3f6181d7 | |||
| 9c94f9c3d0 | |||
| 4a85843bcf | |||
| d4d9b99879 | |||
| 6816b7d95b | |||
| acdf265d7a | |||
| 19495eb9bb | |||
| bacc8609ee | |||
| 99163f5afa | |||
| 0607227730 | |||
| d938fdc496 | |||
| dcb4c3d84a | |||
| 82ebcd9209 | |||
| ff1687744d | |||
| 782c870fb2 | |||
| 71fad63829 | |||
| d65c6101a8 | |||
| 3c40b1bd51 | |||
| 90a8800bb5 | |||
| 97f1dae2d1 | |||
| e54ec05709 | |||
| a24eb99679 | |||
| ad113d00b7 | |||
| 7bd5884d12 | |||
| c3505858a6 | |||
| e76aa37fd4 | |||
| 1a32220ca9 | |||
| 4161403a1d | |||
| 53bcdc4294 | |||
| 30a8ef28cd | |||
| 855f90727a | |||
| 2191a44e36 | |||
| 952276dc2d | |||
| 2286edb329 | |||
| a0f28583e7 | |||
| 8af0af3400 | |||
| 769e5cbb2d | |||
| fc69308057 | |||
| c6d620c99e | |||
| f510a4def6 | |||
| 4ae3069c6f | |||
| c0f27751d3 | |||
| efbcd5a683 | |||
| 6a67712944 | |||
| e8a690928d | |||
| 0eee95af57 | |||
| a09c6e991a | |||
| d06c5ab990 | |||
| e0924d27b8 | |||
| 2775b771f2 | |||
| cf7a3c6a0e | |||
| 230cc6acc3 | |||
| 626a23a585 | |||
| 74f196eebb | |||
| cf2242aea3 | |||
| 8cb59e6fca | |||
| 5cce17e80a | |||
| ee5302fb2d | |||
| 387c6ef664 | |||
| 581734c369 | |||
| d90a969c00 | |||
| 2c8a96bb27 | |||
| 5da168db30 | |||
| e215e2daf3 | |||
| e28f5aa45b | |||
| a2d0e8f233 | |||
| 303d04106a | |||
| c423c496a1 | |||
| 4e85f72f0e | |||
| dd0737aac0 | |||
| f90985845d | |||
| af4917dbb6 | |||
| d9404fcce4 | |||
| 5c01fee5a9 | |||
| 9b27d68a37 | |||
| b99d884e57 | |||
| 587df594b8 | |||
| b896e0d314 | |||
| 559fb7ee45 | |||
| 62545cd983 | |||
| c50812518e | |||
| 4cc5609d8b | |||
| 50fa90dfb8 | |||
| a2c365e710 | |||
| b4ddffdc61 | |||
| 8d4d184fc6 | |||
| ea23539b59 | |||
| 9e92b6f11e | |||
| 6cbde812f6 | |||
| 3b2e932c13 | |||
| 8ff4e52641 | |||
| 2dbc874e3d | |||
| 039a2f1d04 | |||
| 4ef1cf5b35 | |||
| b44ab9e33c | |||
| 8f4c23f1c4 | |||
| 23a391e715 | |||
| 035b0be29f | |||
| e1fcdbc337 | |||
| cfc149e994 | |||
| 2faffbd1b7 | |||
| 8db65704b9 | |||
| 797a01aed4 | |||
| bf515a3d32 | |||
| a06745826a | |||
| 0420ed4f2a | |||
| 3b944addd4 | |||
| 70bf8bc35d | |||
| 724f8a1d45 | |||
| cc2b2146ee | |||
| 8689f5f230 | |||
| e9e0011f1d | |||
| 5b52833785 | |||
| 1525768094 | |||
| a70ea4654e | |||
| b02bf9b6bb | |||
| bee7bc5324 | |||
| 7c2ffd3fef | |||
| db01e7dab6 | |||
| 2326c74eb2 | |||
| b9d15569e8 | |||
| c3cc378d89 | |||
| 27d1f5e0a8 | |||
| 540632bb9e | |||
| d9c028c934 | |||
| c54ad82e8d | |||
| 295b89631b | |||
| 6179faf778 | |||
| 16dc236a25 | |||
| 61ae9d75b6 | |||
| e2401aca68 | |||
| 59943cbb48 | |||
| 02634d404d | |||
| ed12925f7d | |||
| e0ddb97ab4 | |||
| b8c01af0fc | |||
| 6de0a7ddc1 | |||
| 79196c025d | |||
| 94c33ac020 | |||
| b2ecb6352c | |||
| 9dc3ed638a | |||
| 0acace1ace | |||
| 1a2d37e1e6 | |||
| 22adb6494f | |||
| e023736c30 | |||
| dca2262fe6 | |||
| 0684a20ea3 | |||
| a1a72bb8d1 | |||
| 144d55a5be | |||
| 7fc13c5cfd | |||
| dfee7af57b | |||
| 9b0e2daf02 | |||
| 590060a16b | |||
| 368294edf6 | |||
| c4a9ccd6af | |||
| cbf91f2ed3 | |||
| b1460d4787 | |||
| 7dc9e14874 | |||
| 1616ed543d | |||
| dc73fba188 | |||
| ef148dfd37 | |||
| 93bbb3032d | |||
| 4c83d8596d | |||
| d453e6d7db | |||
| c29533994f | |||
| 1afe13b5b5 | |||
| 36600eaaa9 | |||
| 3ee1fc2034 | |||
| e2f93e5a2d | |||
| cfdf2f1153 | |||
| e042143e3f | |||
| 7c613d0d9b | |||
| b00d46bc14 | |||
| 555b0d235b | |||
| 564daf9a7d | |||
| 41bcbe342f | |||
| dbe8dc344e | |||
| e33fb59da1 | |||
| 7aa88aa115 | |||
| 2b6d600879 | |||
| 05c765d442 | |||
| 49b496269c | |||
| 7405925952 | |||
| 3afd543a7e | |||
| b4f2cde5ac | |||
| ed53ef7cee | |||
| 12630b124d | |||
| 1d59ac09d2 | |||
| a8f3a0dd59 | |||
| 124cd70710 | |||
| 782de139c8 | |||
| 32eb32ee5e | |||
| 2f51eb2b41 | |||
| 0ccbd79e10 | |||
| 99bd6de541 | |||
| 1fef36e4bc | |||
| 89375005b5 | |||
| 88e78c9193 | |||
| 29a19ad080 | |||
| 2a039ab746 | |||
| 7e9a0fcdbd | |||
| 7a97532547 | |||
| 996abb2831 | |||
| da500a358f | |||
| c36b846acc | |||
| d9b5c9b2be | |||
| 3dee8778d0 | |||
| d4216b0dcc | |||
| bfe2bf4dce | |||
| 561f9291fd | |||
| b5b0d6b3ea | |||
| a90426b7ca | |||
| 303c3bae7f | |||
| 6b4358f641 | |||
| 552158f3ad | |||
| 7205203dc8 | |||
| 0cadf70072 | |||
| 076b3d0a9a | |||
| 7b0c9e04d3 | |||
| 573df524fe | |||
| aee417c46a | |||
| 04db44067d | |||
| 5b204c54f9 | |||
| daa602422d | |||
| 04dfb14e32 | |||
| c24256cba3 | |||
| 685fb71d89 | |||
| 83b6033906 | |||
| 01e7668915 | |||
| 0994d9c881 | |||
| 030428ba43 | |||
| 8a110e02b9 | |||
| 86d92c17c4 | |||
| c4cc7891b4 | |||
| 218843b9f1 | |||
| d274d093af | |||
| 6432f00f0d | |||
| 4e9e842aa4 | |||
| 07880ca441 | |||
| bcda25a513 | |||
| 8256fcde15 | |||
| af65aa298a | |||
| 6834d17844 | |||
| ed511d7867 | |||
| cd8d736a9f | |||
| 0952b2dfd4 | |||
| 4bedd33c59 | |||
| c5fb0c43f9 | |||
| 9e4780510e | |||
| e8405f40fe | |||
| 065b9e6fb2 | |||
| 98141ca7d8 | |||
| 501577ab28 | |||
| 5669f48343 | |||
| 24ff66d4a9 | |||
| bf184449bc | |||
| 7b98c2c653 | |||
| b6add2a257 | |||
| 2bd41f1330 | |||
| c37cd11ca5 | |||
| 9dee8edc0c | |||
| f6aa28c380 | |||
| dba1644518 | |||
| 260a65b0fb | |||
| 835d2fb98c | |||
| a9811addaa | |||
| ee9d88b637 | |||
| 194a763c46 | |||
| 8d74446bef | |||
| 7ed6c7905c | |||
| 159a37fa37 | |||
| f39ae0e7c1 | |||
| 4a68eac99b | |||
| 2665580120 | |||
| a4391aeedd | |||
| b86a967ee2 | |||
| 608232568b | |||
| 7f85beccb5 | |||
| 767f1255ab | |||
| fddbfe7b0e | |||
| 4ab7fdc28e | |||
| e352b68878 | |||
| 207deeadba | |||
| d18d92f925 | |||
| af3ce47c44 | |||
| d8bfb6712d | |||
| f864f8b5f7 | |||
| 31d72efba7 | |||
| d169c951f3 | |||
| 90d7e38909 | |||
| 938f23e429 | |||
| f97d275413 | |||
| 3acb4ca90e | |||
| e86b81bbf5 | |||
| a5447b8b75 | |||
| 7ce6452d83 | |||
| 5643a306bd | |||
| 3c877c504b | |||
| 892d1acccb | |||
| 1a9c282f76 | |||
| fd1ba46f77 | |||
| a4745626dd | |||
| 17bb7ad278 | |||
| 152988c17b | |||
| 4cd37fc02b | |||
| 69b9d674a3 | |||
| bad8061547 | |||
| 62963dcefd | |||
| 68a35e4735 | |||
| 9b9ad77e1c | |||
| 118b4d4a01 | |||
| da14ab6f16 | |||
| 09a4ca6ab5 | |||
| 8a2df79711 | |||
| c30e486b64 | |||
| a575c0c54b | |||
| 77fe96ac0d | |||
| 5234c3759a | |||
| 41b3511ad9 | |||
| 128e4a2e8d | |||
| 07ac90d798 | |||
| 7de87a9b2c | |||
| dff865239a | |||
| 07f8f70c5b | |||
| f625c5aabe | |||
| 8a74976c1f | |||
| b6bfd4a5cb | |||
| 008fb9d258 | |||
| db6db49ed6 | |||
| 05453881c3 | |||
| 5e47ab9431 | |||
| ec70acd0b9 | |||
| 25e61056b6 | |||
| d579e335b5 | |||
| 7e344ceb85 | |||
| 0145b82ea0 | |||
| b4efe7aab7 | |||
| 9ffe951f6d | |||
| a5ea4f57bd | |||
| 88f4c16755 | |||
| c7ee071efa | |||
| 0740ef7ceb | |||
| b29bd809ac | |||
| 8977c9257a | |||
| 091b7eacba | |||
| e74b1251c0 | |||
| d282a1649d | |||
| 6ce8d49d1b | |||
| c5b197078a | |||
| 0494f20d62 | |||
| 73aff476dd | |||
| 98ee5e651a | |||
| 01871ea383 | |||
| 1dbdb9438f | |||
| c70f0eadb8 | |||
| 26244ad8c2 | |||
| fa0aa5510d | |||
| eec557b6aa | |||
| 0cc27c3cc1 | |||
| 507089d7b2 | |||
| a6b3517b75 | |||
| 2d6beb7813 | |||
| 61bc129e1d | |||
| 52210a57f0 | |||
| 8061a2f108 | |||
| 7444eff6d4 | |||
| f35a9da99a | |||
| c3098e9ab2 | |||
| 686f9288fc | |||
| 1833670fb9 | |||
| 3dd42f5aa2 | |||
| 99a7beba57 | |||
| edee2b753c | |||
| 545d5770be | |||
| ca747a2b54 | |||
| 17da165cfe | |||
| 5e6788c679 | |||
| 425deadca9 | |||
| 2c8e9dd3a5 | |||
| 7a72f1a253 | |||
| 208e556332 | |||
| c65d11bfb5 | |||
| 3b5b52d89a | |||
| a4f6c8f990 | |||
| 670c329852 | |||
| f3551c8422 | |||
| 90b8187882 | |||
| 1a43259989 | |||
| 3c0a630475 | |||
| 2a1e5a9729 | |||
| 413c66beba | |||
| 1416e696b1 | |||
| d373cf89c7 | |||
| dd886d22f0 | |||
| 472569a27c | |||
| 76cf6559cc | |||
| a34e8dcdc9 | |||
| da752fc9a4 | |||
| beb2de2dd9 | |||
| 2a8b65e105 | |||
| 62a916bc24 | |||
| c47b833e7b | |||
| 09b0958b5f | |||
| 3a4c3d3e58 | |||
| 7484292e63 | |||
| 687c2741b8 | |||
| 2fb285e530 | |||
| 16f6473938 | |||
| 66546208b2 | |||
| 532274045e | |||
| 9347c72fb6 | |||
| e90bb7169c | |||
| 8a2c41e183 | |||
| 59fb65293a | |||
| e7718b92b7 | |||
| cdfaf761df | |||
| 1a9ea6f738 | |||
| 945c1c8597 | |||
| e4d0f7acd5 | |||
| 250496c953 | |||
| e47dc758c9 | |||
| b92a843c5f | |||
| 91bea9c5b3 | |||
| d75bb5cbe1 | |||
| 2671259fdb | |||
| 2024010119 | |||
| 412040f77e | |||
| d210660ce8 | |||
| 863a12562b | |||
| 5da606a9ac | |||
| 8d20f3d5c4 | |||
| 5d360180af | |||
| f0fbed6007 | |||
| 519de7c833 | |||
| 97ccef1a04 | |||
| c4df0dd06e | |||
| cd114c6818 | |||
| 1707b8cdba | |||
| 41d4d70b98 | |||
| 0e999482cb | |||
| 65b2c06027 | |||
| d7b61ede07 | |||
| 87fc1c84b8 | |||
| d4b5f12383 | |||
| eb62b0d665 | |||
| 91387a741b | |||
| e8b34cb00d | |||
| 82954258c1 | |||
| 50f092551b | |||
| c36a64be68 | |||
| a343b20775 | |||
| a714e76ae1 | |||
| d21d5c9510 | |||
| cd6788a2bb | |||
| 6b99399c41 | |||
| 952b6af445 | |||
| 7c674ad7fa | |||
| d7d2ac3951 | |||
| 29e67d307a | |||
| 7320b7df62 | |||
| 11fb4233f7 | |||
| 84bb350b14 | |||
| 38e3694d1c | |||
| 1084935241 | |||
| f5f0b9ecaa | |||
| 230fc49ae2 | |||
| 250d507bdf | |||
| a818653174 | |||
| 5c3b044740 | |||
| c5aa8729a1 | |||
| 3f78d76da1 | |||
| 70c19ccf16 | |||
| 68db9cb499 | |||
| d0466fa777 | |||
| 21ab64e962 | |||
| a0145cebf2 | |||
| 69176fc5f4 | |||
| 278dce9ba6 | |||
| 1cfa3ee4c7 | |||
| 9a95cd5794 | |||
| a62fe3df6f | |||
| 7701244a08 | |||
| 96e31e4b78 | |||
| ec208af474 | |||
| 242641264d | |||
| d3a9a0615b | |||
| 3277e8c89c | |||
| d02b9442a5 | |||
| bac385b59c | |||
| b1a0ab8086 | |||
| a33749eb71 | |||
| f5e4ee90e4 | |||
| 690d5e6dbd | |||
| a76c055b63 | |||
| 70c461c60b | |||
| d51b71ee80 | |||
| 3666448ca6 | |||
| d3311d9f43 | |||
| 3e1735b06e | |||
| de7ef7eace | |||
| 7e89458a3b | |||
| f212bafe46 | |||
| 86fe40708b | |||
| d718747c5b | |||
| 46ee9ac41c | |||
| f1d306feab | |||
| 2d0db98e83 | |||
| 3df06a1c68 | |||
| a8f9432a3a | |||
| 561e0b04a8 | |||
| 404b6a864b | |||
| 4feaf31225 | |||
| 391aa14845 | |||
| a0d61b4c37 | |||
| 2952737755 | |||
| f103aa4753 | |||
| 884856023a | |||
| d8188fce7b | |||
| 0f15f1ab73 | |||
| 488a236b7a | |||
| e833823e15 | |||
| ee4ba104e7 | |||
| 4fdc08295b | |||
| a3ff49aaf1 | |||
| 76364ea767 | |||
| 8eec50d764 | |||
| 32b659b346 | |||
| 00809909ae | |||
| 9f7684f6fe | |||
| 2bed7d370e | |||
| d2b852f7cb | |||
| 901939bd96 | |||
| edfdcc8cee | |||
| 3982c9a552 | |||
| 4490b2d209 | |||
| eb4bbf3294 | |||
| dc97d48491 | |||
| 0f50dc848e | |||
| c5e4b83de3 | |||
| a08ab46713 | |||
| f50a7058d6 | |||
| 2c74f0a040 | |||
| 58835e40f3 | |||
| 8befa5918a | |||
| df80f7ff2a | |||
| 5f66786ef1 | |||
| 3a965856a5 | |||
| 03df609d77 | |||
| 178581b560 | |||
| ffd2314120 | |||
| 815b595d2f | |||
| 11e56403dd | |||
| 4baadecda5 | |||
| cf552b5f3b | |||
| 1894304d33 | |||
| 9d5392fb02 | |||
| c280645671 | |||
| 45f92e6b38 | |||
| 6509f09961 | |||
| 3c279a6f0e | |||
| 9ec3f03871 | |||
| 84a9c2c112 | |||
| af368119cb | |||
| 89b9189efa | |||
| dd59b8c7b9 | |||
| f83491274f | |||
| c0435fdff4 | |||
| 3c09c77269 | |||
| 547e101f1d | |||
| 0130f64934 | |||
| 361e0543ee | |||
| 63aa5d3b4e | |||
| 01302d097c | |||
| e6095cb7e8 | |||
| b876b8af11 | |||
| a7c41f3fcd | |||
| 4772bd8d4c | |||
| d471067e3f | |||
| d0b7780239 | |||
| e627ca6bd7 | |||
| c97172bdd4 | |||
| ce8a745fb4 | |||
| 3e9efd1401 | |||
| 20340190b5 | |||
| 265040a78c | |||
| 448d7e0c5a | |||
| 6eb1874c5a | |||
| 4c70745cc1 | |||
| 7795748a3f | |||
| 098ef4d7cf | |||
| e3f91bfe1b | |||
| 7374fe73a3 | |||
| d2bde205f0 | |||
| 5620f70f9a | |||
| 37f258b1bf | |||
| e2dd2a133e | |||
| 7514644e07 | |||
| 16b0aeda7d | |||
| 86e4f4a841 | |||
| 607eacf8c7 | |||
| 7a049644a8 | |||
| 17a13f00f8 | |||
| 43436e48e0 | |||
| 5a39102405 | |||
| 94999101e3 | |||
| e619b7c4f4 | |||
| b7c2e8cb67 | |||
| fb76893e18 | |||
| 88d812fe82 | |||
| 77f9f4664a | |||
| 5c2f85c39e | |||
| ac4d22cd12 | |||
| cf95e44cb4 | |||
| 65dd2bb429 | |||
| 6be855be6a | |||
| b6e3f4423b | |||
| 0c61d81713 | |||
| 7c6f5dba63 | |||
| 44cfc7e62a | |||
| 96670d5f16 | |||
| 36b971ee4e | |||
| f1a9629652 | |||
| 20230402d0 | |||
| 5c2c3a6c88 | |||
| fb019d43bf | |||
| 025aa33773 | |||
| 302e21fd58 | |||
| 211512ae64 | |||
| 8ec917b1c3 | |||
| 1c7534f009 | |||
| ae745d9397 | |||
| 60f37aae2f | |||
| d7daf5f724 | |||
| e5103d9429 | |||
| 8fecb29848 | |||
| 290ea6179d | |||
| 9695a40fc9 | |||
| 1913b95227 | |||
| a874aea692 | |||
| 69c52099e7 | |||
| cfc0747d5d | |||
| fcd7e8768d | |||
| 3c34dd8275 | |||
| 1116e481be | |||
| 63cf9d04de | |||
| 3364d4d147 | |||
| 57ad21e4bd | |||
| 414f87981f | |||
| b1459c79cf | |||
| 352ea07226 | |||
| 27018787af | |||
| 4e305eca26 | |||
| 9e9c0ceaf4 | |||
| b3bf18b1c0 | |||
| b1619f675f | |||
| 96c3de12eb | |||
| 719dbb8bae | |||
| f38a7f7f8f | |||
| 6ea38b4438 | |||
| f7447aece1 | |||
| aa2b9ec476 | |||
| 3ee00f8bc2 | |||
| fccab60a5c | |||
| 0f4af38457 | |||
| aef39f1160 | |||
| 2023012408 | |||
| 95a7661bb1 | |||
| 618d317803 | |||
| ae897c8cdb | |||
| d0a0f3c052 | |||
| 91b9591b10 | |||
| aa7361337d | |||
| 284d77fe2e | |||
| 826178f1e2 | |||
| acccf8a9b8 | |||
| 57c066f0be | |||
| e44f64ae92 | |||
| d51980a3f5 | |||
| c3d73e7ecb | |||
| b077f6821d | |||
| a79de11af7 | |||
| 2023011763 | |||
| b46e40e86b | |||
| a6d6cdd165 | |||
| dc8da605f9 | |||
| 8b299a29c7 | |||
| 3109b865d2 | |||
| 0c5956c43c | |||
| 1c83b39691 | |||
| 77874b473c | |||
| b7cce7be15 | |||
| 3cd3362417 | |||
| e97e925efb | |||
| 0f032235cf | |||
| e0f0984da7 | |||
| 4d22b5aaef | |||
| 80b8846318 | |||
| bf641faafa | |||
| 23d8b78ce1 | |||
| 3b2244077d | |||
| ee5cdb9713 | |||
| 03d02d67f7 | |||
| 5798145581 | |||
| 51ef0b7f66 | |||
| 97b4542c73 | |||
| c1cd0c09a2 | |||
| 1fc1f47d80 | |||
| ec471a5bc2 | |||
| a893fc0ca2 | |||
| 3761dc0433 | |||
| aa71a07fbe | |||
| 088293f5e7 | |||
| 7c660aa86e | |||
| 435d8fa0a2 | |||
| 5cd6f1d064 | |||
| ec20dfe312 | |||
| 924ffb5a35 | |||
| 62c7f59b94 | |||
| e97176b1d7 | |||
| d649f5d826 | |||
| 6c37177cf5 | |||
| 14775aa975 | |||
| 44b6336372 | |||
| 36d2bb332b | |||
| 4dbe45640a | |||
| 4b3f0b9f08 | |||
| 12af069dca | |||
| d42e708d31 | |||
| b7bb973118 | |||
| 750b2a6313 | |||
| de0da86bd7 | |||
| 8e283f512a | |||
| 73162a4bc3 | |||
| 1a9761736e | |||
| fd1f7665a7 | |||
| 6d14573fd0 | |||
| cf69b836ac | |||
| a7a771b92b | |||
| def011c029 | |||
| 4b055bf260 | |||
| 1ba7484d60 | |||
| 51c518da1e | |||
| a3b6b03dfb | |||
| 18e3b38c69 | |||
| 0ad30063ff | |||
| 7812c64a31 | |||
| 3d2376ab52 | |||
| 6b207bbf2b | |||
| 3f079ba7c6 | |||
| 8f4c89f50e | |||
| 6b7a543c82 | |||
| 2ba68d24f2 | |||
| 46877e0a92 | |||
| b55f555487 | |||
| a38b63be18 | |||
| 1bebd6f4f5 | |||
| 3da63f394d | |||
| 2a54e3d770 | |||
| 06b02ba46e | |||
| d01ae55109 | |||
| 8868d7d188 | |||
| 2eec9892be | |||
| 01ae621f11 | |||
| f984aa0d2c | |||
| 0881a6bc17 | |||
| 2c6a73546d | |||
| a29944660e | |||
| f6ce624c6f | |||
| c09ec8e4d1 | |||
| 31bbaad06e | |||
| b9ca1fe830 | |||
| e61585f2f3 | |||
| 0de1aacb0c | |||
| 168829b555 | |||
| 170fc517d0 | |||
| 0fbf1c7c71 | |||
| 694be39c71 | |||
| dad26d81df | |||
| bcaea097ea | |||
| d56fe74e24 | |||
| 4603d540c3 | |||
| f9d53303bb | |||
| d04faa6505 | |||
| 07da058eae | |||
| cefa6b9878 | |||
| 04d0b0223f | |||
| 78ad6d2d88 | |||
| 22cbd9fa58 | |||
| 984049586a | |||
| cdfc2b92e3 | |||
| 4530abe8df | |||
| 586020b8b6 | |||
| 3a8626fd04 | |||
| a6e483a434 | |||
| 6a942e56b1 | |||
| 87c91550ad | |||
| 731daf0f37 | |||
| f931e53890 | |||
| b5efc68737 | |||
| b9e6e7926c | |||
| 845034c81c | |||
| 54d42e3f40 | |||
| e03ac3136e | |||
| 6fb41a202a | |||
| 4bef330ce1 | |||
| 8a5f719964 | |||
| 209d5e8e90 | |||
| 9d041aa582 | |||
| 6532b3e655 | |||
| c1c355160d | |||
| 83515d5610 | |||
| aa10dccf90 | |||
| f4fd53211a | |||
| 4993d19466 | |||
| 19f9bbca0d | |||
| 779d8e1627 | |||
| bb07410448 | |||
| d826f9e72f | |||
| 6a6130615d | |||
| a8e7021be2 | |||
| 38259d0382 | |||
| f7e7259910 | |||
| f0bfeba733 | |||
| c3a7a24eea | |||
| bbbcd780c9 | |||
| 475469a2e7 | |||
| 3a7447dcb6 | |||
| e5d8cbd383 | |||
| 3c08dca7e7 | |||
| d083f01d22 | |||
| 68cf393644 | |||
| 18f7230662 | |||
| 728f735281 | |||
| ecc418ba77 | |||
| 3af5b7f2ac | |||
| 7a7cfcacbe | |||
| 52594355bf | |||
| 0d06c28b19 | |||
| ccc4677252 | |||
| 821fc9feed | |||
| 82b46726fc | |||
| 8df872a482 | |||
| c79c306adb | |||
| 51fdaad002 | |||
| 885cd8ff04 | |||
| 2707af403a | |||
| 2d227e5222 | |||
| 70529878e2 | |||
| 3b7a962dc6 | |||
| 6dcf5c3d7d | |||
| b089bb5e7b | |||
| a91a67668e | |||
| 220a908118 | |||
| 54841248e7 | |||
| a0b42e6538 | |||
| 3312cf525d | |||
| 2093667548 | |||
| 3c868d7961 | |||
| 707f4f5816 | |||
| b3ab6311c5 | |||
| d56f605b63 | |||
| f8b713f425 | |||
| 5209e95bc7 | |||
| ef67a45702 | |||
| b88eb72ac2 | |||
| 32847f7254 | |||
| 71df93b534 | |||
| bb028191f8 | |||
| 19af8fc7d8 | |||
| a06671b47f | |||
| 5f385d88e0 | |||
| 9cb7a364a3 | |||
| f68cbc577d | |||
| dc975e8974 | |||
| 4311ade535 | |||
| cd23401411 | |||
| 176ee6910f | |||
| 13c8f3d3aa | |||
| ce9af687bc | |||
| 43f0d0cacd | |||
| 20b4e6953e | |||
| 7da287e3aa | |||
| 205f885d69 | |||
| 3715cd349d | |||
| e4c3ecc57e | |||
| 673c5d886d | |||
| f799b568d1 | |||
| 7bff4661f6 | |||
| ffd8bef808 | |||
| 02cee2234d | |||
| e0dd2be3fb | |||
| a33c011c21 | |||
| 7c3f42bbba | |||
| edac9820b5 | |||
| 84a47f7102 | |||
| 97ae8afb6f | |||
| 4138333f5c | |||
| 61339a8ae2 | |||
| 50eb2e3855 | |||
| 5fc78e4584 | |||
| 2736a2f69e | |||
| 179993f0cd | |||
| b734f657f9 | |||
| c29d7d02c2 | |||
| 3df6b2a58c | |||
| b8aa2d2c32 | |||
| 0ff885461b | |||
| ca43f95fb1 | |||
| 09700f676b | |||
| 4271e9cffa | |||
| f3dc8a10d5 | |||
| 00fb486f6a | |||
| 4173e94c6f | |||
| 261d3d3340 | |||
| 15e20fcae1 | |||
| f4f47f5fe3 | |||
| 71d11de7ca | |||
| 88d74a15aa | |||
| 0f02fc0c77 | |||
| 3f90fb42d8 | |||
| 9bd8988300 | |||
| 3c804bcfec | |||
| cca4cdc4f1 | |||
| 8f899aaf8a | |||
| e53b4bb439 | |||
| ab247a1309 | |||
| c21e9edad4 | |||
| 9c21a20f8b | |||
| 7191ebb615 | |||
| a74731d7f5 | |||
| e086f0b3fe | |||
| 8255aa23f4 | |||
| a4bc08f5a3 | |||
| 7e5aa1e2a5 | |||
| 0818dbc36a | |||
| 347c4b2625 | |||
| 34f0d4d0c4 | |||
| cbedb57511 | |||
| 9ef825d2fd | |||
| 85ae745910 | |||
| 7411da8d5a | |||
| 3f75a8369f | |||
| 4cd621e877 | |||
| 6e3a2fe0bf | |||
| 8b0e1f941a | |||
| c7c5e7670a | |||
| f6c621ef1b | |||
| faf32d451d | |||
| 252fd7ecb1 | |||
| 7fa89dddb4 | |||
| fefdb8c84e | |||
| a6cc05936e | |||
| b209843545 | |||
| 19759ed36e | |||
| 1a7ae8e7b9 | |||
| da1f645670 | |||
| 3a2015ee26 | |||
| c440418ce6 | |||
| 3d37a5ba1d | |||
| 15f4cfb6d9 | |||
| be36de2482 | |||
| 391237f7df | |||
| 977e5effd9 | |||
| 8b36a4cb19 | |||
| c8cd94a772 | |||
| 764316a53d | |||
| 2048fd4042 | |||
| f84b3de24b | |||
| 6a1f3ec08b | |||
| 2e353aee96 | |||
| 8edfd14a37 | |||
| 1a191ec6f7 | |||
| e7171e94b4 | |||
| 398d937419 | |||
| 34fe5ab143 | |||
| 1b08f43f82 | |||
| b24a2e2fdc | |||
| 4c4c6e626e | |||
| 7310370a31 | |||
| 8ae94f0059 | |||
| 8fccf20892 | |||
| 5a874ae241 | |||
| f4e1ed25f2 | |||
| cbfbb49ab4 | |||
| 489b16efce | |||
| b82c1693c0 | |||
| 019bfc4e35 | |||
| dfda5c054a | |||
| f657169616 | |||
| 4c06da8b70 | |||
| 9fe2393a00 | |||
| e2e8d94b14 | |||
| 4f9a7f8c87 | |||
| bb0502ff44 | |||
| c256442245 | |||
| 1137404190 | |||
| d57c6d0284 | |||
| 76bbf57b3d | |||
| 806a47a7cc | |||
| 29851c18aa | |||
| dea950c2c8 | |||
| a367dfb22e | |||
| 9fe1a7b373 | |||
| 8e2d21c548 | |||
| bedf1cd357 | |||
| 13f180a70c | |||
| 6654239c94 | |||
| 1b61e5e9e9 | |||
| 43b3b907f8 | |||
| fcd896508b | |||
| f55c990e86 | |||
| d110372f99 | |||
| c862af09f2 | |||
| 1cfeec0ca3 | |||
| a0649edc1e | |||
| 0e0bcb3e10 | |||
| 686528d627 | |||
| 3afa920151 | |||
| 32c493e994 | |||
| 1a76bdf891 | |||
| af48b3df29 | |||
| 58ac1fb2fa | |||
| e922704f72 | |||
| c6115735c7 | |||
| 9ddf5c72be | |||
| cc5640326b | |||
| bf447d7703 | |||
| cbb008c938 | |||
| eaa0c52b45 | |||
| 82791f7efc | |||
| 8c533e34ea | |||
| 37708ad9cd | |||
| 090dee857f | |||
| d779ff7e6d | |||
| fd8858f8c9 | |||
| b234647a63 | |||
| 6e93eefc82 | |||
| 38fca30125 | |||
| 012ee9ca85 | |||
| 151252e33a | |||
| 7136cfc68b | |||
| 408c04f25f | |||
| 7f8e0dbc40 | |||
| 0de7ab18f6 | |||
| e9bc7331bd | |||
| 797dd7c449 | |||
| f37ccaa64f | |||
| ab3937ee5a | |||
| 00f4551a7b | |||
| 257ddd028d | |||
| e0a22e76f8 | |||
| 00a3610331 | |||
| f502725120 | |||
| b62a74b315 | |||
| 2ec382ae0e | |||
| cbfee31593 | |||
| 6d647e13ff | |||
| d2af3ff98d | |||
| 052d17e66a | |||
| a9bc954e17 | |||
| 2983426771 | |||
| c61eb94b3f | |||
| 3829eab1cf | |||
| 3fe8eeedc5 | |||
| 1efef88b6e | |||
| 7acdaf0b43 | |||
| 1ed25d76ba | |||
| 474c1f5e32 | |||
| 8b71fea5dc | |||
| 7bd99a22ee | |||
| 75b8cca3b3 | |||
| ef2c29d5d4 | |||
| 218b3c8274 | |||
| db9cb2ddda | |||
| 722d66e85a | |||
| f6269f0193 | |||
| 520eae817a | |||
| d099941360 | |||
| e3e76fa8c5 | |||
| 2553806e79 | |||
| 1bcbc5a353 | |||
| 15d351b0f0 | |||
| c144c95cda | |||
| f08f4fd87d | |||
| f8aaeef218 | |||
| 7915e365b3 | |||
| 1c68f81c37 | |||
| d4c9db0a27 | |||
| b5e0e29ec6 | |||
| 569be4c6c9 | |||
| e7ca237b07 | |||
| a7d3b72117 | |||
| 3ba7b5cf2d | |||
| 254e9765fe | |||
| 3304f284a5 | |||
| 0d5f862daf | |||
| 51dfacd542 | |||
| c691d52fa7 | |||
| de3d09fe79 | |||
| eaa413c566 | |||
| 407205e52b | |||
| 552414978e | |||
| 607081bbaa | |||
| e73383fbbb | |||
| 2e8e63fb0b | |||
| 874f7dd416 | |||
| 8b0e3b1624 | |||
| 9b946f2b7a | |||
| 11841f688b | |||
| 03c4f04246 | |||
| a1f06ae27f | |||
| 69dffd78a6 | |||
| 2750e19657 | |||
| b0987f727b | |||
| a4d9b0b468 | |||
| e2b87e0d74 | |||
| 2166b4ca17 | |||
| d2d4d68585 | |||
| faf68dbc5c | |||
| 305896fcb3 | |||
| 6c9adea0d3 | |||
| fc7630a66d | |||
| 3248153d9f | |||
| 246b9f3130 | |||
| 865144850d | |||
| d9752a4c21 | |||
| dba14d2630 | |||
| 2986e64a49 | |||
| 1d8bd11b67 | |||
| bafb99d520 | |||
| 3cc8a74a91 | |||
| c0aa5a438f | |||
| 825d401403 | |||
| 9dfca77c36 | |||
| 82c4af2902 | |||
| 736344e151 | |||
| 6f9663da62 | |||
| f8ae1786dd | |||
| c60ed17583 | |||
| e0f0b5bcf9 | |||
| 9e96073128 | |||
| 0db65c22d3 | |||
| d785135606 | |||
| ae15eda546 | |||
| f2d44ab5a7 | |||
| 43798fc2e8 | |||
| 9dc4b40d7a | |||
| 1cb19dbf65 | |||
| 1ab4289ad6 | |||
| e2ae1b249c | |||
| 92b7efafca | |||
| f092e4038f | |||
| aa5dae391b | |||
| 08a6fd4ad4 | |||
| a61150a96c | |||
| 0f9cb5590e | |||
| c0a83b27eb | |||
| f79b1f71b8 | |||
| 8e027c445f | |||
| dda3e3c39a | |||
| fd5157998c | |||
| e0217e8c79 | |||
| 3ab1c42266 | |||
| d1676776aa | |||
| bdde69d011 | |||
| 6dec42a33a | |||
| 199bc3f0ad | |||
| 17dd833925 | |||
| 4ec144c969 | |||
| 3e36f2b0ac | |||
| 07a03b3e73 | |||
| c33258832e | |||
| a7aa08ce07 | |||
| 06d63a862e | |||
| 43d1c4c4b5 | |||
| f81feb1e69 | |||
| 01cf01e084 | |||
| 97a725fbd0 | |||
| ace92ba281 | |||
| d631c76e8d | |||
| e6d33f77da | |||
| a6d3e3687b | |||
| 08c2bcb952 | |||
| 98ca4bdede | |||
| 3f8e741562 | |||
| 6e464ebd9b | |||
| c329279339 | |||
| cf04753ad7 | |||
| 69e7eab11f | |||
| dea206b023 | |||
| 5deaf58928 | |||
| 15e2952a2b | |||
| a9fba1c849 | |||
| 71e573d082 | |||
| 334a4fa159 | |||
| 21f94ee800 | |||
| 540bfd7a72 | |||
| 8fbed2b13a | |||
| aa17510e0a | |||
| bf65e8cd12 | |||
| 0f5c6e8f04 | |||
| b1b916ce15 | |||
| a6a732e1fc | |||
| a5c2f28539 | |||
| 18261fe31c | |||
| 079046863c | |||
| 07b965bba1 | |||
| c39113ee41 | |||
| 14f90502a4 | |||
| b0673c3563 | |||
| 373c6d8d55 | |||
| b8fc828955 | |||
| b43b040512 | |||
| 50b7608f9d | |||
| 7085e5b629 | |||
| 7d5985baf9 | |||
| 7c40a424c0 | |||
| baf882ace7 | |||
| ba82f0bef9 | |||
| d9c6a0305b | |||
| d9b1211191 | |||
| 99f1e02766 | |||
| 242c0db26b | |||
| dd49e41c42 | |||
| 6db15e8693 | |||
| 4c9cab3f8a | |||
| b2c0413a98 | |||
| e34c7c00b1 | |||
| 7c447bbdc7 | |||
| 7bf1f2cc84 | |||
| afa2c4e0af | |||
| 2ff7db1b36 | |||
| 9f0626da64 | |||
| d8cb5c1cf5 | |||
| dca56da0ef | |||
| ec75d16ea8 | |||
| 5cae8ea733 | |||
| 1ccd8f6a64 | |||
| 9c293bb82b | |||
| 9897ee9591 | |||
| 5215415315 | |||
| 54891d11e0 | |||
| 567c8303bf | |||
| 2a60edcd52 | |||
| d61ac32d7b | |||
| b57e6cff7e | |||
| 5b99f19dac | |||
| 6c03571887 | |||
| 4fb410a93c | |||
| 5e1db9fdd3 | |||
| 9d7480ae3c | |||
| f39cf6d855 | |||
| 001d116884 | |||
| 02c5e62efe | |||
| 446df07b62 | |||
| 8583b150c9 | |||
| a859aa72ee | |||
| 0896036266 | |||
| 311b78ae82 | |||
| f5cf4fc8fb | |||
| 7ceb58b2aa | |||
| 293dd76af1 | |||
| 3918c45ced | |||
| 4ec403347c | |||
| e01266ffcb | |||
| f246fb2fc2 | |||
| f7b26b34cb | |||
| a1bcdc225e | |||
| 7771241cc0 | |||
| 6e3af646b2 | |||
| 82bf8c138d | |||
| e21b001116 | |||
| 577024f1e9 | |||
| d4ad4a25db |
@@ -0,0 +1,20 @@
|
||||
root = true
|
||||
|
||||
[*.{sh,bash,fish}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
simplify = true
|
||||
binary_next_line = false
|
||||
switch_case_indent = true
|
||||
space_redirects = true
|
||||
function_next_line = false
|
||||
|
||||
# also bash scripts.
|
||||
[{install,uninstall,bin/fzf-preview.sh,bin/fzf-tmux}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
simplify = true
|
||||
binary_next_line = false
|
||||
switch_case_indent = true
|
||||
space_redirects = true
|
||||
function_next_line = false
|
||||
@@ -0,0 +1 @@
|
||||
* @junegunn
|
||||
@@ -0,0 +1 @@
|
||||
github: junegunn
|
||||
@@ -1,22 +0,0 @@
|
||||
<!-- ISSUES NOT FOLLOWING THIS TEMPLATE WILL BE CLOSED AND DELETED -->
|
||||
|
||||
<!-- Check all that apply [x] -->
|
||||
|
||||
- [ ] I have read through the manual page (`man fzf`)
|
||||
- [ ] I have the latest version of fzf
|
||||
- [ ] I have searched through the existing issues
|
||||
|
||||
## Info
|
||||
|
||||
- OS
|
||||
- [ ] Linux
|
||||
- [ ] Mac OS X
|
||||
- [ ] Windows
|
||||
- [ ] Etc.
|
||||
- Shell
|
||||
- [ ] bash
|
||||
- [ ] zsh
|
||||
- [ ] fish
|
||||
|
||||
## Problem / Steps to reproduce
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: Issue Template
|
||||
description: Report a problem or bug related to fzf to help us improve
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: ISSUES NOT FOLLOWING THIS TEMPLATE WILL BE CLOSED AND DELETED
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Checklist
|
||||
options:
|
||||
- label: I have read through the manual page (`man fzf`)
|
||||
required: true
|
||||
- label: I have searched through the existing issues
|
||||
required: true
|
||||
- label: For bug reports, I have checked if the bug is reproducible in the latest version of fzf
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Output of `fzf --version`
|
||||
placeholder: e.g. 0.48.1 (d579e33)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: OS
|
||||
options:
|
||||
- label: Linux
|
||||
- label: macOS
|
||||
- label: Windows
|
||||
- label: Etc.
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Shell
|
||||
options:
|
||||
- label: bash
|
||||
- label: zsh
|
||||
- label: fish
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Problem / Steps to reproduce
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,17 @@
|
||||
## Contribution Policy
|
||||
|
||||
We do not accept pull requests generated primarily by AI without genuine understanding or real-world usage context.
|
||||
|
||||
All contributions are expected to demonstrate:
|
||||
- A clear understanding of the codebase
|
||||
- Alignment with product direction
|
||||
- Thoughtful reasoning behind changes
|
||||
- Evidence of real-world usage or hands-on experience with the problem
|
||||
|
||||
If these expectations are not met, we would prefer to implement the changes ourselves rather than spend time reviewing low-effort submissions.
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgement
|
||||
|
||||
- [ ] I confirm that this PR meets the above expectations and reflects my own understanding and real-world context.
|
||||
@@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -0,0 +1,64 @@
|
||||
go:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- src/**
|
||||
- main.go
|
||||
- go.mod
|
||||
- go.sum
|
||||
|
||||
shell:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- shell/**
|
||||
|
||||
bash:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- shell/**/*.bash
|
||||
|
||||
zsh:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- shell/**/*.zsh
|
||||
|
||||
fish:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- shell/**/*.fish
|
||||
|
||||
vim:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- plugin/**
|
||||
|
||||
docs:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- '*.md'
|
||||
- doc/**
|
||||
- man/**
|
||||
|
||||
ci:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- .github/**
|
||||
|
||||
build:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- Makefile
|
||||
- .goreleaser.yml
|
||||
- Dockerfile
|
||||
|
||||
test:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- test/**
|
||||
- src/**/*_test.go
|
||||
|
||||
install:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- install
|
||||
- install.ps1
|
||||
- uninstall
|
||||
@@ -0,0 +1,44 @@
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning
|
||||
name: CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, devel ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
permissions:
|
||||
actions: read # for github/codeql-action/init to get workflow details
|
||||
contents: read # for actions/checkout to fetch code
|
||||
security-events: write # for github/codeql-action/autobuild to send a status report
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ['go']
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
@@ -0,0 +1,14 @@
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v5
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v5
|
||||
@@ -0,0 +1,17 @@
|
||||
name: Label PRs
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v6
|
||||
with:
|
||||
configuration-path: .github/labeler.yml
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, devel ]
|
||||
pull_request:
|
||||
branches: [ master, devel ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
LANG: C.UTF-8
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.23"
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@97ecb7b512899eb71ab1bf2310a624c6f1589ac6 # v1
|
||||
with:
|
||||
ruby-version: 3.4.6
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
sudo install -d -m 0755 /etc/apt/keyrings
|
||||
wget -qO- https://apt.fury.io/nushell/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/fury-nushell.gpg
|
||||
echo "deb [signed-by=/etc/apt/keyrings/fury-nushell.gpg] https://apt.fury.io/nushell/ /" | sudo tee /etc/apt/sources.list.d/fury-nushell.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes zsh fish tmux shfmt nushell
|
||||
|
||||
- name: Install Ruby gems
|
||||
run: bundle install
|
||||
|
||||
- name: Rubocop
|
||||
run: make lint
|
||||
|
||||
- name: Unit test
|
||||
run: make test
|
||||
|
||||
- name: Fuzz test
|
||||
run: |
|
||||
go test ./src/algo/ -fuzz=FuzzIndexByteTwo -fuzztime=5s
|
||||
go test ./src/algo/ -fuzz=FuzzLastIndexByteTwo -fuzztime=5s
|
||||
|
||||
- name: Integration test
|
||||
run: make install && ./install --all && tmux new-session -d && ruby test/runner.rb --verbose
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
name: Test fzf on macOS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, devel ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.23"
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@97ecb7b512899eb71ab1bf2310a624c6f1589ac6 # v1
|
||||
with:
|
||||
ruby-version: 3.0.0
|
||||
|
||||
- name: Install packages
|
||||
run: HOMEBREW_NO_INSTALL_CLEANUP=1 brew install fish zsh tmux shfmt
|
||||
|
||||
- name: Install Ruby gems
|
||||
run: gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1
|
||||
|
||||
- name: Rubocop
|
||||
run: rubocop --require rubocop-minitest --require rubocop-performance
|
||||
|
||||
- name: Unit test
|
||||
run: make test
|
||||
|
||||
- name: Integration test
|
||||
run: make install && ./install --all && LC_ALL=C tmux new-session -d && ruby test/test_go.rb --verbose
|
||||
@@ -0,0 +1,10 @@
|
||||
name: "Spell Check"
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
typos:
|
||||
name: Spell Check with Typos
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: crate-ci/typos@685eb3d55be2f85191e8c84acb9f44d7756f84ab # v1.29.4
|
||||
@@ -0,0 +1,14 @@
|
||||
name: Publish to Winget
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: vedantmgoyal2009/winget-releaser@4ffc7888bffd451b357355dc214d43bb9f23917e # v2
|
||||
with:
|
||||
identifier: junegunn.fzf
|
||||
installers-regex: '-windows_(armv7|arm64|amd64)\.zip$'
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
+5
-1
@@ -1,10 +1,14 @@
|
||||
bin/fzf
|
||||
bin/fzf.exe
|
||||
dist
|
||||
target
|
||||
pkg
|
||||
Gemfile.lock
|
||||
.DS_Store
|
||||
doc/tags
|
||||
vendor
|
||||
gopath
|
||||
*.zwc
|
||||
fzf
|
||||
tmp
|
||||
*.patch
|
||||
.idea
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
---
|
||||
version: 2
|
||||
project_name: fzf
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod download
|
||||
|
||||
builds:
|
||||
- id: fzf
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
- windows
|
||||
- freebsd
|
||||
- openbsd
|
||||
- android
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- loong64
|
||||
- ppc64le
|
||||
- s390x
|
||||
- riscv64
|
||||
goarm:
|
||||
- "5"
|
||||
- "6"
|
||||
- "7"
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
|
||||
ignore:
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
- goos: openbsd
|
||||
goarch: arm
|
||||
- goos: freebsd
|
||||
goarch: arm64
|
||||
- goos: openbsd
|
||||
goarch: arm64
|
||||
- goos: openbsd
|
||||
goarch: riscv64
|
||||
- goos: android
|
||||
goarch: amd64
|
||||
- goos: android
|
||||
goarch: arm
|
||||
|
||||
# .goreleaser.yaml
|
||||
notarize:
|
||||
macos:
|
||||
- # Whether this configuration is enabled or not.
|
||||
#
|
||||
# Default: false.
|
||||
# Templates: allowed.
|
||||
enabled: "{{ not .IsSnapshot }}"
|
||||
|
||||
# Before notarizing, we need to sign the binary.
|
||||
# This blocks defines the configuration for doing so.
|
||||
sign:
|
||||
# The .p12 certificate file path or its base64'd contents.
|
||||
certificate: "{{.Env.MACOS_SIGN_P12}}"
|
||||
|
||||
# The password to be used to open the certificate.
|
||||
password: "{{.Env.MACOS_SIGN_PASSWORD}}"
|
||||
|
||||
# Then, we notarize the binaries.
|
||||
notarize:
|
||||
# The issuer ID.
|
||||
# Its the UUID you see when creating the App Store Connect key.
|
||||
issuer_id: "{{.Env.MACOS_NOTARY_ISSUER_ID}}"
|
||||
|
||||
# Key ID.
|
||||
# You can see it in the list of App Store Connect Keys.
|
||||
# It will also be in the ApiKey filename.
|
||||
key_id: "{{.Env.MACOS_NOTARY_KEY_ID}}"
|
||||
|
||||
# The .p8 key file path or its base64'd contents.
|
||||
key: "{{.Env.MACOS_NOTARY_KEY}}"
|
||||
|
||||
# Whether to wait for the notarization to finish.
|
||||
# Not recommended, as it could take a really long time.
|
||||
wait: true
|
||||
|
||||
archives:
|
||||
- name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
ids:
|
||||
- fzf
|
||||
formats:
|
||||
- tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats:
|
||||
- zip
|
||||
files:
|
||||
- non-existent*
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: junegunn
|
||||
name: fzf
|
||||
prerelease: auto
|
||||
name_template: '{{ .Version }}'
|
||||
|
||||
snapshot:
|
||||
version_template: "{{ .Version }}-devel"
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- README
|
||||
- test
|
||||
@@ -0,0 +1,42 @@
|
||||
AllCops:
|
||||
NewCops: enable
|
||||
Layout/LineLength:
|
||||
Enabled: false
|
||||
Metrics:
|
||||
Enabled: false
|
||||
Lint/ShadowingOuterLocalVariable:
|
||||
Enabled: false
|
||||
Lint/NestedMethodDefinition:
|
||||
Enabled: false
|
||||
Style/MethodCallWithArgsParentheses:
|
||||
Enabled: true
|
||||
AllowedMethods:
|
||||
- assert
|
||||
- exit
|
||||
- paste
|
||||
- puts
|
||||
- raise
|
||||
- refute
|
||||
- require
|
||||
- send_keys
|
||||
AllowedPatterns:
|
||||
- ^assert_
|
||||
- ^refute_
|
||||
Style/NumericPredicate:
|
||||
Enabled: false
|
||||
Style/StringConcatenation:
|
||||
Enabled: false
|
||||
Style/OptionalBooleanParameter:
|
||||
Enabled: false
|
||||
Style/WordArray:
|
||||
MinSize: 1
|
||||
Minitest/AssertEqual:
|
||||
Enabled: false
|
||||
Minitest/EmptyLineBeforeAssertionMethods:
|
||||
Enabled: false
|
||||
Naming/VariableNumber:
|
||||
Enabled: false
|
||||
Lint/EmptyBlock:
|
||||
Enabled: false
|
||||
Style/SafeNavigationChainLength:
|
||||
Enabled: false
|
||||
@@ -0,0 +1,3 @@
|
||||
golang 1.23
|
||||
ruby 3.4
|
||||
shfmt 3.12
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
language: go
|
||||
dist: xenial
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- sourceline: "ppa:pi-rho/dev"
|
||||
- sourceline: "ppa:fish-shell/release-2"
|
||||
packages:
|
||||
- tmux
|
||||
- zsh
|
||||
- fish
|
||||
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- stage: unittest
|
||||
go: "1.13.x"
|
||||
script: make && make test
|
||||
|
||||
- stage: cli
|
||||
go: "1.13.x"
|
||||
rvm: "2.5"
|
||||
script: |
|
||||
make install && ./install --all && tmux new "ruby test/test_go.rb > out && touch ok" && cat out && [ -e ok ]
|
||||
|
||||
+714
@@ -0,0 +1,714 @@
|
||||
Advanced fzf examples
|
||||
======================
|
||||
|
||||
* *Last update: 2025/02/02*
|
||||
* *Requires fzf 0.59.0 or later*
|
||||
|
||||
---
|
||||
|
||||
<!-- vim-markdown-toc GFM -->
|
||||
|
||||
* [Introduction](#introduction)
|
||||
* [Display modes](#display-modes)
|
||||
* [`--height`](#--height)
|
||||
* [`--tmux`](#--tmux)
|
||||
* [Dynamic reloading of the list](#dynamic-reloading-of-the-list)
|
||||
* [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r)
|
||||
* [Toggling between data sources](#toggling-between-data-sources)
|
||||
* [Toggling with a single key binding](#toggling-with-a-single-key-binding)
|
||||
* [Ripgrep integration](#ripgrep-integration)
|
||||
* [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter)
|
||||
* [Using fzf as interactive Ripgrep launcher](#using-fzf-as-interactive-ripgrep-launcher)
|
||||
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
|
||||
* [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode)
|
||||
* [Switching between Ripgrep mode and fzf mode using a single key binding](#switching-between-ripgrep-mode-and-fzf-mode-using-a-single-key-binding)
|
||||
* [Controlling Ripgrep search and fzf search simultaneously](#controlling-ripgrep-search-and-fzf-search-simultaneously)
|
||||
* [Log tailing](#log-tailing)
|
||||
* [Key bindings for git objects](#key-bindings-for-git-objects)
|
||||
* [Files listed in `git status`](#files-listed-in-git-status)
|
||||
* [Branches](#branches)
|
||||
* [Commit hashes](#commit-hashes)
|
||||
* [Color themes](#color-themes)
|
||||
* [fzf Theme Playground](#fzf-theme-playground)
|
||||
* [Generating fzf color theme from Vim color schemes](#generating-fzf-color-theme-from-vim-color-schemes)
|
||||
|
||||
<!-- vim-markdown-toc -->
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
fzf is an interactive [Unix filter][filter] program that is designed to be
|
||||
used with other Unix tools. It reads a list of items from the standard input,
|
||||
allows you to select a subset of the items, and prints the selected ones to
|
||||
the standard output. You can think of it as an interactive version of *grep*,
|
||||
and it's already useful even if you don't know any of its options.
|
||||
|
||||
```sh
|
||||
# 1. ps: Feed the list of processes to fzf
|
||||
# 2. fzf: Interactively select a process using fuzzy matching algorithm
|
||||
# 3. awk: Take the PID from the selected line
|
||||
# 3. kill: Kill the process with the PID
|
||||
ps -ef | fzf | awk '{print $2}' | xargs kill -9
|
||||
```
|
||||
|
||||
[filter]: https://en.wikipedia.org/wiki/Filter_(software)
|
||||
|
||||
While the above example succinctly summarizes the fundamental concept of fzf,
|
||||
you can build much more sophisticated interactive workflows using fzf once you
|
||||
learn its wide variety of features.
|
||||
|
||||
- To see the full list of options and features, see `man fzf`
|
||||
- To see the latest additions, see [CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
This document will guide you through some examples that will familiarize you
|
||||
with the advanced features of fzf.
|
||||
|
||||
Display modes
|
||||
-------------
|
||||
|
||||
### `--height`
|
||||
|
||||
fzf by default opens in fullscreen mode, but it's not always desirable.
|
||||
Oftentimes, you want to see the current context of the terminal while using
|
||||
fzf. `--height` is an option for opening fzf below the cursor in
|
||||
non-fullscreen mode so you can still see the previous commands and their
|
||||
results above it.
|
||||
|
||||
```sh
|
||||
fzf --height=40%
|
||||
```
|
||||
|
||||

|
||||
|
||||
You might also want to experiment with other layout options such as
|
||||
`--layout=reverse`, `--info=inline`, `--border`, `--margin`, etc.
|
||||
|
||||
```sh
|
||||
fzf --height=40% --layout=reverse
|
||||
fzf --height=40% --layout=reverse --info=inline
|
||||
fzf --height=40% --layout=reverse --info=inline --border
|
||||
fzf --height=40% --layout=reverse --info=inline --border --margin=1
|
||||
fzf --height=40% --layout=reverse --info=inline --border --margin=1 --padding=1
|
||||
```
|
||||
|
||||

|
||||
|
||||
*(See man page to see the full list of options)*
|
||||
|
||||
But you definitely don't want to repeat `--height=40% --layout=reverse
|
||||
--info=inline --border --margin=1 --padding=1` every time you use fzf. You
|
||||
could write a wrapper script or shell alias, but there is an easier option.
|
||||
Define `$FZF_DEFAULT_OPTS` like so:
|
||||
|
||||
```sh
|
||||
export FZF_DEFAULT_OPTS="--height=40% --layout=reverse --info=inline --border --margin=1 --padding=1"
|
||||
```
|
||||
|
||||
### `--tmux`
|
||||
|
||||
(Requires tmux 3.3 or later)
|
||||
|
||||
If you're using tmux, you can open fzf in a tmux popup using `--tmux` option.
|
||||
|
||||
```sh
|
||||
# Open fzf in a tmux popup at the center of the screen with 70% width and height
|
||||
fzf --tmux 70%
|
||||
```
|
||||
|
||||

|
||||
|
||||
`--tmux` option is silently ignored if you're not on tmux. So if you're trying
|
||||
to avoid opening fzf in fullscreen, try specifying both `--height` and `--tmux`.
|
||||
|
||||
```sh
|
||||
# --tmux is specified later so it takes precedence over --height when on tmux.
|
||||
# If you're not on tmux, --tmux is ignored and --height is used instead.
|
||||
fzf --height 70% --tmux 70%
|
||||
```
|
||||
|
||||
You can also specify the position, width, and height of the popup window in
|
||||
the following format:
|
||||
|
||||
* `[center|top|bottom|left|right][,SIZE[%]][,SIZE[%][,border-native]]`
|
||||
|
||||
```sh
|
||||
# 100% width and 60% height
|
||||
fzf --tmux 100%,60% --border horizontal
|
||||
```
|
||||
|
||||

|
||||
|
||||
```sh
|
||||
# On the right (50% width)
|
||||
fzf --tmux right
|
||||
```
|
||||
|
||||

|
||||
|
||||
```sh
|
||||
# On the left (40% width and 70% height)
|
||||
fzf --tmux left,40%,70%
|
||||
```
|
||||
|
||||

|
||||
|
||||
> [!TIP]
|
||||
> You might also want to check out my tmux plugins which support this popup
|
||||
> window layout.
|
||||
>
|
||||
> - https://github.com/junegunn/tmux-fzf-url
|
||||
> - https://github.com/junegunn/tmux-fzf-maccy
|
||||
|
||||
Dynamic reloading of the list
|
||||
-----------------------------
|
||||
|
||||
fzf can dynamically update the candidate list using an arbitrary program with
|
||||
`reload` bindings (The design document for `reload` can be found
|
||||
[here][reload]).
|
||||
|
||||
[reload]: https://github.com/junegunn/fzf/issues/1750
|
||||
|
||||
### Updating the list of processes by pressing CTRL-R
|
||||
|
||||
This example shows how you can set up a binding for dynamically updating the
|
||||
list without restarting fzf.
|
||||
|
||||
```sh
|
||||
(date; ps -ef) |
|
||||
fzf --bind='ctrl-r:reload(date; ps -ef)' \
|
||||
--header=$'Press CTRL-R to reload\n\n' --header-lines=2 \
|
||||
--preview='echo {}' --preview-window=down,3,wrap \
|
||||
--layout=reverse --height=80% | awk '{print $2}' | xargs kill -9
|
||||
```
|
||||
|
||||

|
||||
|
||||
- The initial command is `(date; ps -ef)`. It prints the current date and
|
||||
time, and the list of the processes.
|
||||
- With `--header` option, you can show any message as the fixed header.
|
||||
- To disallow selecting the first two lines (`date` and `ps` header), we use
|
||||
`--header-lines=2` option.
|
||||
- `--bind='ctrl-r:reload(date; ps -ef)'` binds CTRL-R to `reload` action that
|
||||
runs `date; ps -ef`, so we can update the list of the processes by pressing
|
||||
CTRL-R.
|
||||
- We use simple `echo {}` preview option, so we can see the entire line on the
|
||||
preview window below even if it's too long
|
||||
|
||||
### Toggling between data sources
|
||||
|
||||
You're not limited to just one reload binding. Set up multiple bindings so
|
||||
you can switch between data sources.
|
||||
|
||||
```sh
|
||||
find * | fzf --prompt 'All> ' \
|
||||
--header 'CTRL-D: Directories / CTRL-F: Files' \
|
||||
--bind 'ctrl-d:change-prompt(Directories> )+reload(find * -type d)' \
|
||||
--bind 'ctrl-f:change-prompt(Files> )+reload(find * -type f)'
|
||||
```
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### Toggling with a single key binding
|
||||
|
||||
The above example uses two different key bindings to toggle between two modes,
|
||||
but can we just use a single key binding?
|
||||
|
||||
To make a key binding behave differently each time it is pressed, we need:
|
||||
|
||||
1. a way to store the current state. i.e. "which mode are we in?"
|
||||
2. and a way to dynamically perform different actions depending on the state.
|
||||
|
||||
The following example shows how to 1. store the current mode in the prompt
|
||||
string, 2. and use this information (`$FZF_PROMPT`) to determine which
|
||||
actions to perform using the `transform` action.
|
||||
|
||||
```sh
|
||||
fd --type file |
|
||||
fzf --prompt 'Files> ' \
|
||||
--header 'CTRL-T: Switch between Files/Directories' \
|
||||
--bind 'ctrl-t:transform:[[ ! $FZF_PROMPT =~ Files ]] &&
|
||||
echo "change-prompt(Files> )+reload(fd --type file)" ||
|
||||
echo "change-prompt(Directories> )+reload(fd --type directory)"' \
|
||||
--preview '[[ $FZF_PROMPT =~ Files ]] && bat --color=always {} || tree -C {}'
|
||||
```
|
||||
|
||||
Ripgrep integration
|
||||
-------------------
|
||||
|
||||
### Using fzf as the secondary filter
|
||||
|
||||
* Requires [bat][bat]
|
||||
* Requires [Ripgrep][rg]
|
||||
|
||||
[bat]: https://github.com/sharkdp/bat
|
||||
[rg]: https://github.com/BurntSushi/ripgrep
|
||||
|
||||
fzf is pretty fast for filtering a list that you will rarely have to think
|
||||
about its performance. But it is not the right tool for searching for text
|
||||
inside many large files, and in that case you should definitely use something
|
||||
like [Ripgrep][rg].
|
||||
|
||||
In the next example, Ripgrep is the primary filter that searches for the given
|
||||
text in files, and fzf is used as the secondary fuzzy filter that adds
|
||||
interactivity to the workflow. And we use [bat][bat] to show the matching line in
|
||||
the preview window.
|
||||
|
||||
This is a bash script and it will not run as expected on other non-compliant
|
||||
shells. To avoid the compatibility issue, let's save this snippet as a script
|
||||
file called `rfv`.
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# 1. Search for text in files using Ripgrep
|
||||
# 2. Interactively narrow down the list using fzf
|
||||
# 3. Open the file in Vim
|
||||
rg --color=always --line-number --no-heading --smart-case "${*:-}" |
|
||||
fzf --ansi \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--delimiter : \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
And run it with an initial query string.
|
||||
|
||||
```sh
|
||||
# Make the script executable
|
||||
chmod +x rfv
|
||||
|
||||
# Run it with the initial query "algo"
|
||||
./rfv algo
|
||||
```
|
||||
|
||||
> Ripgrep will perform the initial search and list all the lines that contain
|
||||
`algo`. Then we further narrow down the list on fzf.
|
||||
|
||||

|
||||
|
||||
I know it's a lot to digest, let's try to break down the code.
|
||||
|
||||
- Ripgrep prints the matching lines in the following format
|
||||
```
|
||||
man/man1/fzf.1:54:.BI "--algo=" TYPE
|
||||
man/man1/fzf.1:55:Fuzzy matching algorithm (default: v2)
|
||||
man/man1/fzf.1:58:.BR v2 " Optimal scoring algorithm (quality)"
|
||||
src/pattern_test.go:7: "github.com/junegunn/fzf/src/algo"
|
||||
```
|
||||
The first token delimited by `:` is the file path, and the second token is
|
||||
the line number of the matching line. They respectively correspond to `{1}`
|
||||
and `{2}` in the preview command.
|
||||
- `--preview 'bat --color=always {1} --highlight-line {2}'`
|
||||
- As we run `rg` with `--color=always` option, we should tell fzf to parse
|
||||
ANSI color codes in the input by setting `--ansi`.
|
||||
- We customize how fzf colors various text elements using `--color` option.
|
||||
`-1` tells fzf to keep the original color from the input. See `man fzf` for
|
||||
available color options.
|
||||
- The value of `--preview-window` option consists of 5 components delimited
|
||||
by `,`
|
||||
1. `up` -- Position of the preview window
|
||||
1. `60%` -- Size of the preview window
|
||||
1. `border-bottom` -- Preview window border only on the bottom side
|
||||
1. `+{2}+3/3` -- Scroll offset of the preview contents
|
||||
1. `~3` -- Fixed header
|
||||
- Let's break down the latter two. We want to display the bat output in the
|
||||
preview window with a certain scroll offset so that the matching line is
|
||||
positioned near the center of the preview window.
|
||||
- `+{2}` -- The base offset is extracted from the second token
|
||||
- `+3` -- We add 3 lines to the base offset to compensate for the header
|
||||
part of `bat` output
|
||||
- ```
|
||||
───────┬──────────────────────────────────────────────────────────
|
||||
│ File: CHANGELOG.md
|
||||
───────┼──────────────────────────────────────────────────────────
|
||||
1 │ CHANGELOG
|
||||
2 │ =========
|
||||
3 │
|
||||
4 │ 0.26.0
|
||||
5 │ ------
|
||||
```
|
||||
- `/3` adjusts the offset so that the matching line is shown at a third
|
||||
position in the window
|
||||
- `~3` makes the top three lines fixed header so that they are always
|
||||
visible regardless of the scroll offset
|
||||
- Instead of using shell script to process the final output of fzf, we use
|
||||
`become(...)` action which was added in [fzf 0.38.0][0.38.0] to turn fzf
|
||||
into a new process that opens the file with `vim` (`vim {1}`) and move the
|
||||
cursor to the line (`+{2}`).
|
||||
|
||||
[0.38.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0380
|
||||
|
||||
### Using fzf as interactive Ripgrep launcher
|
||||
|
||||
We have learned that we can bind `reload` action to a key (e.g.
|
||||
`--bind=ctrl-r:execute(ps -ef)`). In the next example, we are going to **bind
|
||||
`reload` action to `change` event** so that whenever the user *changes* the
|
||||
query string on fzf, `reload` action is triggered.
|
||||
|
||||
Here is a variation of the above `rfv` script. fzf will restart Ripgrep every
|
||||
time the user updates the query string on fzf. Searching and filtering is
|
||||
completely done by Ripgrep, and fzf merely provides the interactive interface.
|
||||
So we lose the "fuzziness", but the performance will be better on larger
|
||||
projects, and it will free up memory as you narrow down the results.
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# 1. Search for text in files using Ripgrep
|
||||
# 2. Interactively restart Ripgrep with reload action
|
||||
# 3. Open the file in Vim
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload:$RG_PREFIX {q} || true" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--delimiter : \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||

|
||||
|
||||
- Instead of starting fzf in the usual `rg ... | fzf` form, we make it start
|
||||
the initial Ripgrep process immediately via `start:reload` binding for the
|
||||
consistency of the code.
|
||||
- Filtering is no longer a responsibility of fzf; hence `--disabled`
|
||||
- `{q}` in the reload command evaluates to the query string on fzf prompt.
|
||||
- `sleep 0.1` in the reload command is for "debouncing". This small delay will
|
||||
reduce the number of intermediate Ripgrep processes while we're typing in
|
||||
a query.
|
||||
|
||||
### Switching to fzf-only search mode
|
||||
|
||||
In the previous example, we lost fuzzy matching capability as we completely
|
||||
delegated search functionality to Ripgrep. But we can dynamically switch to
|
||||
fzf-only search mode by *"unbinding"* `reload` action from `change` event.
|
||||
|
||||
```sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Two-phase filtering with Ripgrep and fzf
|
||||
#
|
||||
# 1. Search for text in files using Ripgrep
|
||||
# 2. Interactively restart Ripgrep with reload action
|
||||
# * Press alt-enter to switch to fzf-only filtering
|
||||
# 3. Open the file in Vim
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload:$RG_PREFIX {q}" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--prompt '1. ripgrep> ' \
|
||||
--delimiter : \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
* Phase 1. Filtering with Ripgrep
|
||||

|
||||
* Phase 2. Filtering with fzf
|
||||

|
||||
|
||||
- We added `--prompt` option to show that fzf is initially running in "Ripgrep
|
||||
launcher mode".
|
||||
- We added `alt-enter` binding that
|
||||
1. unbinds `change` event, so Ripgrep is no longer restarted on key press
|
||||
2. changes the prompt to `2. fzf>`
|
||||
3. enables search functionality of fzf
|
||||
4. clears the current query string that was used to start Ripgrep process
|
||||
5. and unbinds `alt-enter` itself as this is a one-off event
|
||||
- We reverted `--color` option for customizing how the matching chunks are
|
||||
displayed in the second phase
|
||||
|
||||
### Switching between Ripgrep mode and fzf mode
|
||||
|
||||
[fzf 0.30.0][0.30.0] added `rebind` action so we can "rebind" the bindings
|
||||
that were previously "unbound" via `unbind`.
|
||||
|
||||
This is an improved version of the previous example that allows us to switch
|
||||
between Ripgrep launcher mode and fzf-only filtering mode via CTRL-R and
|
||||
CTRL-F.
|
||||
|
||||
```sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Switch between Ripgrep launcher mode (CTRL-R) and fzf filtering mode (CTRL-F)
|
||||
rm -f /tmp/rg-fzf-{r,f}
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload($RG_PREFIX {q})+unbind(ctrl-r)" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+rebind(ctrl-r)+transform-query(echo {q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f)" \
|
||||
--bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)+transform-query(echo {q} > /tmp/rg-fzf-f; cat /tmp/rg-fzf-r)" \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--prompt '1. ripgrep> ' \
|
||||
--delimiter : \
|
||||
--header '╱ CTRL-R (ripgrep mode) ╱ CTRL-F (fzf mode) ╱' \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
- To restore the query string when switching between modes, we store the
|
||||
current query in `/tmp/rg-fzf-{r,f}` files and restore the query using
|
||||
`transform-query` action which was added in [fzf 0.36.0][0.36.0].
|
||||
- Also note that we unbind `ctrl-r` binding on `start` event which is
|
||||
triggered once when fzf starts.
|
||||
|
||||
[0.30.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0300
|
||||
[0.36.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0360
|
||||
|
||||
### Switching between Ripgrep mode and fzf mode using a single key binding
|
||||
|
||||
In contrast to the previous version, we use just one hotkey to toggle between
|
||||
ripgrep and fzf mode. This is achieved by using the `$FZF_PROMPT` as a state
|
||||
within the `transform` action, a feature introduced in [fzf 0.45.0][0.45.0]. A
|
||||
more detailed explanation of this feature can be found in a previous section -
|
||||
[Toggling with a single keybinding](#toggling-with-a-single-key-binding).
|
||||
|
||||
[0.45.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0450
|
||||
|
||||
When using the `transform` action, the placeholder (`\{q}`) should be escaped to
|
||||
prevent immediate evaluation.
|
||||
|
||||
```sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Switch between Ripgrep mode and fzf filtering mode (CTRL-T)
|
||||
rm -f /tmp/rg-fzf-{r,f}
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload:$RG_PREFIX {q}" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--bind 'ctrl-t:transform:[[ ! $FZF_PROMPT =~ ripgrep ]] &&
|
||||
echo "rebind(change)+change-prompt(1. ripgrep> )+disable-search+transform-query:echo \{q} > /tmp/rg-fzf-f; cat /tmp/rg-fzf-r" ||
|
||||
echo "unbind(change)+change-prompt(2. fzf> )+enable-search+transform-query:echo \{q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f"' \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--prompt '1. ripgrep> ' \
|
||||
--delimiter : \
|
||||
--header 'CTRL-T: Switch between ripgrep/fzf' \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
### Controlling Ripgrep search and fzf search simultaneously
|
||||
|
||||
`search` and `transform-search` action allow you to trigger an fzf search with
|
||||
an arbitrary query string. This frees fzf from strictly following the prompt
|
||||
input, enabling custom search syntax.
|
||||
|
||||
In the example below, `transform` action is used to conditionally trigger
|
||||
`reload` for ripgrep, followed by `search` for fzf. The first word of the
|
||||
query initiates the Ripgrep process to generate the initial results, while the
|
||||
remainder of the query is passed to fzf for secondary filtering.
|
||||
|
||||
```sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export TEMP=$(mktemp -u)
|
||||
trap 'rm -f "$TEMP"' EXIT
|
||||
|
||||
INITIAL_QUERY="${*:-}"
|
||||
TRANSFORMER='
|
||||
rg_pat={q:1} # The first word is passed to ripgrep
|
||||
fzf_pat={q:2..} # The rest are passed to fzf
|
||||
|
||||
if ! [[ -r "$TEMP" ]] || [[ $rg_pat != $(cat "$TEMP") ]]; then
|
||||
echo "$rg_pat" > "$TEMP"
|
||||
printf "reload:sleep 0.1; rg --column --line-number --no-heading --color=always --smart-case %q || true" "$rg_pat"
|
||||
fi
|
||||
echo "+search:$fzf_pat"
|
||||
'
|
||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--with-shell 'bash -c' \
|
||||
--bind "start,change:transform:$TRANSFORMER" \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--delimiter : \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-line,+{2}+3/3,~3' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
Log tailing
|
||||
-----------
|
||||
|
||||
fzf can run long-running preview commands and render partial results before
|
||||
completion. And when you specify `follow` flag in `--preview-window` option,
|
||||
fzf will "`tail -f`" the result, automatically scrolling to the bottom.
|
||||
|
||||
```bash
|
||||
# With "follow", preview window will automatically scroll to the bottom.
|
||||
# "\033[2J" is an ANSI escape sequence for clearing the screen.
|
||||
# When fzf reads this code it clears the previous preview contents.
|
||||
fzf --preview-window follow --preview 'for i in $(seq 100000); do
|
||||
echo "$i"
|
||||
sleep 0.01
|
||||
(( i % 300 == 0 )) && printf "\033[2J"
|
||||
done'
|
||||
```
|
||||
|
||||

|
||||
|
||||
Admittedly, that was a silly example. Here's a practical one for browsing
|
||||
Kubernetes pods.
|
||||
|
||||
```bash
|
||||
pods() {
|
||||
command='kubectl get pods --all-namespaces' fzf \
|
||||
--info=inline --layout=reverse --header-lines=1 \
|
||||
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
|
||||
--header $'╱ Enter (kubectl exec) ╱ CTRL-O (open log in editor) ╱ CTRL-R (reload) ╱\n\n' \
|
||||
--bind 'start,ctrl-r:reload:$command' \
|
||||
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
|
||||
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash' \
|
||||
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2})' \
|
||||
--preview-window up:follow \
|
||||
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
- The preview window will *"log tail"* the pod
|
||||
- Holding on to a large amount of log will consume a lot of memory. So we
|
||||
limited the initial log amount with `--tail=10000`.
|
||||
- `execute` bindings allow you to run any command without leaving fzf
|
||||
- Press enter key on a pod to `kubectl exec` into it
|
||||
- Press CTRL-O to open the log in your editor
|
||||
- Press CTRL-R to reload the pod list
|
||||
- Press CTRL-/ repeatedly to rotate through a different sets of preview
|
||||
window options
|
||||
1. `80%,border-bottom`
|
||||
1. `hidden`
|
||||
1. Empty string after `|` translates to the default options from `--preview-window`
|
||||
|
||||
Key bindings for git objects
|
||||
----------------------------
|
||||
|
||||
Oftentimes, you want to put the identifiers of various Git object to the
|
||||
command-line. For example, it is common to write commands like these:
|
||||
|
||||
```sh
|
||||
git checkout [SOME_COMMIT_HASH or BRANCH or TAG]
|
||||
git diff [SOME_COMMIT_HASH or BRANCH or TAG] [SOME_COMMIT_HASH or BRANCH or TAG]
|
||||
```
|
||||
|
||||
[fzf-git.sh](https://github.com/junegunn/fzf-git.sh) project defines a set of
|
||||
fzf-based key bindings for Git objects. I strongly recommend that you check
|
||||
them out because they are seriously useful.
|
||||
|
||||
### Files listed in `git status`
|
||||
|
||||
<kbd>CTRL-G</kbd><kbd>CTRL-F</kbd>
|
||||
|
||||

|
||||
|
||||
### Branches
|
||||
|
||||
<kbd>CTRL-G</kbd><kbd>CTRL-B</kbd>
|
||||
|
||||

|
||||
|
||||
### Commit hashes
|
||||
|
||||
<kbd>CTRL-G</kbd><kbd>CTRL-H</kbd>
|
||||
|
||||

|
||||
|
||||
Color themes
|
||||
------------
|
||||
|
||||
You can customize how fzf colors the text elements with `--color` option. Here
|
||||
are a few color themes. Note that you need a terminal emulator that can
|
||||
display 24-bit colors.
|
||||
|
||||
```sh
|
||||
# junegunn/seoul256.vim (dark)
|
||||
export FZF_DEFAULT_OPTS='--color=bg+:#3F3F3F,bg:#4B4B4B,border:#6B6B6B,spinner:#98BC99,hl:#719872,fg:#D9D9D9,header:#719872,info:#BDBB72,pointer:#E12672,marker:#E17899,fg+:#D9D9D9,preview-bg:#3F3F3F,prompt:#98BEDE,hl+:#98BC99'
|
||||
```
|
||||
|
||||

|
||||
|
||||
```sh
|
||||
# junegunn/seoul256.vim (light)
|
||||
export FZF_DEFAULT_OPTS='--color=bg+:#D9D9D9,bg:#E1E1E1,border:#C8C8C8,spinner:#719899,hl:#719872,fg:#616161,header:#719872,info:#727100,pointer:#E12672,marker:#E17899,fg+:#616161,preview-bg:#D9D9D9,prompt:#0099BD,hl+:#719899'
|
||||
```
|
||||
|
||||

|
||||
|
||||
```sh
|
||||
# morhetz/gruvbox
|
||||
export FZF_DEFAULT_OPTS='--color=bg+:#3c3836,bg:#32302f,spinner:#fb4934,hl:#928374,fg:#ebdbb2,header:#928374,info:#8ec07c,pointer:#fb4934,marker:#fb4934,fg+:#ebdbb2,prompt:#fb4934,hl+:#fb4934'
|
||||
```
|
||||
|
||||

|
||||
|
||||
```sh
|
||||
# arcticicestudio/nord-vim
|
||||
export FZF_DEFAULT_OPTS='--color=bg+:#3B4252,bg:#2E3440,spinner:#81A1C1,hl:#616E88,fg:#D8DEE9,header:#616E88,info:#81A1C1,pointer:#81A1C1,marker:#81A1C1,fg+:#D8DEE9,prompt:#81A1C1,hl+:#81A1C1'
|
||||
```
|
||||
|
||||

|
||||
|
||||
```sh
|
||||
# tomasr/molokai
|
||||
export FZF_DEFAULT_OPTS='--color=bg+:#293739,bg:#1B1D1E,border:#808080,spinner:#E6DB74,hl:#7E8E91,fg:#F8F8F2,header:#7E8E91,info:#A6E22E,pointer:#A6E22E,marker:#F92672,fg+:#F8F8F2,prompt:#F92672,hl+:#F92672'
|
||||
```
|
||||
|
||||

|
||||
|
||||
### fzf Theme Playground
|
||||
|
||||
[fzf Theme Playground](https://vitormv.github.io/fzf-themes/) created by
|
||||
[Vitor Mello](https://github.com/vitormv) is a webpage where you can
|
||||
interactively create fzf themes.
|
||||
|
||||
### Generating fzf color theme from Vim color schemes
|
||||
|
||||
The Vim plugin of fzf can generate `--color` option from the current color
|
||||
scheme according to `g:fzf_colors` variable. You can find the detailed
|
||||
explanation [here](https://github.com/junegunn/fzf/blob/master/README-VIM.md#explanation-of-gfzf_colors).
|
||||
|
||||
Here is an example. Add this to your Vim configuration file.
|
||||
|
||||
```vim
|
||||
let g:fzf_colors =
|
||||
\ { 'fg': ['fg', 'Normal'],
|
||||
\ 'bg': ['bg', 'Normal'],
|
||||
\ 'preview-bg': ['bg', 'NormalFloat'],
|
||||
\ 'hl': ['fg', 'Comment'],
|
||||
\ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
|
||||
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
|
||||
\ 'hl+': ['fg', 'Statement'],
|
||||
\ 'info': ['fg', 'PreProc'],
|
||||
\ 'border': ['fg', 'Ignore'],
|
||||
\ 'prompt': ['fg', 'Conditional'],
|
||||
\ 'pointer': ['fg', 'Exception'],
|
||||
\ 'marker': ['fg', 'Keyword'],
|
||||
\ 'spinner': ['fg', 'Label'],
|
||||
\ 'header': ['fg', 'Comment'] }
|
||||
```
|
||||
|
||||
Then you can see how the `--color` option is generated by printing the result
|
||||
of `fzf#wrap()`.
|
||||
|
||||
```vim
|
||||
:echo fzf#wrap()
|
||||
```
|
||||
|
||||
Use this command to append `export FZF_DEFAULT_OPTS="..."` line to the end of
|
||||
the current file.
|
||||
|
||||
```vim
|
||||
:call append('$', printf('export FZF_DEFAULT_OPTS="%s"', matchstr(fzf#wrap().options, "--color[^']*")))
|
||||
```
|
||||
@@ -6,7 +6,7 @@ Build instructions
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.11 or above
|
||||
- Go 1.23 or above
|
||||
|
||||
### Using Makefile
|
||||
|
||||
@@ -17,33 +17,57 @@ make
|
||||
# Build fzf binary and copy it to bin directory
|
||||
make install
|
||||
|
||||
# Build 32-bit and 64-bit executables and tarballs in target
|
||||
make release
|
||||
# Build fzf binaries and archives for all platforms using goreleaser
|
||||
make build
|
||||
|
||||
# Make release archives for all supported platforms in target
|
||||
make release-all
|
||||
# Publish GitHub release
|
||||
make release
|
||||
```
|
||||
|
||||
### Using `go get`
|
||||
> [!WARNING]
|
||||
> Makefile uses git commands to determine the version and the revision
|
||||
> information for `fzf --version`. So if you're building fzf from an
|
||||
> environment where its git information is not available, you have to manually
|
||||
> set `$FZF_VERSION` and `$FZF_REVISION`.
|
||||
>
|
||||
> e.g. `FZF_VERSION=0.24.0 FZF_REVISION=tarball make`
|
||||
|
||||
Alternatively, you can build fzf directly with `go get` command without
|
||||
manually cloning the repository.
|
||||
> [!TIP]
|
||||
> To build fzf with profiling options enabled, set `TAGS=pprof`
|
||||
>
|
||||
> ```sh
|
||||
> TAGS=pprof make clean install
|
||||
> fzf --profile-cpu /tmp/cpu.pprof --profile-mem /tmp/mem.pprof \
|
||||
> --profile-block /tmp/block.pprof --profile-mutex /tmp/mutex.pprof
|
||||
> ```
|
||||
|
||||
Running tests
|
||||
-------------
|
||||
|
||||
```sh
|
||||
go get -u github.com/junegunn/fzf
|
||||
# Run go unit tests
|
||||
make test
|
||||
|
||||
# Run integration tests (requires to be on tmux)
|
||||
make itest
|
||||
|
||||
# Run a single test case
|
||||
ruby test/runner.rb --name test_something
|
||||
```
|
||||
|
||||
Third-party libraries used
|
||||
--------------------------
|
||||
|
||||
- [mattn/go-runewidth](https://github.com/mattn/go-runewidth)
|
||||
- Licensed under [MIT](http://mattn.mit-license.org)
|
||||
- [rivo/uniseg](https://github.com/rivo/uniseg)
|
||||
- Licensed under [MIT](https://raw.githubusercontent.com/rivo/uniseg/master/LICENSE.txt)
|
||||
- [mattn/go-shellwords](https://github.com/mattn/go-shellwords)
|
||||
- Licensed under [MIT](http://mattn.mit-license.org)
|
||||
- [mattn/go-isatty](https://github.com/mattn/go-isatty)
|
||||
- Licensed under [MIT](http://mattn.mit-license.org)
|
||||
- [tcell](https://github.com/gdamore/tcell)
|
||||
- Licensed under [Apache License 2.0](https://github.com/gdamore/tcell/blob/master/LICENSE)
|
||||
- [fastwalk](https://github.com/charlievieth/fastwalk)
|
||||
- Licensed under [MIT](https://raw.githubusercontent.com/charlievieth/fastwalk/master/LICENSE)
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
+2505
-1
File diff suppressed because it is too large
Load Diff
+5
-4
@@ -1,6 +1,6 @@
|
||||
FROM archlinux/base:latest
|
||||
RUN pacman -Sy && pacman --noconfirm -S awk git tmux zsh fish ruby procps go make
|
||||
RUN gem install --no-document minitest
|
||||
FROM rubylang/ruby:3.4.1-noble
|
||||
RUN apt-get update -y && apt install -y git make golang zsh fish tmux
|
||||
RUN gem install --no-document -v 5.22.3 minitest
|
||||
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
|
||||
RUN echo '. ~/.bashrc' >> ~/.bash_profile
|
||||
|
||||
@@ -8,4 +8,5 @@ RUN echo '. ~/.bashrc' >> ~/.bash_profile
|
||||
RUN rm -f /etc/bash.bashrc
|
||||
COPY . /fzf
|
||||
RUN cd /fzf && make install && ./install --all
|
||||
CMD tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ]
|
||||
ENV LANG=C.UTF-8
|
||||
CMD ["bash", "-ic", "tmux new 'set -o pipefail; ruby /fzf/test/runner.rb | tee out && touch ok' && cat out && [ -e ok ]"]
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'minitest', '5.25.4'
|
||||
gem 'rubocop', '1.71.0'
|
||||
gem 'rubocop-minitest', '0.36.0'
|
||||
gem 'rubocop-performance', '1.23.1'
|
||||
@@ -0,0 +1,47 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
ast (2.4.2)
|
||||
json (2.9.1)
|
||||
language_server-protocol (3.17.0.3)
|
||||
minitest (5.25.4)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.7.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
racc (1.8.1)
|
||||
rainbow (3.1.1)
|
||||
regexp_parser (2.10.0)
|
||||
rubocop (1.71.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.36.2, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.37.0)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-minitest (0.36.0)
|
||||
rubocop (>= 1.61, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-performance (1.23.1)
|
||||
rubocop (>= 1.48.1, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
ruby-progressbar (1.13.0)
|
||||
unicode-display_width (2.6.0)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-23
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
minitest (= 5.25.4)
|
||||
rubocop (= 1.71.0)
|
||||
rubocop-minitest (= 0.36.0)
|
||||
rubocop-performance (= 1.23.1)
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.2
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Junegunn Choi
|
||||
Copyright (c) 2013-2026 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,28 +1,51 @@
|
||||
GO ?= go
|
||||
GOOS ?= $(word 1, $(subst /, " ", $(word 4, $(shell go version))))
|
||||
DOCKER ?= docker
|
||||
GOOS ?= $(shell $(GO) env GOOS)
|
||||
|
||||
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
|
||||
ROOT_DIR := $(shell dirname $(MAKEFILE))
|
||||
SOURCES := $(wildcard *.go src/*.go src/*/*.go) $(MAKEFILE)
|
||||
SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh man/man1/*.1) $(MAKEFILE)
|
||||
|
||||
REVISION := $(shell git log -n 1 --pretty=format:%h -- $(SOURCES))
|
||||
BUILD_FLAGS := -a -ldflags "-X main.revision=$(REVISION) -w -extldflags=$(LDFLAGS)" -tags "$(TAGS)"
|
||||
BASH_SCRIPTS := $(ROOT_DIR)/bin/fzf-preview.sh \
|
||||
$(ROOT_DIR)/bin/fzf-tmux \
|
||||
$(ROOT_DIR)/install \
|
||||
$(ROOT_DIR)/uninstall \
|
||||
$(ROOT_DIR)/shell/common.sh \
|
||||
$(ROOT_DIR)/shell/update.sh \
|
||||
$(ROOT_DIR)/shell/completion.bash \
|
||||
$(ROOT_DIR)/shell/key-bindings.bash
|
||||
|
||||
ifdef FZF_VERSION
|
||||
VERSION := $(FZF_VERSION)
|
||||
else
|
||||
VERSION := $(shell git describe --abbrev=0 2> /dev/null | sed "s/^v//")
|
||||
endif
|
||||
ifeq ($(VERSION),)
|
||||
$(error Not on git repository; cannot determine $$FZF_VERSION)
|
||||
endif
|
||||
VERSION_TRIM := $(shell echo $(VERSION) | sed "s/^v//; s/-.*//")
|
||||
VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM))
|
||||
|
||||
ifdef FZF_REVISION
|
||||
REVISION := $(FZF_REVISION)
|
||||
else
|
||||
REVISION := $(shell git log -n 1 --pretty=format:%h --abbrev=8 -- $(SOURCES) 2> /dev/null)
|
||||
endif
|
||||
ifeq ($(REVISION),)
|
||||
$(error Not on git repository; cannot determine $$FZF_REVISION)
|
||||
endif
|
||||
BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision=$(REVISION)" -tags "$(TAGS)" -trimpath
|
||||
|
||||
BINARY32 := fzf-$(GOOS)_386
|
||||
BINARY64 := fzf-$(GOOS)_amd64
|
||||
BINARYS390 := fzf-$(GOOS)_s390x
|
||||
BINARYARM5 := fzf-$(GOOS)_arm5
|
||||
BINARYARM6 := fzf-$(GOOS)_arm6
|
||||
BINARYARM7 := fzf-$(GOOS)_arm7
|
||||
BINARYARM8 := fzf-$(GOOS)_arm8
|
||||
BINARYPPC64LE := fzf-$(GOOS)_ppc64le
|
||||
VERSION := $(shell awk -F= '/version =/ {print $$2}' src/constants.go | tr -d "\" ")
|
||||
RELEASE32 := fzf-$(VERSION)-$(GOOS)_386
|
||||
RELEASE64 := fzf-$(VERSION)-$(GOOS)_amd64
|
||||
RELEASEARM5 := fzf-$(VERSION)-$(GOOS)_arm5
|
||||
RELEASEARM6 := fzf-$(VERSION)-$(GOOS)_arm6
|
||||
RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7
|
||||
RELEASEARM8 := fzf-$(VERSION)-$(GOOS)_arm8
|
||||
RELEASEPPC64LE := fzf-$(VERSION)-$(GOOS)_ppc64le
|
||||
BINARYRISCV64 := fzf-$(GOOS)_riscv64
|
||||
BINARYLOONG64 := fzf-$(GOOS)_loong64
|
||||
|
||||
# https://en.wikipedia.org/wiki/Uname
|
||||
UNAME_M := $(shell uname -m)
|
||||
@@ -30,6 +53,10 @@ ifeq ($(UNAME_M),x86_64)
|
||||
BINARY := $(BINARY64)
|
||||
else ifeq ($(UNAME_M),amd64)
|
||||
BINARY := $(BINARY64)
|
||||
else ifeq ($(UNAME_M),i86pc)
|
||||
BINARY := $(BINARY64)
|
||||
else ifeq ($(UNAME_M),s390x)
|
||||
BINARY := $(BINARYS390)
|
||||
else ifeq ($(UNAME_M),i686)
|
||||
BINARY := $(BINARY32)
|
||||
else ifeq ($(UNAME_M),i386)
|
||||
@@ -41,49 +68,25 @@ else ifeq ($(UNAME_M),armv6l)
|
||||
else ifeq ($(UNAME_M),armv7l)
|
||||
BINARY := $(BINARYARM7)
|
||||
else ifeq ($(UNAME_M),armv8l)
|
||||
# armv8l is always 32-bit and should implement the armv7 ISA, so
|
||||
# just use the same filename as for armv7.
|
||||
BINARY := $(BINARYARM7)
|
||||
else ifeq ($(UNAME_M),arm64)
|
||||
BINARY := $(BINARYARM8)
|
||||
else ifeq ($(UNAME_M),aarch64)
|
||||
BINARY := $(BINARYARM8)
|
||||
else ifeq ($(UNAME_M),ppc64le)
|
||||
BINARY := $(BINARYPPC64LE)
|
||||
else ifeq ($(UNAME_M),riscv64)
|
||||
BINARY := $(BINARYRISCV64)
|
||||
else ifeq ($(UNAME_M),loongarch64)
|
||||
BINARY := $(BINARYLOONG64)
|
||||
else
|
||||
$(error "Build on $(UNAME_M) is not supported, yet.")
|
||||
$(error Build on $(UNAME_M) is not supported, yet.)
|
||||
endif
|
||||
|
||||
all: target/$(BINARY)
|
||||
|
||||
target:
|
||||
mkdir -p $@
|
||||
|
||||
ifeq ($(GOOS),windows)
|
||||
release: target/$(BINARY32) target/$(BINARY64)
|
||||
cd target && cp -f $(BINARY32) fzf.exe && zip $(RELEASE32).zip fzf.exe
|
||||
cd target && cp -f $(BINARY64) fzf.exe && zip $(RELEASE64).zip fzf.exe
|
||||
cd target && rm -f fzf.exe
|
||||
else ifeq ($(GOOS),linux)
|
||||
release: target/$(BINARY32) target/$(BINARY64) target/$(BINARYARM5) target/$(BINARYARM6) target/$(BINARYARM7) target/$(BINARYARM8) target/$(BINARYPPC64LE)
|
||||
cd target && cp -f $(BINARY32) fzf && tar -czf $(RELEASE32).tgz fzf
|
||||
cd target && cp -f $(BINARY64) fzf && tar -czf $(RELEASE64).tgz fzf
|
||||
cd target && cp -f $(BINARYARM5) fzf && tar -czf $(RELEASEARM5).tgz fzf
|
||||
cd target && cp -f $(BINARYARM6) fzf && tar -czf $(RELEASEARM6).tgz fzf
|
||||
cd target && cp -f $(BINARYARM7) fzf && tar -czf $(RELEASEARM7).tgz fzf
|
||||
cd target && cp -f $(BINARYARM8) fzf && tar -czf $(RELEASEARM8).tgz fzf
|
||||
cd target && cp -f $(BINARYPPC64LE) fzf && tar -czf $(RELEASEPPC64LE).tgz fzf
|
||||
cd target && rm -f fzf
|
||||
else
|
||||
release: target/$(BINARY32) target/$(BINARY64)
|
||||
cd target && cp -f $(BINARY32) fzf && tar -czf $(RELEASE32).tgz fzf
|
||||
cd target && cp -f $(BINARY64) fzf && tar -czf $(RELEASE64).tgz fzf
|
||||
cd target && rm -f fzf
|
||||
endif
|
||||
|
||||
release-all: clean test
|
||||
GOOS=darwin make release
|
||||
GOOS=linux make release
|
||||
GOOS=freebsd make release
|
||||
GOOS=openbsd make release
|
||||
GOOS=windows make release
|
||||
|
||||
test: $(SOURCES)
|
||||
SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
|
||||
github.com/junegunn/fzf/src \
|
||||
@@ -91,10 +94,71 @@ test: $(SOURCES)
|
||||
github.com/junegunn/fzf/src/tui \
|
||||
github.com/junegunn/fzf/src/util
|
||||
|
||||
itest:
|
||||
ruby test/runner.rb
|
||||
|
||||
bench:
|
||||
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
|
||||
|
||||
lint: $(SOURCES) test/*.rb test/lib/*.rb ${BASH_SCRIPTS}
|
||||
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
|
||||
bundle exec rubocop -a --require rubocop-minitest --require rubocop-performance
|
||||
shell/update.sh --check ${BASH_SCRIPTS}
|
||||
|
||||
fmt: $(SOURCES) $(BASH_SCRIPTS)
|
||||
gofmt -s -w src
|
||||
shell/update.sh ${BASH_SCRIPTS}
|
||||
|
||||
install: bin/fzf
|
||||
|
||||
generate:
|
||||
PATH=$(PATH):$(GOPATH)/bin $(GO) generate ./...
|
||||
|
||||
build:
|
||||
goreleaser build --clean --snapshot --skip=post-hooks
|
||||
|
||||
release:
|
||||
# Make sure that the tests pass and the build works
|
||||
TAGS=tcell make test
|
||||
make test build clean
|
||||
|
||||
ifndef GITHUB_TOKEN
|
||||
$(error GITHUB_TOKEN is not defined)
|
||||
endif
|
||||
|
||||
# Check if we are on master branch
|
||||
ifneq ($(shell git symbolic-ref --short HEAD),master)
|
||||
$(error Not on master branch)
|
||||
endif
|
||||
|
||||
# Check if version numbers are properly updated
|
||||
grep -q ^$(VERSION_REGEX)$$ CHANGELOG.md
|
||||
grep -qF '"fzf $(VERSION_TRIM)"' man/man1/fzf.1
|
||||
grep -qF '"fzf $(VERSION_TRIM)"' man/man1/fzf-tmux.1
|
||||
grep -qF $(VERSION) install
|
||||
grep -qF $(VERSION) install.ps1
|
||||
|
||||
# Make release note out of CHANGELOG.md
|
||||
mkdir -p tmp
|
||||
sed -n '/^$(VERSION_REGEX)$$/,/^[0-9]/p' CHANGELOG.md | tail -r | \
|
||||
sed '1,/^ *$$/d' | tail -r | sed 1,2d | tee tmp/release-note
|
||||
|
||||
# Push to temp branch first so that install scripts always works on master branch
|
||||
git checkout -B temp master
|
||||
git push origin temp --follow-tags --force
|
||||
|
||||
# Make a GitHub release
|
||||
goreleaser --clean --release-notes tmp/release-note
|
||||
|
||||
# Push to master
|
||||
git checkout master
|
||||
git push origin master
|
||||
|
||||
# Delete temp branch
|
||||
git push origin --delete temp
|
||||
|
||||
clean:
|
||||
$(RM) -r target
|
||||
$(RM) -r dist target
|
||||
|
||||
target/$(BINARY32): $(SOURCES)
|
||||
GOARCH=386 $(GO) build $(BUILD_FLAGS) -o $@
|
||||
@@ -102,6 +166,8 @@ target/$(BINARY32): $(SOURCES)
|
||||
target/$(BINARY64): $(SOURCES)
|
||||
GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@
|
||||
|
||||
target/$(BINARYS390): $(SOURCES)
|
||||
GOARCH=s390x $(GO) build $(BUILD_FLAGS) -o $@
|
||||
# https://github.com/golang/go/wiki/GoArm
|
||||
target/$(BINARYARM5): $(SOURCES)
|
||||
GOARCH=arm GOARM=5 $(GO) build $(BUILD_FLAGS) -o $@
|
||||
@@ -118,19 +184,26 @@ target/$(BINARYARM8): $(SOURCES)
|
||||
target/$(BINARYPPC64LE): $(SOURCES)
|
||||
GOARCH=ppc64le $(GO) build $(BUILD_FLAGS) -o $@
|
||||
|
||||
target/$(BINARYRISCV64): $(SOURCES)
|
||||
GOARCH=riscv64 $(GO) build $(BUILD_FLAGS) -o $@
|
||||
|
||||
target/$(BINARYLOONG64): $(SOURCES)
|
||||
GOARCH=loong64 $(GO) build $(BUILD_FLAGS) -o $@
|
||||
|
||||
bin/fzf: target/$(BINARY) | bin
|
||||
-rm -f bin/fzf
|
||||
cp -f target/$(BINARY) bin/fzf
|
||||
|
||||
docker:
|
||||
docker build -t fzf-arch .
|
||||
docker run -it fzf-arch tmux
|
||||
$(DOCKER) build -t fzf-ubuntu .
|
||||
$(DOCKER) run -it fzf-ubuntu tmux
|
||||
|
||||
docker-test:
|
||||
docker build -t fzf-arch .
|
||||
docker run -it fzf-arch
|
||||
$(DOCKER) build -t fzf-ubuntu .
|
||||
$(DOCKER) run -it fzf-ubuntu
|
||||
|
||||
update:
|
||||
$(GO) get -u
|
||||
$(GO) mod tidy
|
||||
|
||||
.PHONY: all release release-all test install clean docker docker-test update
|
||||
.PHONY: all generate build release test itest bench lint install clean docker docker-test update fmt
|
||||
|
||||
+224
-54
@@ -1,6 +1,56 @@
|
||||
FZF Vim integration
|
||||
===================
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Once you have fzf installed, you can enable it inside Vim simply by adding the
|
||||
directory to `&runtimepath` in your Vim configuration file. The path may
|
||||
differ depending on the package manager.
|
||||
|
||||
```vim
|
||||
" If installed using Homebrew
|
||||
set rtp+=/usr/local/opt/fzf
|
||||
|
||||
" If installed using Homebrew on Apple Silicon
|
||||
set rtp+=/opt/homebrew/opt/fzf
|
||||
|
||||
" If you have cloned fzf on ~/.fzf directory
|
||||
set rtp+=~/.fzf
|
||||
```
|
||||
|
||||
If you use [vim-plug](https://github.com/junegunn/vim-plug), the same can be
|
||||
written as:
|
||||
|
||||
```vim
|
||||
" If installed using Homebrew
|
||||
Plug '/usr/local/opt/fzf'
|
||||
|
||||
" If installed using Homebrew on Apple Silicon
|
||||
Plug '/opt/homebrew/opt/fzf'
|
||||
|
||||
" If you have cloned fzf on ~/.fzf directory
|
||||
Plug '~/.fzf'
|
||||
```
|
||||
|
||||
But if you want the latest Vim plugin file from GitHub rather than the one
|
||||
included in the package, write:
|
||||
|
||||
```vim
|
||||
Plug 'junegunn/fzf'
|
||||
```
|
||||
|
||||
The Vim plugin will pick up fzf binary available on the system. If fzf is not
|
||||
found on `$PATH`, it will ask you if it should download the latest binary for
|
||||
you.
|
||||
|
||||
To make sure that you have the latest version of the binary, set up
|
||||
post-update hook like so:
|
||||
|
||||
```vim
|
||||
Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }
|
||||
```
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
@@ -71,7 +121,7 @@ let g:fzf_action = {
|
||||
|
||||
" An action can be a reference to a function that processes selected lines
|
||||
function! s:build_quickfix_list(lines)
|
||||
call setqflist(map(copy(a:lines), '{ "filename": v:val }'))
|
||||
call setqflist(map(copy(a:lines), '{ "filename": v:val, "lnum": 1 }'))
|
||||
copen
|
||||
cc
|
||||
endfunction
|
||||
@@ -83,10 +133,19 @@ let g:fzf_action = {
|
||||
\ 'ctrl-v': 'vsplit' }
|
||||
|
||||
" Default fzf layout
|
||||
" - down / up / left / right
|
||||
let g:fzf_layout = { 'down': '~40%' }
|
||||
" - Popup window (center of the screen)
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||
|
||||
" You can set up fzf window using a Vim command (Neovim or latest Vim 8 required)
|
||||
" - Popup window (center of the current window)
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true } }
|
||||
|
||||
" - Popup window (anchored to the bottom of the current window)
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true, 'yoffset': 1.0 } }
|
||||
|
||||
" - down / up / left / right
|
||||
let g:fzf_layout = { 'down': '40%' }
|
||||
|
||||
" - Window using a Vim command
|
||||
let g:fzf_layout = { 'window': 'enew' }
|
||||
let g:fzf_layout = { 'window': '-tabnew' }
|
||||
let g:fzf_layout = { 'window': '10new' }
|
||||
@@ -96,6 +155,7 @@ let g:fzf_layout = { 'window': '10new' }
|
||||
let g:fzf_colors =
|
||||
\ { 'fg': ['fg', 'Normal'],
|
||||
\ 'bg': ['bg', 'Normal'],
|
||||
\ 'query': ['fg', 'Normal'],
|
||||
\ 'hl': ['fg', 'Comment'],
|
||||
\ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
|
||||
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
|
||||
@@ -115,6 +175,57 @@ let g:fzf_colors =
|
||||
let g:fzf_history_dir = '~/.local/share/fzf-history'
|
||||
```
|
||||
|
||||
##### Explanation of `g:fzf_colors`
|
||||
|
||||
`g:fzf_colors` is a dictionary mapping fzf elements to a color specification
|
||||
list:
|
||||
|
||||
element: [ component, group1 [, group2, ...] ]
|
||||
|
||||
- `element` is an fzf element to apply a color to:
|
||||
|
||||
| Element | Description |
|
||||
| --- | --- |
|
||||
| `fg` / `bg` / `hl` | Item (foreground / background / highlight) |
|
||||
| `fg+` / `bg+` / `hl+` | Current item (foreground / background / highlight) |
|
||||
| `preview-fg` / `preview-bg` | Preview window text and background |
|
||||
| `hl` / `hl+` | Highlighted substrings (normal / current) |
|
||||
| `gutter` | Background of the gutter on the left |
|
||||
| `pointer` | Pointer to the current line (`>`) |
|
||||
| `marker` | Multi-select marker (`>`) |
|
||||
| `border` | Border around the window (`--border` and `--preview`) |
|
||||
| `header` | Header (`--header` or `--header-lines`) |
|
||||
| `info` | Info line (match counters) |
|
||||
| `spinner` | Streaming input indicator |
|
||||
| `query` | Query string |
|
||||
| `disabled` | Query string when search is disabled |
|
||||
| `prompt` | Prompt before query (`> `) |
|
||||
| `pointer` | Pointer to the current line (`>`) |
|
||||
|
||||
- `component` specifies the component (`fg` / `bg`) from which to extract the
|
||||
color when considering each of the following highlight groups
|
||||
|
||||
- `group1 [, group2, ...]` is a list of highlight groups that are searched (in
|
||||
order) for a matching color definition
|
||||
|
||||
For example, consider the following specification:
|
||||
|
||||
```vim
|
||||
'prompt': ['fg', 'Conditional', 'Comment'],
|
||||
```
|
||||
|
||||
This means we color the **prompt**
|
||||
- using the `fg` attribute of the `Conditional` if it exists,
|
||||
- otherwise use the `fg` attribute of the `Comment` highlight group if it exists,
|
||||
- otherwise fall back to the default color settings for the **prompt**.
|
||||
|
||||
You can examine the color option generated according the setting by printing
|
||||
the result of `fzf#wrap()` function like so:
|
||||
|
||||
```vim
|
||||
:echo fzf#wrap()
|
||||
```
|
||||
|
||||
`fzf#run`
|
||||
---------
|
||||
|
||||
@@ -128,19 +239,20 @@ call fzf#run({'sink': 'e'})
|
||||
```
|
||||
|
||||
We haven't specified the `source`, so this is equivalent to starting fzf on
|
||||
command line without standard input pipe; fzf will use find command (or
|
||||
`$FZF_DEFAULT_COMMAND` if defined) to list the files under the current
|
||||
directory. When you select one, it will open it with the sink, `:e` command.
|
||||
If you want to open it in a new tab, you can pass `:tabedit` command instead
|
||||
as the sink.
|
||||
command line without standard input pipe; fzf will traverse the file system
|
||||
under the current directory to get the list of files. (If
|
||||
`$FZF_DEFAULT_COMMAND` is set, fzf will use the output of the command
|
||||
instead.) When you select one, it will open it with the sink, `:e` command. If
|
||||
you want to open it in a new tab, you can pass `:tabedit` command instead as
|
||||
the sink.
|
||||
|
||||
```vim
|
||||
call fzf#run({'sink': 'tabedit'})
|
||||
```
|
||||
|
||||
Instead of using the default find command, you can use any shell command as
|
||||
the source. The following example will list the files managed by git. It's
|
||||
equivalent to running `git ls-files | fzf` on shell.
|
||||
You can use any shell command as the source to generate the list. The
|
||||
following example will list the files managed by git. It's equivalent to
|
||||
running `git ls-files | fzf` on shell.
|
||||
|
||||
```vim
|
||||
call fzf#run({'source': 'git ls-files', 'sink': 'e'})
|
||||
@@ -178,12 +290,15 @@ The following table summarizes the available options.
|
||||
| `source` | string | External command to generate input to fzf (e.g. `find .`) |
|
||||
| `source` | list | Vim list as input to fzf |
|
||||
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
|
||||
| `sink` | funcref | Reference to function to process each selected item |
|
||||
| `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once |
|
||||
| `sink` | funcref | Function to be called with each selected item |
|
||||
| `sinklist` (or `sink*`) | funcref | Similar to `sink`, but takes the list of output lines at once |
|
||||
| `exit` | funcref | Function to be called with the exit status of fzf (e.g. 0, 1, 2, 130) |
|
||||
| `options` | string/list | Options to fzf |
|
||||
| `dir` | string | Working directory |
|
||||
| `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) |
|
||||
| `tmux` | string | (Layout) `--tmux` options (e.g. `90%,70%`) |
|
||||
| `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new`) |
|
||||
| `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}`) |
|
||||
|
||||
`options` entry can be either a string or a list. For simple cases, string
|
||||
should suffice, but prefer to use list type to avoid escaping issues.
|
||||
@@ -193,6 +308,19 @@ call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'})
|
||||
call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']})
|
||||
```
|
||||
|
||||
When `window` entry is a dictionary, fzf will start in a popup window. The
|
||||
following options are allowed:
|
||||
|
||||
- Required:
|
||||
- `width` [float range [0 ~ 1]] or [integer range [8 ~ ]]
|
||||
- `height` [float range [0 ~ 1]] or [integer range [4 ~ ]]
|
||||
- Optional:
|
||||
- `yoffset` [float default 0.5 range [0 ~ 1]]
|
||||
- `xoffset` [float default 0.5 range [0 ~ 1]]
|
||||
- `relative` [boolean default v:false]
|
||||
- `border` [string default `rounded` (`sharp` on Windows)]: Border style
|
||||
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]`
|
||||
|
||||
`fzf#wrap`
|
||||
----------
|
||||
|
||||
@@ -225,8 +353,9 @@ After we *"wrap"* our spec, we pass it to `fzf#run`.
|
||||
call fzf#run(fzf#wrap({'source': 'ls'}))
|
||||
```
|
||||
|
||||
Now it supports `CTRL-T`, `CTRL-V`, and `CTRL-X` key bindings and it opens fzf
|
||||
window according to `g:fzf_layout` setting.
|
||||
Now it supports `CTRL-T`, `CTRL-V`, and `CTRL-X` key bindings (configurable
|
||||
via `g:fzf_action`) and it opens fzf window according to `g:fzf_layout`
|
||||
setting.
|
||||
|
||||
To make it easier to use, let's define `LS` command.
|
||||
|
||||
@@ -249,7 +378,7 @@ Our `:LS` command will be much more useful if we can pass a directory argument
|
||||
to it, so that something like `:LS /tmp` is possible.
|
||||
|
||||
```vim
|
||||
command! -bang -complete=dir -nargs=* LS
|
||||
command! -bang -complete=dir -nargs=? LS
|
||||
\ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0))
|
||||
```
|
||||
|
||||
@@ -259,63 +388,104 @@ a unique name to our command and pass it as the first argument to `fzf#wrap`.
|
||||
```vim
|
||||
" The query history for this command will be stored as 'ls' inside g:fzf_history_dir.
|
||||
" The name is ignored if g:fzf_history_dir is not defined.
|
||||
command! -bang -complete=dir -nargs=* LS
|
||||
command! -bang -complete=dir -nargs=? LS
|
||||
\ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': <q-args>}, <bang>0))
|
||||
```
|
||||
|
||||
### Global options supported by `fzf#wrap`
|
||||
|
||||
- `g:fzf_layout`
|
||||
- `g:fzf_action`
|
||||
- **Works only when no custom `sink` (or `sinklist`) is provided**
|
||||
- Having custom sink usually means that each entry is not an ordinary
|
||||
file path (e.g. name of color scheme), so we can't blindly apply the
|
||||
same strategy (i.e. `tabedit some-color-scheme` doesn't make sense)
|
||||
- `g:fzf_colors`
|
||||
- `g:fzf_history_dir`
|
||||
|
||||
Tips
|
||||
----
|
||||
|
||||
### fzf inside terminal buffer
|
||||
|
||||
The latest versions of Vim and Neovim include builtin terminal emulator
|
||||
(`:terminal`) and fzf will start in a terminal buffer in the following cases:
|
||||
|
||||
- On Neovim
|
||||
- On GVim
|
||||
- On Terminal Vim with a non-default layout
|
||||
- `call fzf#run({'left': '30%'})` or `let g:fzf_layout = {'left': '30%'}`
|
||||
|
||||
#### Starting fzf in Neovim floating window
|
||||
On the latest versions of Vim and Neovim, fzf will start in a terminal buffer.
|
||||
If you find the default ANSI colors to be different, consider configuring the
|
||||
colors using `g:terminal_ansi_colors` in regular Vim or `g:terminal_color_x`
|
||||
in Neovim.
|
||||
|
||||
```vim
|
||||
" Using floating windows of Neovim to start fzf
|
||||
" Terminal colors for seoul256 color scheme
|
||||
if has('nvim')
|
||||
let $FZF_DEFAULT_OPTS .= ' --border --margin=0,2'
|
||||
|
||||
function! FloatingFZF()
|
||||
let width = float2nr(&columns * 0.9)
|
||||
let height = float2nr(&lines * 0.6)
|
||||
let opts = { 'relative': 'editor',
|
||||
\ 'row': (&lines - height) / 2,
|
||||
\ 'col': (&columns - width) / 2,
|
||||
\ 'width': width,
|
||||
\ 'height': height }
|
||||
|
||||
let win = nvim_open_win(nvim_create_buf(v:false, v:true), v:true, opts)
|
||||
call setwinvar(win, '&winhighlight', 'NormalFloat:Normal')
|
||||
endfunction
|
||||
|
||||
let g:fzf_layout = { 'window': 'call FloatingFZF()' }
|
||||
let g:terminal_color_0 = '#4e4e4e'
|
||||
let g:terminal_color_1 = '#d68787'
|
||||
let g:terminal_color_2 = '#5f865f'
|
||||
let g:terminal_color_3 = '#d8af5f'
|
||||
let g:terminal_color_4 = '#85add4'
|
||||
let g:terminal_color_5 = '#d7afaf'
|
||||
let g:terminal_color_6 = '#87afaf'
|
||||
let g:terminal_color_7 = '#d0d0d0'
|
||||
let g:terminal_color_8 = '#626262'
|
||||
let g:terminal_color_9 = '#d75f87'
|
||||
let g:terminal_color_10 = '#87af87'
|
||||
let g:terminal_color_11 = '#ffd787'
|
||||
let g:terminal_color_12 = '#add4fb'
|
||||
let g:terminal_color_13 = '#ffafaf'
|
||||
let g:terminal_color_14 = '#87d7d7'
|
||||
let g:terminal_color_15 = '#e4e4e4'
|
||||
else
|
||||
let g:terminal_ansi_colors = [
|
||||
\ '#4e4e4e', '#d68787', '#5f865f', '#d8af5f',
|
||||
\ '#85add4', '#d7afaf', '#87afaf', '#d0d0d0',
|
||||
\ '#626262', '#d75f87', '#87af87', '#ffd787',
|
||||
\ '#add4fb', '#ffafaf', '#87d7d7', '#e4e4e4'
|
||||
\ ]
|
||||
endif
|
||||
|
||||
```
|
||||
|
||||
#### Hide statusline
|
||||
### Starting fzf in a popup window
|
||||
|
||||
```vim
|
||||
" Required:
|
||||
" - width [float range [0 ~ 1]] or [integer range [8 ~ ]]
|
||||
" - height [float range [0 ~ 1]] or [integer range [4 ~ ]]
|
||||
"
|
||||
" Optional:
|
||||
" - xoffset [float default 0.5 range [0 ~ 1]]
|
||||
" - yoffset [float default 0.5 range [0 ~ 1]]
|
||||
" - relative [boolean default v:false]
|
||||
" - border [string default 'rounded']: Border style
|
||||
" - 'rounded' / 'sharp' / 'horizontal' / 'vertical' / 'top' / 'bottom' / 'left' / 'right'
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||
```
|
||||
|
||||
Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2
|
||||
or above) by putting `--tmux` option value in `tmux` key.
|
||||
|
||||
```vim
|
||||
" See `--tmux` option in `man fzf` for available options
|
||||
" [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
||||
if exists('$TMUX')
|
||||
let g:fzf_layout = { 'tmux': '90%,70%' }
|
||||
else
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||
endif
|
||||
```
|
||||
|
||||
### Hide statusline
|
||||
|
||||
When fzf starts in a terminal buffer, the file type of the buffer is set to
|
||||
`fzf`. So you can set up `FileType fzf` autocmd to customize the settings of
|
||||
the window.
|
||||
|
||||
For example, if you use the default layout (`{'down': '~40%'}`) on Neovim, you
|
||||
might want to temporarily disable the statusline for a cleaner look.
|
||||
For example, if you open fzf on the bottom on the screen (e.g. `{'down':
|
||||
'40%'}`), you might want to temporarily disable the statusline for a cleaner
|
||||
look.
|
||||
|
||||
```vim
|
||||
if has('nvim') && !exists('g:fzf_layout')
|
||||
autocmd! FileType fzf
|
||||
autocmd FileType fzf set laststatus=0 noshowmode noruler
|
||||
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
|
||||
endif
|
||||
let g:fzf_layout = { 'down': '30%' }
|
||||
autocmd! FileType fzf
|
||||
autocmd FileType fzf set laststatus=0 noshowmode noruler
|
||||
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
|
||||
```
|
||||
|
||||
[License](LICENSE)
|
||||
@@ -323,4 +493,4 @@ endif
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019 Junegunn Choi
|
||||
Copyright (c) 2013-2026 Junegunn Choi
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# Security Reporting
|
||||
|
||||
If you wish to report a security vulnerability privately, we appreciate your diligence. Please follow the guidelines below to submit your report.
|
||||
|
||||
## Reporting
|
||||
|
||||
To report a security vulnerability, please provide the following information:
|
||||
|
||||
1. **PROJECT**
|
||||
- https://github.com/junegunn/fzf
|
||||
|
||||
2. **PUBLIC**
|
||||
- Indicate whether this vulnerability has already been publicly discussed or disclosed.
|
||||
- If so, provide relevant links.
|
||||
|
||||
3. **DESCRIPTION**
|
||||
- Provide a detailed description of the security vulnerability.
|
||||
- Include as much information as possible to help us understand and address the issue.
|
||||
|
||||
Send this information, along with any additional relevant details, to <junegunn.c AT gmail DOT com>.
|
||||
|
||||
## Confidentiality
|
||||
|
||||
We kindly ask you to keep the report confidential until a public announcement is made.
|
||||
|
||||
## Notes
|
||||
|
||||
- Vulnerabilities will be handled on a best-effort basis.
|
||||
- You may request an advance copy of the patched release, but we cannot guarantee early access before the public release.
|
||||
- You will be notified via email simultaneously with the public announcement.
|
||||
- We will respond within a few weeks to confirm whether your report has been accepted or rejected.
|
||||
|
||||
Thank you for helping to improve the security of our project!
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# The purpose of this script is to demonstrate how to preview a file or an
|
||||
# image in the preview window of fzf.
|
||||
#
|
||||
# Dependencies:
|
||||
# - https://github.com/sharkdp/bat
|
||||
# - https://github.com/hpjansson/chafa
|
||||
# - https://iterm2.com/utilities/imgcat
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
>&2 echo "usage: $0 FILENAME[:LINENO][:IGNORED]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
file=${1/#\~\//$HOME/}
|
||||
|
||||
center=0
|
||||
if [[ ! -r $file ]]; then
|
||||
if [[ $file =~ ^(.+):([0-9]+)\ *$ ]] && [[ -r ${BASH_REMATCH[1]} ]]; then
|
||||
file=${BASH_REMATCH[1]}
|
||||
center=${BASH_REMATCH[2]}
|
||||
elif [[ $file =~ ^(.+):([0-9]+):[0-9]+\ *$ ]] && [[ -r ${BASH_REMATCH[1]} ]]; then
|
||||
file=${BASH_REMATCH[1]}
|
||||
center=${BASH_REMATCH[2]}
|
||||
fi
|
||||
fi
|
||||
|
||||
type=$(file --brief --dereference --mime -- "$file")
|
||||
|
||||
if [[ ! $type =~ image/ ]]; then
|
||||
if [[ $type =~ =binary ]]; then
|
||||
file "$1"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Sometimes bat is installed as batcat.
|
||||
if command -v batcat > /dev/null; then
|
||||
batname="batcat"
|
||||
elif command -v bat > /dev/null; then
|
||||
batname="bat"
|
||||
else
|
||||
cat "$1"
|
||||
exit
|
||||
fi
|
||||
|
||||
${batname} --style="${BAT_STYLE:-numbers}" --color=always --pager=never --highlight-line="${center:-0}" -- "$file"
|
||||
exit
|
||||
fi
|
||||
|
||||
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||
if [[ $dim == x ]]; then
|
||||
dim=$(stty size < /dev/tty | awk '{print $2 "x" $1}')
|
||||
elif ! [[ $KITTY_WINDOW_ID ]] && ((FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stty size < /dev/tty | awk '{print $1}'))); then
|
||||
# Avoid scrolling issue when the Sixel image touches the bottom of the screen
|
||||
# * https://github.com/junegunn/fzf/issues/2544
|
||||
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
||||
fi
|
||||
|
||||
# 1. Use icat (from Kitty) if kitten is installed
|
||||
if [[ $KITTY_WINDOW_ID ]] || [[ $GHOSTTY_RESOURCES_DIR ]] && command -v kitten > /dev/null; then
|
||||
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
|
||||
# you have to use 'stream'.
|
||||
#
|
||||
# 2. The last line of the output is the ANSI reset code without newline.
|
||||
# This confuses fzf and makes it render scroll offset indicator.
|
||||
# So we remove the last line and append the reset code to its previous line.
|
||||
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
|
||||
|
||||
# 2. Use chafa with Sixel output
|
||||
elif command -v chafa > /dev/null; then
|
||||
chafa -s "$dim" "$file"
|
||||
# Add a new line character so that fzf can display multiple images in the preview window
|
||||
echo
|
||||
|
||||
# 3. If chafa is not found but imgcat is available, use it on iTerm2
|
||||
elif command -v imgcat > /dev/null; then
|
||||
# NOTE: We should use https://iterm2.com/utilities/it2check to check if the
|
||||
# user is running iTerm2. But for the sake of simplicity, we just assume
|
||||
# that's the case here.
|
||||
imgcat -W "${dim%%x*}" -H "${dim##*x}" "$file"
|
||||
|
||||
# 4. Cannot find any suitable method to preview the image
|
||||
else
|
||||
file "$file"
|
||||
fi
|
||||
+126
-71
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# fzf-tmux: starts fzf in a tmux pane
|
||||
# usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
|
||||
# usage: fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS]
|
||||
|
||||
fail() {
|
||||
>&2 echo "$1"
|
||||
exit 2
|
||||
}
|
||||
|
||||
fzf="$(command -v fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
|
||||
[[ -x "$fzf" ]] || fail 'fzf executable not found'
|
||||
fzf="$(command which fzf)" || fzf="$(dirname "$0")/fzf"
|
||||
[[ -x $fzf ]] || fail 'fzf executable not found'
|
||||
|
||||
args=()
|
||||
opt=""
|
||||
@@ -16,19 +16,30 @@ skip=""
|
||||
swap=""
|
||||
close=""
|
||||
term=""
|
||||
[[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines) || lines=$(tmux display-message -p "#{pane_height}")
|
||||
[[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}")
|
||||
[[ -n $LINES ]] && lines=$LINES || lines=$(tput lines) || lines=$(tmux display-message -p "#{pane_height}")
|
||||
[[ -n $COLUMNS ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}")
|
||||
|
||||
tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
|
||||
tmux_32=$(awk '{print ($1 >= 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version >= 3.2")
|
||||
|
||||
help() {
|
||||
>&2 echo 'usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
|
||||
>&2 echo 'usage: fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS]
|
||||
|
||||
Layout
|
||||
-u [HEIGHT[%]] Split above (up)
|
||||
-d [HEIGHT[%]] Split below (down)
|
||||
-l [WIDTH[%]] Split left
|
||||
-r [WIDTH[%]] Split right
|
||||
LAYOUT OPTIONS:
|
||||
(default layout: -d 50%)
|
||||
|
||||
(default: -d 50%)
|
||||
Popup window (requires tmux 3.2 or above):
|
||||
-p [WIDTH[%][,HEIGHT[%]]] (default: 50%)
|
||||
-w WIDTH[%]
|
||||
-h HEIGHT[%]
|
||||
-x COL
|
||||
-y ROW
|
||||
|
||||
Split pane:
|
||||
-u [HEIGHT[%]] Split above (up)
|
||||
-d [HEIGHT[%]] Split below (down)
|
||||
-l [WIDTH[%]] Split left
|
||||
-r [WIDTH[%]] Split right
|
||||
'
|
||||
exit
|
||||
}
|
||||
@@ -36,7 +47,7 @@ help() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
arg="$1"
|
||||
shift
|
||||
[[ -z "$skip" ]] && case "$arg" in
|
||||
[[ -z $skip ]] && case "$arg" in
|
||||
-)
|
||||
term=1
|
||||
;;
|
||||
@@ -47,17 +58,19 @@ while [[ $# -gt 0 ]]; do
|
||||
echo "fzf-tmux (with fzf $("$fzf" --version))"
|
||||
exit
|
||||
;;
|
||||
-w*|-h*|-d*|-u*|-r*|-l*)
|
||||
if [[ "$arg" =~ ^.[lrw] ]]; then
|
||||
-p* | -w* | -h* | -x* | -y* | -d* | -u* | -r* | -l*)
|
||||
if [[ $arg =~ ^-[pwhxy] ]]; then
|
||||
[[ $opt =~ "-E" ]] || opt="-E"
|
||||
elif [[ $arg =~ ^.[lr] ]]; then
|
||||
opt="-h"
|
||||
if [[ "$arg" =~ ^.l ]]; then
|
||||
if [[ $arg =~ ^.l ]]; then
|
||||
opt="$opt -d"
|
||||
swap="; swap-pane -D ; select-pane -L"
|
||||
close="; tmux swap-pane -D"
|
||||
fi
|
||||
else
|
||||
opt=""
|
||||
if [[ "$arg" =~ ^.u ]]; then
|
||||
if [[ $arg =~ ^.u ]]; then
|
||||
opt="$opt -d"
|
||||
swap="; swap-pane -D ; select-pane -U"
|
||||
close="; tmux swap-pane -D"
|
||||
@@ -66,7 +79,7 @@ while [[ $# -gt 0 ]]; do
|
||||
if [[ ${#arg} -gt 2 ]]; then
|
||||
size="${arg:2}"
|
||||
else
|
||||
if [[ "$1" =~ ^[0-9]+%?$ ]]; then
|
||||
if [[ $1 =~ ^[0-9%,]+$ ]] || [[ $1 =~ ^[A-Z]$ ]]; then
|
||||
size="$1"
|
||||
shift
|
||||
else
|
||||
@@ -74,21 +87,37 @@ while [[ $# -gt 0 ]]; do
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$size" =~ %$ ]]; then
|
||||
size=${size:0:((${#size}-1))}
|
||||
if [[ -n "$swap" ]]; then
|
||||
opt="$opt -p $(( 100 - size ))"
|
||||
if [[ $arg =~ ^-p ]]; then
|
||||
if [[ -n $size ]]; then
|
||||
w=${size%%,*}
|
||||
h=${size##*,}
|
||||
opt="$opt -w$w -h$h"
|
||||
fi
|
||||
elif [[ $arg =~ ^-[whxy] ]]; then
|
||||
opt="$opt ${arg:0:2}$size"
|
||||
elif [[ $size =~ %$ ]]; then
|
||||
size=${size:0:${#size}-1}
|
||||
if [[ $tmux_32 == 1 ]]; then
|
||||
if [[ -n $swap ]]; then
|
||||
opt="$opt -l $((100 - size))%"
|
||||
else
|
||||
opt="$opt -l $size%"
|
||||
fi
|
||||
else
|
||||
opt="$opt -p $size"
|
||||
if [[ -n $swap ]]; then
|
||||
opt="$opt -p $((100 - size))"
|
||||
else
|
||||
opt="$opt -p $size"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if [[ -n "$swap" ]]; then
|
||||
if [[ "$arg" =~ ^.l ]]; then
|
||||
if [[ -n $swap ]]; then
|
||||
if [[ $arg =~ ^.l ]]; then
|
||||
max=$columns
|
||||
else
|
||||
max=$lines
|
||||
fi
|
||||
size=$(( max - size ))
|
||||
size=$((max - size))
|
||||
[[ $size -lt 0 ]] && size=0
|
||||
opt="$opt -l $size"
|
||||
else
|
||||
@@ -106,20 +135,22 @@ while [[ $# -gt 0 ]]; do
|
||||
args+=("$arg")
|
||||
;;
|
||||
esac
|
||||
[[ -n "$skip" ]] && args+=("$arg")
|
||||
[[ -n $skip ]] && args+=("$arg")
|
||||
done
|
||||
|
||||
if [[ -z "$TMUX" || "$opt" =~ ^-h && "$columns" -le 40 || ! "$opt" =~ ^-h && "$lines" -le 15 ]]; then
|
||||
if [[ -z $TMUX ]]; then
|
||||
"$fzf" "${args[@]}"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# --height option is not allowed
|
||||
args+=("--no-height")
|
||||
# * --height option is not allowed
|
||||
# * CTRL-Z is also disabled
|
||||
# * fzf-tmux script is not compatible with --tmux option in fzf 0.53.0 or later
|
||||
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore" "--no-tmux")
|
||||
|
||||
# Handle zoomed tmux pane by moving it to a temp window
|
||||
if tmux list-panes -F '#F' | grep -q Z; then
|
||||
zoomed=1
|
||||
# Handle zoomed tmux pane without popup options by moving it to a temp window
|
||||
if [[ ! $opt =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then
|
||||
zoomed_without_popup=1
|
||||
original_window=$(tmux display-message -p "#{window_id}")
|
||||
tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - '\\;' do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'")
|
||||
tmux swap-pane -t $tmp_window \; select-window -t $tmp_window
|
||||
@@ -128,21 +159,27 @@ fi
|
||||
set -e
|
||||
|
||||
# Clean up named pipes on exit
|
||||
id=$RANDOM
|
||||
argsf="${TMPDIR:-/tmp}/fzf-args-$id"
|
||||
fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id"
|
||||
fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id"
|
||||
fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id"
|
||||
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/fzf-tmux-XXXXXX")
|
||||
argsf="$tmpdir/args"
|
||||
fifo1="$tmpdir/fifo1"
|
||||
fifo2="$tmpdir/fifo2"
|
||||
fifo3="$tmpdir/fifo3"
|
||||
if tmux_win_opts=$(tmux show-options -p remain-on-exit \; show-options -p synchronize-panes 2> /dev/null); then
|
||||
tmux_win_opts=($(sed '/ off/d; s/synchronize-panes/set-option -p synchronize-panes/; s/remain-on-exit/set-option -p remain-on-exit/; s/$/ \\;/' <<< "$tmux_win_opts"))
|
||||
tmux_off_opts='; set-option -p synchronize-panes off ; set-option -p remain-on-exit off'
|
||||
else
|
||||
tmux_win_opts=($(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/'))
|
||||
tmux_off_opts='; set-window-option synchronize-panes off ; set-window-option remain-on-exit off'
|
||||
fi
|
||||
cleanup() {
|
||||
\rm -f $argsf $fifo1 $fifo2 $fifo3
|
||||
|
||||
\rm -rf "$tmpdir"
|
||||
# Restore tmux window options
|
||||
if [[ "${#tmux_win_opts[@]}" -gt 0 ]]; then
|
||||
eval "tmux ${tmux_win_opts[@]}"
|
||||
if [[ ${#tmux_win_opts[@]} -gt 1 ]]; then
|
||||
eval "tmux ${tmux_win_opts[*]}"
|
||||
fi
|
||||
|
||||
# Remove temp window if we were zoomed
|
||||
if [[ -n "$zoomed" ]]; then
|
||||
# Remove temp window if we were zoomed without popup options
|
||||
if [[ -n $zoomed_without_popup ]]; then
|
||||
tmux display-message -p "#{window_id}" > /dev/null
|
||||
tmux swap-pane -t $original_window \; \
|
||||
select-window -t $original_window \; \
|
||||
@@ -150,7 +187,7 @@ cleanup() {
|
||||
resize-pane -Z
|
||||
fi
|
||||
|
||||
if [ $# -gt 0 ]; then
|
||||
if [[ $# -gt 0 ]]; then
|
||||
trap - EXIT
|
||||
exit 130
|
||||
fi
|
||||
@@ -158,44 +195,62 @@ cleanup() {
|
||||
trap 'cleanup 1' SIGUSR1
|
||||
trap 'cleanup' EXIT
|
||||
|
||||
envs="env TERM=$TERM "
|
||||
[[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
|
||||
[[ -n "$FZF_DEFAULT_COMMAND" ]] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
|
||||
|
||||
mkfifo -m o+w $fifo2
|
||||
mkfifo -m o+w $fifo3
|
||||
envs="export TERM=$(printf %q "$TERM") "
|
||||
if [[ $opt =~ "-E" ]]; then
|
||||
if [[ $tmux_version == 3.2 ]]; then
|
||||
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
|
||||
elif [[ $tmux_32 == 1 ]]; then
|
||||
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
|
||||
opt="-B $opt"
|
||||
else
|
||||
echo "fzf-tmux: tmux 3.2 or above is required for popup mode" >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
|
||||
envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
|
||||
envs="$envs FZF_DEFAULT_OPTS_FILE=$(printf %q "$FZF_DEFAULT_OPTS_FILE")"
|
||||
[[ -n $RUNEWIDTH_EASTASIAN ]] && envs="$envs RUNEWIDTH_EASTASIAN=$(printf %q "$RUNEWIDTH_EASTASIAN")"
|
||||
[[ -n $BAT_THEME ]] && envs="$envs BAT_THEME=$(printf %q "$BAT_THEME")"
|
||||
echo "$envs;" > "$argsf"
|
||||
|
||||
# Build arguments to fzf
|
||||
opts=""
|
||||
for arg in "${args[@]}"; do
|
||||
arg="${arg//\\/\\\\}"
|
||||
arg="${arg//\"/\\\"}"
|
||||
arg="${arg//\`/\\\`}"
|
||||
arg="${arg//$/\\$}"
|
||||
opts="$opts \"$arg\""
|
||||
done
|
||||
opts=$(printf "%q " "${args[@]}")
|
||||
|
||||
pppid=$$
|
||||
echo -n "trap 'kill -SIGUSR1 -$pppid' EXIT SIGINT SIGTERM;" > $argsf
|
||||
echo -n "trap 'kill -SIGUSR1 -$pppid' EXIT SIGINT SIGTERM;" >> $argsf
|
||||
close="; trap - EXIT SIGINT SIGTERM $close"
|
||||
|
||||
tmux_win_opts=( $(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/') )
|
||||
export TMUX=$(cut -d , -f 1,2 <<< "$TMUX")
|
||||
mkfifo -m o+w $fifo2
|
||||
if [[ $opt =~ "-E" ]]; then
|
||||
cat $fifo2 &
|
||||
if [[ -n $term ]] || [[ -t 0 ]]; then
|
||||
cat <<< "\"$fzf\" $opts > $fifo2; out=\$? $close; exit \$out" >> $argsf
|
||||
else
|
||||
mkfifo $fifo1
|
||||
cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; out=\$? $close; exit \$out" >> $argsf
|
||||
cat <&0 > $fifo1 &
|
||||
fi
|
||||
|
||||
if [[ -n "$term" ]] || [[ -t 0 ]]; then
|
||||
tmux popup -d "$PWD" $opt "bash $argsf" > /dev/null 2>&1
|
||||
exit $?
|
||||
fi
|
||||
|
||||
mkfifo -m o+w $fifo3
|
||||
if [[ -n $term ]] || [[ -t 0 ]]; then
|
||||
cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" >> $argsf
|
||||
TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\
|
||||
set-window-option remain-on-exit off \;\
|
||||
split-window $opt "$envs bash -c 'cd $(printf %q "$PWD"); exec -a fzf bash $argsf'" $swap \
|
||||
> /dev/null 2>&1
|
||||
else
|
||||
mkfifo $fifo1
|
||||
cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" >> $argsf
|
||||
TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\
|
||||
set-window-option remain-on-exit off \;\
|
||||
split-window $opt "$envs bash -c 'exec -a fzf bash $argsf'" $swap \
|
||||
> /dev/null 2>&1
|
||||
cat <&0 > $fifo1 &
|
||||
fi
|
||||
tmux \
|
||||
split-window -c "$PWD" $opt "bash -c 'exec -a fzf bash $argsf'" $swap \
|
||||
$tmux_off_opts \
|
||||
> /dev/null 2>&1 || {
|
||||
"$fzf" "${args[@]}"
|
||||
exit $?
|
||||
}
|
||||
cat $fifo2
|
||||
exit "$(cat $fifo3)"
|
||||
|
||||
|
||||
+245
-82
@@ -1,49 +1,98 @@
|
||||
fzf.txt fzf Last change: November 23 2019
|
||||
fzf.txt fzf Last change: February 15 2024
|
||||
FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
|
||||
==============================================================================
|
||||
|
||||
FZF Vim integration
|
||||
Summary
|
||||
:FZF[!]
|
||||
Configuration
|
||||
Examples
|
||||
fzf#run
|
||||
fzf#wrap
|
||||
Tips
|
||||
fzf inside terminal buffer
|
||||
Starting fzf in Neovim floating window
|
||||
Hide statusline
|
||||
License
|
||||
FZF Vim integration |fzf-vim-integration|
|
||||
Installation |fzf-installation|
|
||||
Summary |fzf-summary|
|
||||
:FZF[!] |:FZF|
|
||||
Configuration |fzf-configuration|
|
||||
Examples |fzf-examples|
|
||||
Explanation of g:fzf_colors |fzf-explanation-of-gfzfcolors|
|
||||
fzf#run |fzf#run|
|
||||
fzf#wrap |fzf#wrap|
|
||||
Global options supported by fzf#wrap |fzf-global-options-supported-by-fzf#wrap|
|
||||
Tips |fzf-tips|
|
||||
fzf inside terminal buffer |fzf-inside-terminal-buffer|
|
||||
Starting fzf in a popup window |fzf-starting-fzf-in-a-popup-window|
|
||||
Hide statusline |fzf-hide-statusline|
|
||||
License |fzf-license|
|
||||
|
||||
FZF VIM INTEGRATION *fzf-vim-integration*
|
||||
==============================================================================
|
||||
|
||||
|
||||
INSTALLATION *fzf-installation*
|
||||
==============================================================================
|
||||
|
||||
Once you have fzf installed, you can enable it inside Vim simply by adding the
|
||||
directory to 'runtimepath' in your Vim configuration file. The path may differ
|
||||
depending on the package manager.
|
||||
>
|
||||
" If installed using Homebrew
|
||||
set rtp+=/usr/local/opt/fzf
|
||||
|
||||
" If installed using Homebrew on Apple Silicon
|
||||
set rtp+=/opt/homebrew/opt/fzf
|
||||
|
||||
" If you have cloned fzf on ~/.fzf directory
|
||||
set rtp+=~/.fzf
|
||||
<
|
||||
If you use {vim-plug}{1}, the same can be written as:
|
||||
>
|
||||
" If installed using Homebrew
|
||||
Plug '/usr/local/opt/fzf'
|
||||
|
||||
" If installed using Homebrew on Apple Silicon
|
||||
Plug '/opt/homebrew/opt/fzf'
|
||||
|
||||
" If you have cloned fzf on ~/.fzf directory
|
||||
Plug '~/.fzf'
|
||||
<
|
||||
But if you want the latest Vim plugin file from GitHub rather than the one
|
||||
included in the package, write:
|
||||
>
|
||||
Plug 'junegunn/fzf'
|
||||
<
|
||||
The Vim plugin will pick up fzf binary available on the system. If fzf is not
|
||||
found on `$PATH`, it will ask you if it should download the latest binary for
|
||||
you.
|
||||
|
||||
To make sure that you have the latest version of the binary, set up
|
||||
post-update hook like so:
|
||||
|
||||
*fzf#install*
|
||||
>
|
||||
Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }
|
||||
<
|
||||
{1} https://github.com/junegunn/vim-plug
|
||||
|
||||
|
||||
SUMMARY *fzf-summary*
|
||||
==============================================================================
|
||||
|
||||
The Vim plugin of fzf provides two core functions, and `:FZF` command which is
|
||||
the basic file selector command built on top of them.
|
||||
|
||||
1. `fzf#run([spec dict])`
|
||||
1. `fzf#run([spec dict])`
|
||||
- Starts fzf inside Vim with the given spec
|
||||
- `:call fzf#run({'source': 'ls'})`
|
||||
2. `fzf#wrap([spec dict]) -> (dict)`
|
||||
- `:call fzf#run({'source': 'ls'})`
|
||||
2. `fzf#wrap([spec dict]) -> (dict)`
|
||||
- Takes a spec for `fzf#run` and returns an extended version of it with
|
||||
additional options for addressing global preferences (`g:fzf_xxx`)
|
||||
- `:echo fzf#wrap({'source': 'ls'})`
|
||||
- `:echo fzf#wrap({'source': 'ls'})`
|
||||
- We usually wrap a spec with `fzf#wrap` before passing it to `fzf#run`
|
||||
- `:call fzf#run(fzf#wrap({'source': 'ls'}))`
|
||||
3. `:FZF [fzf_options string] [path string]`
|
||||
- `:call fzf#run(fzf#wrap({'source': 'ls'}))`
|
||||
3. `:FZF [fzf_options string] [path string]`
|
||||
- Basic fuzzy file selector
|
||||
- A reference implementation for those who don't want to write VimScript to
|
||||
implement custom commands
|
||||
- If you're looking for more such commands, check out {fzf.vim}{1} project.
|
||||
- If you're looking for more such commands, check out {fzf.vim}{2} project.
|
||||
|
||||
The most important of all is `fzf#run`, but it would be easier to understand
|
||||
the whole if we start off with `:FZF` command.
|
||||
|
||||
{1} https://github.com/junegunn/fzf.vim
|
||||
{2} https://github.com/junegunn/fzf.vim
|
||||
|
||||
|
||||
:FZF[!]
|
||||
@@ -63,14 +112,14 @@ the whole if we start off with `:FZF` command.
|
||||
" Bang version starts fzf in fullscreen mode
|
||||
:FZF!
|
||||
<
|
||||
Similarly to {ctrlp.vim}{2}, use enter key, CTRL-T, CTRL-X or CTRL-V to open
|
||||
Similarly to {ctrlp.vim}{3}, use Enter key, CTRL-T, CTRL-X or CTRL-V to open
|
||||
selected files in the current window, in new tabs, in horizontal splits, or in
|
||||
vertical splits respectively.
|
||||
|
||||
Note that the environment variables `FZF_DEFAULT_COMMAND` and
|
||||
`FZF_DEFAULT_OPTS` also apply here.
|
||||
|
||||
{2} https://github.com/kien/ctrlp.vim
|
||||
{3} https://github.com/kien/ctrlp.vim
|
||||
|
||||
|
||||
< Configuration >_____________________________________________________________~
|
||||
@@ -100,7 +149,7 @@ Examples~
|
||||
|
||||
" An action can be a reference to a function that processes selected lines
|
||||
function! s:build_quickfix_list(lines)
|
||||
call setqflist(map(copy(a:lines), '{ "filename": v:val }'))
|
||||
call setqflist(map(copy(a:lines), '{ "filename": v:val, "lnum": 1 }'))
|
||||
copen
|
||||
cc
|
||||
endfunction
|
||||
@@ -112,10 +161,19 @@ Examples~
|
||||
\ 'ctrl-v': 'vsplit' }
|
||||
|
||||
" Default fzf layout
|
||||
" - down / up / left / right
|
||||
let g:fzf_layout = { 'down': '~40%' }
|
||||
" - Popup window (center of the screen)
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||
|
||||
" You can set up fzf window using a Vim command (Neovim or latest Vim 8 required)
|
||||
" - Popup window (center of the current window)
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true } }
|
||||
|
||||
" - Popup window (anchored to the bottom of the current window)
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true, 'yoffset': 1.0 } }
|
||||
|
||||
" - down / up / left / right
|
||||
let g:fzf_layout = { 'down': '40%' }
|
||||
|
||||
" - Window using a Vim command
|
||||
let g:fzf_layout = { 'window': 'enew' }
|
||||
let g:fzf_layout = { 'window': '-tabnew' }
|
||||
let g:fzf_layout = { 'window': '10new' }
|
||||
@@ -144,6 +202,53 @@ Examples~
|
||||
let g:fzf_history_dir = '~/.local/share/fzf-history'
|
||||
<
|
||||
|
||||
Explanation of g:fzf_colors~
|
||||
*fzf-explanation-of-gfzfcolors*
|
||||
|
||||
`g:fzf_colors` is a dictionary mapping fzf elements to a color specification
|
||||
list:
|
||||
>
|
||||
element: [ component, group1 [, group2, ...] ]
|
||||
<
|
||||
- `element` is an fzf element to apply a color to:
|
||||
|
||||
----------------------------+------------------------------------------------------
|
||||
Element | Description ~
|
||||
----------------------------+------------------------------------------------------
|
||||
`fg` / `bg` / `hl` | Item (foreground / background / highlight)
|
||||
`fg+` / `bg+` / `hl+` | Current item (foreground / background / highlight)
|
||||
`preview-fg` / `preview-bg` | Preview window text and background
|
||||
`gutter` | Background of the gutter on the left
|
||||
`pointer` | Pointer to the current line ( `>` )
|
||||
`marker` | Multi-select marker ( `>` )
|
||||
`border` | Border around the window ( `--border` and `--preview` )
|
||||
`header` | Header ( `--header` or `--header-lines` )
|
||||
`info` | Info line (match counters)
|
||||
`spinner` | Streaming input indicator
|
||||
`query` | Query string
|
||||
`disabled` | Query string when search is disabled
|
||||
`prompt` | Prompt before query ( `> ` )
|
||||
----------------------------+------------------------------------------------------
|
||||
- `component` specifies the component (`fg` / `bg`) from which to extract the
|
||||
color when considering each of the following highlight groups
|
||||
- `group1 [, group2, ...]` is a list of highlight groups that are searched (in
|
||||
order) for a matching color definition
|
||||
|
||||
For example, consider the following specification:
|
||||
>
|
||||
'prompt': ['fg', 'Conditional', 'Comment'],
|
||||
<
|
||||
This means we color the prompt - using the `fg` attribute of the `Conditional`
|
||||
if it exists, - otherwise use the `fg` attribute of the `Comment` highlight
|
||||
group if it exists, - otherwise fall back to the default color settings for
|
||||
the prompt.
|
||||
|
||||
You can examine the color option generated according to the setting by printing
|
||||
the result of `fzf#wrap()` function like so:
|
||||
>
|
||||
:echo fzf#wrap()
|
||||
<
|
||||
|
||||
FZF#RUN
|
||||
==============================================================================
|
||||
|
||||
@@ -157,17 +262,18 @@ entry.
|
||||
call fzf#run({'sink': 'e'})
|
||||
<
|
||||
We haven't specified the `source`, so this is equivalent to starting fzf on
|
||||
command line without standard input pipe; fzf will use find command (or
|
||||
`$FZF_DEFAULT_COMMAND` if defined) to list the files under the current
|
||||
directory. When you select one, it will open it with the sink, `:e` command.
|
||||
If you want to open it in a new tab, you can pass `:tabedit` command instead
|
||||
as the sink.
|
||||
command line without standard input pipe; fzf will traverse the file system
|
||||
under the current directory to get the list of files. (If
|
||||
`$FZF_DEFAULT_COMMAND` is set, fzf will use the output of the command
|
||||
instead.) When you select one, it will open it with the sink, `:e` command. If
|
||||
you want to open it in a new tab, you can pass `:tabedit` command instead as
|
||||
the sink.
|
||||
>
|
||||
call fzf#run({'sink': 'tabedit'})
|
||||
<
|
||||
Instead of using the default find command, you can use any shell command as
|
||||
the source. The following example will list the files managed by git. It's
|
||||
equivalent to running `git ls-files | fzf` on shell.
|
||||
You can use any shell command as the source to generate the list. The
|
||||
following example will list the files managed by git. It's equivalent to
|
||||
running `git ls-files | fzf` on shell.
|
||||
>
|
||||
call fzf#run({'source': 'git ls-files', 'sink': 'e'})
|
||||
<
|
||||
@@ -195,15 +301,18 @@ The following table summarizes the available options.
|
||||
---------------------------+---------------+----------------------------------------------------------------------
|
||||
Option name | Type | Description ~
|
||||
---------------------------+---------------+----------------------------------------------------------------------
|
||||
`source` | string | External command to generate input to fzf (e.g. `find .` )
|
||||
`source` | string | External command to generate input to fzf (e.g. `find .` )
|
||||
`source` | list | Vim list as input to fzf
|
||||
`sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` )
|
||||
`sink` | funcref | Reference to function to process each selected item
|
||||
`sink*` | funcref | Similar to `sink` , but takes the list of output lines at once
|
||||
`sink` | funcref | Function to be called with each selected item
|
||||
`sinklist` (or `sink*` ) | funcref | Similar to `sink` , but takes the list of output lines at once
|
||||
`exit` | funcref | Function to be called with the exit status of fzf (e.g. 0, 1, 2, 130)
|
||||
`options` | string/list | Options to fzf
|
||||
`dir` | string | Working directory
|
||||
`up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` )
|
||||
`window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` )
|
||||
`tmux` | string | (Layout) `--tmux` options (e.g. `90%,70%` )
|
||||
`window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` )
|
||||
`window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}` )
|
||||
---------------------------+---------------+----------------------------------------------------------------------
|
||||
|
||||
`options` entry can be either a string or a list. For simple cases, string
|
||||
@@ -212,6 +321,19 @@ should suffice, but prefer to use list type to avoid escaping issues.
|
||||
call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'})
|
||||
call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']})
|
||||
<
|
||||
When `window` entry is a dictionary, fzf will start in a popup window. The
|
||||
following options are allowed:
|
||||
|
||||
- Required:
|
||||
- `width` [float range [0 ~ 1]] or [integer range [8 ~ ]]
|
||||
- `height` [float range [0 ~ 1]] or [integer range [4 ~ ]]
|
||||
- Optional:
|
||||
- `yoffset` [float default 0.5 range [0 ~ 1]]
|
||||
- `xoffset` [float default 0.5 range [0 ~ 1]]
|
||||
- `relative` [boolean default v:false]
|
||||
- `border` [string default `rounded` (`sharp` on Windows)]: Border style
|
||||
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]`
|
||||
|
||||
|
||||
FZF#WRAP
|
||||
==============================================================================
|
||||
@@ -227,7 +349,7 @@ So how can we make our custom `fzf#run` calls also respect those variables?
|
||||
Simply by "wrapping" the spec dictionary with `fzf#wrap` before passing it to
|
||||
`fzf#run`.
|
||||
|
||||
- `fzf#wrap([name string], [spec dict], [fullscreen bool]) -> (dict)`
|
||||
- `fzf#wrap([name string], [spec dict], [fullscreen bool]) -> (dict)`
|
||||
- All arguments are optional. Usually we only need to pass a spec
|
||||
dictionary.
|
||||
- `name` is for managing history files. It is ignored if `g:fzf_history_dir`
|
||||
@@ -244,8 +366,8 @@ After we "wrap" our spec, we pass it to `fzf#run`.
|
||||
>
|
||||
call fzf#run(fzf#wrap({'source': 'ls'}))
|
||||
<
|
||||
Now it supports CTRL-T, CTRL-V, and CTRL-X key bindings and it opens fzf
|
||||
window according to `g:fzf_layout` setting.
|
||||
Now it supports CTRL-T, CTRL-V, and CTRL-X key bindings (configurable via
|
||||
`g:fzf_action`) and it opens fzf window according to `g:fzf_layout` setting.
|
||||
|
||||
To make it easier to use, let's define `LS` command.
|
||||
>
|
||||
@@ -261,9 +383,9 @@ last `fullscreen` argument of `fzf#wrap` (see :help <bang>).
|
||||
command! -bang LS call fzf#run(fzf#wrap({'source': 'ls'}, <bang>0))
|
||||
<
|
||||
Our `:LS` command will be much more useful if we can pass a directory argument
|
||||
to it, so that something like `:LS /tmp` is possible.
|
||||
to it, so that something like `:LS /tmp` is possible.
|
||||
>
|
||||
command! -bang -complete=dir -nargs=* LS
|
||||
command! -bang -complete=dir -nargs=? LS
|
||||
\ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0))
|
||||
<
|
||||
Lastly, if you have enabled `g:fzf_history_dir`, you might want to assign a
|
||||
@@ -271,10 +393,23 @@ unique name to our command and pass it as the first argument to `fzf#wrap`.
|
||||
>
|
||||
" The query history for this command will be stored as 'ls' inside g:fzf_history_dir.
|
||||
" The name is ignored if g:fzf_history_dir is not defined.
|
||||
command! -bang -complete=dir -nargs=* LS
|
||||
command! -bang -complete=dir -nargs=? LS
|
||||
\ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': <q-args>}, <bang>0))
|
||||
<
|
||||
|
||||
< Global options supported by fzf#wrap >______________________________________~
|
||||
*fzf-global-options-supported-by-fzf#wrap*
|
||||
|
||||
- `g:fzf_layout`
|
||||
- `g:fzf_action`
|
||||
- Works only when no custom `sink` (or `sinklist`) is provided
|
||||
- Having custom sink usually means that each entry is not an ordinary
|
||||
file path (e.g. name of color scheme), so we can't blindly apply the
|
||||
same strategy (i.e. `tabedit some-color-scheme` doesn't make sense)
|
||||
- `g:fzf_colors`
|
||||
- `g:fzf_history_dir`
|
||||
|
||||
|
||||
TIPS *fzf-tips*
|
||||
==============================================================================
|
||||
|
||||
@@ -282,55 +417,83 @@ TIPS *fzf-tips*
|
||||
< fzf inside terminal buffer >________________________________________________~
|
||||
*fzf-inside-terminal-buffer*
|
||||
|
||||
The latest versions of Vim and Neovim include builtin terminal emulator
|
||||
(`:terminal`) and fzf will start in a terminal buffer in the following cases:
|
||||
|
||||
- On Neovim
|
||||
- On GVim
|
||||
- On Terminal Vim with a non-default layout
|
||||
- `call fzf#run({'left': '30%'})` or `let g:fzf_layout = {'left': '30%'}`
|
||||
On the latest versions of Vim and Neovim, fzf will start in a terminal buffer.
|
||||
If you find the default ANSI colors to be different, consider configuring the
|
||||
colors using `g:terminal_ansi_colors` in regular Vim or `g:terminal_color_x`
|
||||
in Neovim.
|
||||
|
||||
|
||||
Starting fzf in Neovim floating window~
|
||||
*fzf-starting-fzf-in-neovim-floating-window*
|
||||
>
|
||||
" Using floating windows of Neovim to start fzf
|
||||
" Terminal colors for seoul256 color scheme
|
||||
if has('nvim')
|
||||
let $FZF_DEFAULT_OPTS .= ' --border --margin=0,2'
|
||||
|
||||
function! FloatingFZF()
|
||||
let width = float2nr(&columns * 0.9)
|
||||
let height = float2nr(&lines * 0.6)
|
||||
let opts = { 'relative': 'editor',
|
||||
\ 'row': (&lines - height) / 2,
|
||||
\ 'col': (&columns - width) / 2,
|
||||
\ 'width': width,
|
||||
\ 'height': height }
|
||||
|
||||
let win = nvim_open_win(nvim_create_buf(v:false, v:true), v:true, opts)
|
||||
call setwinvar(win, '&winhighlight', 'NormalFloat:Normal')
|
||||
endfunction
|
||||
|
||||
let g:fzf_layout = { 'window': 'call FloatingFZF()' }
|
||||
let g:terminal_color_0 = '#4e4e4e'
|
||||
let g:terminal_color_1 = '#d68787'
|
||||
let g:terminal_color_2 = '#5f865f'
|
||||
let g:terminal_color_3 = '#d8af5f'
|
||||
let g:terminal_color_4 = '#85add4'
|
||||
let g:terminal_color_5 = '#d7afaf'
|
||||
let g:terminal_color_6 = '#87afaf'
|
||||
let g:terminal_color_7 = '#d0d0d0'
|
||||
let g:terminal_color_8 = '#626262'
|
||||
let g:terminal_color_9 = '#d75f87'
|
||||
let g:terminal_color_10 = '#87af87'
|
||||
let g:terminal_color_11 = '#ffd787'
|
||||
let g:terminal_color_12 = '#add4fb'
|
||||
let g:terminal_color_13 = '#ffafaf'
|
||||
let g:terminal_color_14 = '#87d7d7'
|
||||
let g:terminal_color_15 = '#e4e4e4'
|
||||
else
|
||||
let g:terminal_ansi_colors = [
|
||||
\ '#4e4e4e', '#d68787', '#5f865f', '#d8af5f',
|
||||
\ '#85add4', '#d7afaf', '#87afaf', '#d0d0d0',
|
||||
\ '#626262', '#d75f87', '#87af87', '#ffd787',
|
||||
\ '#add4fb', '#ffafaf', '#87d7d7', '#e4e4e4'
|
||||
\ ]
|
||||
endif
|
||||
|
||||
<
|
||||
|
||||
Hide statusline~
|
||||
< Starting fzf in a popup window >____________________________________________~
|
||||
*fzf-starting-fzf-in-a-popup-window*
|
||||
>
|
||||
" Required:
|
||||
" - width [float range [0 ~ 1]] or [integer range [8 ~ ]]
|
||||
" - height [float range [0 ~ 1]] or [integer range [4 ~ ]]
|
||||
"
|
||||
" Optional:
|
||||
" - xoffset [float default 0.5 range [0 ~ 1]]
|
||||
" - yoffset [float default 0.5 range [0 ~ 1]]
|
||||
" - relative [boolean default v:false]
|
||||
" - border [string default 'rounded']: Border style
|
||||
" - 'rounded' / 'sharp' / 'horizontal' / 'vertical' / 'top' / 'bottom' / 'left' / 'right'
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||
<
|
||||
Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2
|
||||
or above) by putting `--tmux` options in `tmux` key.
|
||||
>
|
||||
" See `--tmux` option in `man fzf` for available options
|
||||
" [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
||||
if exists('$TMUX')
|
||||
let g:fzf_layout = { 'tmux': '90%,70%' }
|
||||
else
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||
endif
|
||||
<
|
||||
|
||||
< Hide statusline >___________________________________________________________~
|
||||
*fzf-hide-statusline*
|
||||
|
||||
When fzf starts in a terminal buffer, the file type of the buffer is set to
|
||||
`fzf`. So you can set up `FileType fzf` autocmd to customize the settings of
|
||||
`fzf`. So you can set up `FileType fzf` autocmd to customize the settings of
|
||||
the window.
|
||||
|
||||
For example, if you use the default layout (`{'down': '~40%'}`) on Neovim, you
|
||||
might want to temporarily disable the statusline for a cleaner look.
|
||||
For example, if you open fzf on the bottom on the screen (e.g. `{'down':
|
||||
'40%'}`), you might want to temporarily disable the statusline for a cleaner
|
||||
look.
|
||||
>
|
||||
if has('nvim') && !exists('g:fzf_layout')
|
||||
autocmd! FileType fzf
|
||||
autocmd FileType fzf set laststatus=0 noshowmode noruler
|
||||
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
|
||||
endif
|
||||
let g:fzf_layout = { 'down': '30%' }
|
||||
autocmd! FileType fzf
|
||||
autocmd FileType fzf set laststatus=0 noshowmode noruler
|
||||
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
|
||||
<
|
||||
|
||||
LICENSE *fzf-license*
|
||||
@@ -338,7 +501,7 @@ LICENSE *fzf-license*
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019 Junegunn Choi
|
||||
Copyright (c) 2013-2026 Junegunn Choi
|
||||
|
||||
==============================================================================
|
||||
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
module github.com/junegunn/fzf
|
||||
|
||||
require (
|
||||
github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635 // indirect
|
||||
github.com/gdamore/tcell v0.0.0-20170915061752-0a0db94084df
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
|
||||
github.com/jtolds/gls v4.2.1+incompatible // indirect
|
||||
github.com/lucasb-eyer/go-colorful v0.0.0-20170223221042-c900de9dbbc7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c
|
||||
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c
|
||||
github.com/mattn/go-shellwords v1.0.3
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
|
||||
golang.org/x/crypto v0.0.0-20170728183002-558b6879de74
|
||||
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 // indirect
|
||||
golang.org/x/text v0.0.0-20170530162606-4ee4af566555 // indirect
|
||||
github.com/charlievieth/fastwalk v1.0.14
|
||||
github.com/gdamore/tcell/v2 v2.9.0
|
||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741
|
||||
github.com/mattn/go-isatty v0.0.22
|
||||
github.com/rivo/uniseg v0.4.7
|
||||
golang.org/x/sys v0.35.0
|
||||
golang.org/x/term v0.34.0
|
||||
)
|
||||
|
||||
go 1.13
|
||||
require (
|
||||
github.com/gdamore/encoding v1.0.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
)
|
||||
|
||||
go 1.23.0
|
||||
|
||||
@@ -1,26 +1,54 @@
|
||||
github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635 h1:hheUEMzaOie/wKeIc1WPa7CDVuIO5hqQxjS+dwTQEnI=
|
||||
github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635/go.mod h1:yrQYJKKDTrHmbYxI7CYi+/hbdiDT2m4Hj+t0ikCjsrQ=
|
||||
github.com/gdamore/tcell v0.0.0-20170915061752-0a0db94084df h1:tLS1QD2puA1USLvkEnGfOt+Zp2ijtNIK3z2YFaIZZR4=
|
||||
github.com/gdamore/tcell v0.0.0-20170915061752-0a0db94084df/go.mod h1:tqyG50u7+Ctv1w5VX67kLzKcj9YXR/JSBZQq/+mLl1A=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/lucasb-eyer/go-colorful v0.0.0-20170223221042-c900de9dbbc7 h1:G52I+Gk/wPD4HKvKT0Vxxp9OUPxqKs3OK6rffSPtNkA=
|
||||
github.com/lucasb-eyer/go-colorful v0.0.0-20170223221042-c900de9dbbc7/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4=
|
||||
github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c h1:3nKFouDdpgGUV/uerJcYWH45ZbJzX0SiVWfTgmUeTzc=
|
||||
github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c h1:eFzthqtg3W6Pihj3DMTXLAF4f+ge5r5Ie5g6HLIZAF0=
|
||||
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-shellwords v1.0.3 h1:K/VxK7SZ+cvuPgFSLKi5QPI9Vr/ipOf4C1gN+ntueUk=
|
||||
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||
golang.org/x/crypto v0.0.0-20170728183002-558b6879de74 h1:/0jf0Cx3u07Ma4EzUA6NIGuvk9hb3Br6i9V8atthkwk=
|
||||
golang.org/x/crypto v0.0.0-20170728183002-558b6879de74/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 h1:rM0ROo5vb9AdYJi1110yjWGMej9ITfKddS89P3Fkhug=
|
||||
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20170530162606-4ee4af566555 h1:pjwO9HxObpgZBurDvTLFbSinaf3+cKpTAjVfiGaHwrI=
|
||||
golang.org/x/text v0.0.0-20170530162606-4ee4af566555/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
github.com/charlievieth/fastwalk v1.0.14 h1:3Eh5uaFGwHZd8EGwTjJnSpBkfwfsak9h6ICgnWlhAyg=
|
||||
github.com/charlievieth/fastwalk v1.0.14/go.mod h1:diVcUreiU1aQ4/Wu3NbxxH4/KYdKpLDojrQ1Bb2KgNY=
|
||||
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
||||
github.com/gdamore/tcell/v2 v2.9.0 h1:N6t+eqK7/xwtRPwxzs1PXeRWnm0H9l02CrgJ7DLn1ys=
|
||||
github.com/gdamore/tcell/v2 v2.9.0/go.mod h1:8/ZoqM9rxzYphT9tH/9LnunhV9oPBqwS8WHGYm5nrmo=
|
||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 h1:7dYDtfMDfKzjT+DVfIS4iqknSEKtZpEcXtu6vuaasHs=
|
||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.20.0
|
||||
version=0.72.0
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
binary_arch=
|
||||
shells="bash zsh fish"
|
||||
shells="bash zsh fish nushell"
|
||||
prefix='~/.fzf'
|
||||
prefix_expand=~/.fzf
|
||||
fish_dir=${XDG_CONFIG_HOME:-$HOME/.config}/fish
|
||||
nushell_autoload_dir=${XDG_CONFIG_HOME:-$HOME/.config}/nushell/autoload
|
||||
|
||||
help() {
|
||||
cat << EOF
|
||||
@@ -28,9 +28,7 @@ usage: $0 [OPTIONS]
|
||||
--no-bash Do not set up bash configuration
|
||||
--no-zsh Do not set up zsh configuration
|
||||
--no-fish Do not set up fish configuration
|
||||
|
||||
--32 Download 32-bit binary
|
||||
--64 Download 64-bit binary
|
||||
--no-nushell Do not set up nushell configuration
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -50,18 +48,17 @@ for opt in "$@"; do
|
||||
prefix_expand=${XDG_CONFIG_HOME:-$HOME/.config}/fzf/fzf
|
||||
mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/fzf"
|
||||
;;
|
||||
--key-bindings) key_bindings=1 ;;
|
||||
--no-key-bindings) key_bindings=0 ;;
|
||||
--completion) auto_completion=1 ;;
|
||||
--no-completion) auto_completion=0 ;;
|
||||
--update-rc) update_config=1 ;;
|
||||
--no-update-rc) update_config=0 ;;
|
||||
--32) binary_arch=386 ;;
|
||||
--64) binary_arch=amd64 ;;
|
||||
--bin) ;;
|
||||
--no-bash) shells=${shells/bash/} ;;
|
||||
--no-zsh) shells=${shells/zsh/} ;;
|
||||
--no-fish) shells=${shells/fish/} ;;
|
||||
--key-bindings) key_bindings=1 ;;
|
||||
--no-key-bindings) key_bindings=0 ;;
|
||||
--completion) auto_completion=1 ;;
|
||||
--no-completion) auto_completion=0 ;;
|
||||
--update-rc) update_config=1 ;;
|
||||
--no-update-rc) update_config=0 ;;
|
||||
--bin) ;;
|
||||
--no-bash) shells=${shells/bash/} ;;
|
||||
--no-zsh) shells=${shells/zsh/} ;;
|
||||
--no-fish) shells=${shells/fish/} ;;
|
||||
--no-nushell) shells=${shells/nushell/} ;;
|
||||
*)
|
||||
echo "unknown option: $opt"
|
||||
help
|
||||
@@ -89,7 +86,7 @@ ask() {
|
||||
check_binary() {
|
||||
echo -n " - Checking fzf executable ... "
|
||||
local output
|
||||
output=$("$fzf_base"/bin/fzf --version 2>&1)
|
||||
output=$(FZF_DEFAULT_OPTS= "$fzf_base"/bin/fzf --version 2>&1)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: $output"
|
||||
binary_error="Invalid binary"
|
||||
@@ -110,7 +107,7 @@ check_binary() {
|
||||
|
||||
link_fzf_in_path() {
|
||||
if which_fzf="$(command -v fzf)"; then
|
||||
echo " - Found in \$PATH"
|
||||
echo ' - Found in $PATH'
|
||||
echo " - Creating symlink: bin/fzf -> $which_fzf"
|
||||
(cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf)
|
||||
check_binary && return
|
||||
@@ -118,35 +115,38 @@ link_fzf_in_path() {
|
||||
return 1
|
||||
}
|
||||
|
||||
tar_opts="-xzf -"
|
||||
if tar --no-same-owner -tf /dev/null 2> /dev/null; then
|
||||
tar_opts="--no-same-owner $tar_opts"
|
||||
fi
|
||||
|
||||
try_curl() {
|
||||
command -v curl > /dev/null &&
|
||||
if [[ $1 =~ tgz$ ]]; then
|
||||
curl -fL $1 | tar -xzf -
|
||||
else
|
||||
local temp=${TMPDIR:-/tmp}/fzf.zip
|
||||
curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
|
||||
fi
|
||||
if [[ $1 =~ tar.gz$ ]]; then
|
||||
curl -fL $1 | tar $tar_opts
|
||||
else
|
||||
local temp=${TMPDIR:-/tmp}/fzf.zip
|
||||
curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
|
||||
fi
|
||||
}
|
||||
|
||||
try_wget() {
|
||||
command -v wget > /dev/null &&
|
||||
if [[ $1 =~ tgz$ ]]; then
|
||||
wget -O - $1 | tar -xzf -
|
||||
else
|
||||
local temp=${TMPDIR:-/tmp}/fzf.zip
|
||||
wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
|
||||
fi
|
||||
if [[ $1 =~ tar.gz$ ]]; then
|
||||
wget -O - $1 | tar $tar_opts
|
||||
else
|
||||
local temp=${TMPDIR:-/tmp}/fzf.zip
|
||||
wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
|
||||
fi
|
||||
}
|
||||
|
||||
download() {
|
||||
echo "Downloading bin/fzf ..."
|
||||
if [[ ! "$version" =~ alpha ]]; then
|
||||
if [ -x "$fzf_base"/bin/fzf ]; then
|
||||
echo " - Already exists"
|
||||
check_binary && return
|
||||
fi
|
||||
link_fzf_in_path && return
|
||||
if [ -x "$fzf_base"/bin/fzf ]; then
|
||||
echo " - Already exists"
|
||||
check_binary && return
|
||||
fi
|
||||
link_fzf_in_path && return
|
||||
mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin
|
||||
if [ $? -ne 0 ]; then
|
||||
binary_error="Failed to create bin directory"
|
||||
@@ -154,9 +154,7 @@ download() {
|
||||
fi
|
||||
|
||||
local url
|
||||
[[ "$version" =~ alpha ]] &&
|
||||
url=https://github.com/junegunn/fzf-bin/releases/download/alpha/${1} ||
|
||||
url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}
|
||||
url=https://github.com/junegunn/fzf/releases/download/v$version/${1}
|
||||
set -o pipefail
|
||||
if ! (try_curl $url || try_wget $url); then
|
||||
set +o pipefail
|
||||
@@ -174,31 +172,30 @@ download() {
|
||||
}
|
||||
|
||||
# Try to download binary executable
|
||||
archi=$(uname -sm)
|
||||
archi=$(uname -smo 2> /dev/null || uname -sm)
|
||||
binary_available=1
|
||||
binary_error=""
|
||||
case "$archi" in
|
||||
Darwin\ *64) download fzf-$version-darwin_${binary_arch:-amd64}.tgz ;;
|
||||
Darwin\ *86) download fzf-$version-darwin_${binary_arch:-386}.tgz ;;
|
||||
Linux\ armv5*) download fzf-$version-linux_${binary_arch:-arm5}.tgz ;;
|
||||
Linux\ armv6*) download fzf-$version-linux_${binary_arch:-arm6}.tgz ;;
|
||||
Linux\ armv7*) download fzf-$version-linux_${binary_arch:-arm7}.tgz ;;
|
||||
Linux\ armv8*) download fzf-$version-linux_${binary_arch:-arm8}.tgz ;;
|
||||
Linux\ aarch64*) download fzf-$version-linux_${binary_arch:-arm8}.tgz ;;
|
||||
Linux\ *64) download fzf-$version-linux_${binary_arch:-amd64}.tgz ;;
|
||||
Linux\ *86) download fzf-$version-linux_${binary_arch:-386}.tgz ;;
|
||||
FreeBSD\ *64) download fzf-$version-freebsd_${binary_arch:-amd64}.tgz ;;
|
||||
FreeBSD\ *86) download fzf-$version-freebsd_${binary_arch:-386}.tgz ;;
|
||||
OpenBSD\ *64) download fzf-$version-openbsd_${binary_arch:-amd64}.tgz ;;
|
||||
OpenBSD\ *86) download fzf-$version-openbsd_${binary_arch:-386}.tgz ;;
|
||||
CYGWIN*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;;
|
||||
MINGW*\ *86) download fzf-$version-windows_${binary_arch:-386}.zip ;;
|
||||
MINGW*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;;
|
||||
MSYS*\ *86) download fzf-$version-windows_${binary_arch:-386}.zip ;;
|
||||
MSYS*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;;
|
||||
Windows*\ *86) download fzf-$version-windows_${binary_arch:-386}.zip ;;
|
||||
Windows*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;;
|
||||
*) binary_available=0 binary_error=1 ;;
|
||||
Darwin\ arm64*) download fzf-$version-darwin_arm64.tar.gz ;;
|
||||
Darwin\ x86_64*) download fzf-$version-darwin_amd64.tar.gz ;;
|
||||
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
|
||||
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
|
||||
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;
|
||||
Linux\ armv8*) download fzf-$version-linux_arm64.tar.gz ;;
|
||||
Linux\ aarch64\ Android) download fzf-$version-android_arm64.tar.gz ;;
|
||||
Linux\ aarch64*) download fzf-$version-linux_arm64.tar.gz ;;
|
||||
Linux\ loongarch64*) download fzf-$version-linux_loong64.tar.gz ;;
|
||||
Linux\ riscv64*) download fzf-$version-linux_riscv64.tar.gz ;;
|
||||
Linux\ ppc64le*) download fzf-$version-linux_ppc64le.tar.gz ;;
|
||||
Linux\ *64*) download fzf-$version-linux_amd64.tar.gz ;;
|
||||
Linux\ s390x*) download fzf-$version-linux_s390x.tar.gz ;;
|
||||
FreeBSD\ *64*) download fzf-$version-freebsd_amd64.tar.gz ;;
|
||||
OpenBSD\ *64*) download fzf-$version-openbsd_amd64.tar.gz ;;
|
||||
CYGWIN*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
||||
MINGW*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
||||
MSYS*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
||||
Windows*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
||||
*) binary_available=0 binary_error=1 ;;
|
||||
esac
|
||||
|
||||
cd "$fzf_base"
|
||||
@@ -209,12 +206,12 @@ if [ -n "$binary_error" ]; then
|
||||
echo " - $binary_error !!!"
|
||||
fi
|
||||
if command -v go > /dev/null; then
|
||||
echo -n "Building binary (go get -u github.com/junegunn/fzf) ... "
|
||||
echo -n "Building binary (go install github.com/junegunn/fzf) ... "
|
||||
if [ -z "${GOPATH-}" ]; then
|
||||
export GOPATH="${TMPDIR:-/tmp}/fzf-gopath"
|
||||
mkdir -p "$GOPATH"
|
||||
fi
|
||||
if go get -u github.com/junegunn/fzf; then
|
||||
if go install -ldflags "-s -w -X main.version=$version -X main.revision=go-install" github.com/junegunn/fzf; then
|
||||
echo "OK"
|
||||
cp "$GOPATH/bin/fzf" "$fzf_base/bin/"
|
||||
else
|
||||
@@ -227,10 +224,12 @@ if [ -n "$binary_error" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ "$*" =~ "--bin" ]] && exit 0
|
||||
[[ $* =~ "--bin" ]] && exit 0
|
||||
|
||||
for s in $shells; do
|
||||
if ! command -v "$s" > /dev/null; then
|
||||
bin=$s
|
||||
[[ "$s" = nushell ]] && bin=nu
|
||||
if ! command -v "$bin" > /dev/null; then
|
||||
shells=${shells/$s/}
|
||||
fi
|
||||
done
|
||||
@@ -254,16 +253,17 @@ fi
|
||||
|
||||
echo
|
||||
for shell in $shells; do
|
||||
[[ "$shell" = fish ]] && continue
|
||||
fzf_completion="source \"$fzf_base/shell/completion.${shell}\""
|
||||
fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\""
|
||||
[[ $shell == fish ]] && continue
|
||||
[[ $shell == nushell ]] && continue
|
||||
src=${prefix_expand}.${shell}
|
||||
echo -n "Generate $src ... "
|
||||
|
||||
fzf_completion="[[ \$- == *i* ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null"
|
||||
if [ $auto_completion -eq 0 ]; then
|
||||
fzf_completion="# $fzf_completion"
|
||||
fi
|
||||
|
||||
fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\""
|
||||
if [ $key_bindings -eq 0 ]; then
|
||||
fzf_key_bindings="# $fzf_key_bindings"
|
||||
fi
|
||||
@@ -272,9 +272,19 @@ for shell in $shells; do
|
||||
# Setup fzf
|
||||
# ---------
|
||||
if [[ ! "\$PATH" == *$fzf_base_esc/bin* ]]; then
|
||||
export PATH="\${PATH:+\${PATH}:}$fzf_base/bin"
|
||||
PATH="\${PATH:+\${PATH}:}$fzf_base/bin"
|
||||
fi
|
||||
|
||||
EOF
|
||||
|
||||
if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then
|
||||
if [[ $shell == zsh ]]; then
|
||||
echo "source <(fzf --$shell)" >> "$src"
|
||||
else
|
||||
echo "eval \"\$(fzf --$shell)\"" >> "$src"
|
||||
fi
|
||||
else
|
||||
cat >> "$src" << EOF
|
||||
# Auto-completion
|
||||
# ---------------
|
||||
$fzf_completion
|
||||
@@ -283,66 +293,64 @@ $fzf_completion
|
||||
# ------------
|
||||
$fzf_key_bindings
|
||||
EOF
|
||||
fi
|
||||
echo "OK"
|
||||
done
|
||||
|
||||
# fish
|
||||
if [[ "$shells" =~ fish ]]; then
|
||||
if [[ $shells =~ fish ]]; then
|
||||
echo -n "Update fish_user_paths ... "
|
||||
fish << EOF
|
||||
echo \$fish_user_paths | \grep "$fzf_base"/bin > /dev/null
|
||||
or set --universal fish_user_paths \$fish_user_paths "$fzf_base"/bin
|
||||
EOF
|
||||
[ $? -eq 0 ] && echo "OK" || echo "Failed"
|
||||
|
||||
mkdir -p "${fish_dir}/functions"
|
||||
if [ -e "${fish_dir}/functions/fzf.fish" ]; then
|
||||
echo -n "Remove unnecessary ${fish_dir}/functions/fzf.fish ... "
|
||||
rm -f "${fish_dir}/functions/fzf.fish" && echo "OK" || echo "Failed"
|
||||
fi
|
||||
|
||||
fish_binding="${fish_dir}/functions/fzf_key_bindings.fish"
|
||||
if [ $key_bindings -ne 0 ]; then
|
||||
echo -n "Symlink $fish_binding ... "
|
||||
ln -sf "$fzf_base/shell/key-bindings.fish" \
|
||||
"$fish_binding" && echo "OK" || echo "Failed"
|
||||
else
|
||||
echo -n "Removing $fish_binding ... "
|
||||
rm -f "$fish_binding"
|
||||
echo "OK"
|
||||
fi
|
||||
fi
|
||||
|
||||
append_line() {
|
||||
set -e
|
||||
|
||||
local update line file pat lno
|
||||
local update line file pat lines
|
||||
update="$1"
|
||||
line="$2"
|
||||
file="$3"
|
||||
pat="${4:-}"
|
||||
lno=""
|
||||
at_lno="${5:-}"
|
||||
lines=""
|
||||
|
||||
echo "Update $file:"
|
||||
echo " - $line"
|
||||
if [ -f "$file" ]; then
|
||||
if [ $# -lt 4 ]; then
|
||||
lno=$(\grep -nF "$line" "$file" | sed 's/:.*//' | tr '\n' ' ')
|
||||
if [[ -n $pat ]]; then
|
||||
lines=$(\grep -nF "$pat" "$file")
|
||||
else
|
||||
lno=$(\grep -nF "$pat" "$file" | sed 's/:.*//' | tr '\n' ' ')
|
||||
lines=$(\grep -nF "${line#"${line%%[![:space:]]*}"}" "$file")
|
||||
fi
|
||||
fi
|
||||
if [ -n "$lno" ]; then
|
||||
echo " - Already exists: line #$lno"
|
||||
else
|
||||
if [ $update -eq 1 ]; then
|
||||
|
||||
if [ -n "$lines" ]; then
|
||||
echo " - Already exists:"
|
||||
sed 's/^/ Line /' <<< "$lines"
|
||||
|
||||
update=0
|
||||
if ! \grep -qv "^[0-9]*:[[:space:]]*#" <<< "$lines"; then
|
||||
echo " - But they all seem to be commented"
|
||||
ask " - Continue modifying $file?"
|
||||
update=$?
|
||||
fi
|
||||
fi
|
||||
|
||||
set -e
|
||||
if [ "$update" -eq 1 ]; then
|
||||
if [[ -z $at_lno ]]; then
|
||||
[ -f "$file" ] && echo >> "$file"
|
||||
echo "$line" >> "$file"
|
||||
echo " + Added"
|
||||
else
|
||||
echo " ~ Skipped"
|
||||
sed -i.~fzf_bak "${at_lno}a\\"$'\n'"$line" "$file" && rm "$file.~fzf_bak"
|
||||
fi
|
||||
echo " + Added"
|
||||
else
|
||||
echo " ~ Skipped"
|
||||
fi
|
||||
|
||||
echo
|
||||
set +e
|
||||
}
|
||||
@@ -365,28 +373,103 @@ if [ $update_config -eq 2 ]; then
|
||||
fi
|
||||
echo
|
||||
for shell in $shells; do
|
||||
[[ "$shell" = fish ]] && continue
|
||||
[[ $shell == fish ]] && continue
|
||||
[[ $shell == nushell ]] && continue
|
||||
[ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc
|
||||
append_line $update_config "[ -f ${prefix}.${shell} ] && source ${prefix}.${shell}" "$dest" "${prefix}.${shell}"
|
||||
done
|
||||
|
||||
if [ $key_bindings -eq 1 ] && [[ "$shells" =~ fish ]]; then
|
||||
if [[ $shells =~ fish ]]; then
|
||||
bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
|
||||
if [ ! -e "$bind_file" ]; then
|
||||
create_file "$bind_file" \
|
||||
'function fish_user_key_bindings' \
|
||||
' fzf_key_bindings' \
|
||||
'end'
|
||||
if [[ $key_bindings -eq 1 || $auto_completion -eq 1 ]]; then
|
||||
mkdir -p "${fish_dir}/functions"
|
||||
if [[ $key_bindings -eq 1 && $auto_completion -eq 1 ]]; then
|
||||
create_file "$bind_file" \
|
||||
'function fish_user_key_bindings' \
|
||||
' fzf --fish | source' \
|
||||
'end'
|
||||
elif [[ $key_bindings -eq 1 ]]; then
|
||||
create_file "$bind_file" \
|
||||
'function fish_user_key_bindings' \
|
||||
" $fzf_key_bindings" \
|
||||
'end'
|
||||
elif [[ $auto_completion -eq 1 ]]; then
|
||||
create_file "$bind_file" \
|
||||
'function fish_user_key_bindings' \
|
||||
" $fzf_completion" \
|
||||
'end'
|
||||
fi
|
||||
lno_func=$(\grep -nF "function fish_user_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
|
||||
else
|
||||
lno_func=0
|
||||
fi
|
||||
else
|
||||
append_line $update_config "fzf_key_bindings" "$bind_file"
|
||||
echo "Check $bind_file:"
|
||||
lno_func=$(\grep -nF "function fish_user_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
|
||||
if [[ -z $lno_func ]]; then
|
||||
echo -e "function fish_user_key_bindings\nend" >> "$bind_file"
|
||||
lno_func=$(\grep -nF "function fish_user_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
|
||||
fi
|
||||
lno_keys=$(\grep -nF "fzf_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
|
||||
if [[ -n $lno_keys ]]; then
|
||||
echo " ** Found 'fzf_key_bindings' in line #$lno_keys"
|
||||
if [[ $key_bindings -eq 1 && $auto_completion -eq 1 ]]; then
|
||||
echo " ** You have to replace the line to 'fzf --fish | source'"
|
||||
elif [[ $key_bindings -eq 1 ]]; then
|
||||
echo " ** You have to replace the line to '$fzf_key_bindings'"
|
||||
else
|
||||
echo " ** You have to remove the line"
|
||||
fi
|
||||
echo
|
||||
else
|
||||
echo " - Clear"
|
||||
echo
|
||||
if [[ $key_bindings -eq 1 && $auto_completion -eq 1 ]]; then
|
||||
sed -i.~fzf_bak "\#$fzf_completion#d" "$bind_file" && rm "$bind_file.~fzf_bak"
|
||||
sed -i.~fzf_bak "\#$fzf_key_bindings#d" "$bind_file" && rm "$bind_file.~fzf_bak"
|
||||
append_line $update_config " fzf --fish | source" "$bind_file" "" "$lno_func"
|
||||
else
|
||||
sed -i.~fzf_bak '/fzf --fish \| source/d' "$bind_file" && rm "$bind_file.~fzf_bak"
|
||||
if [[ $key_bindings -eq 1 ]]; then
|
||||
sed -i.~fzf_bak "\#$fzf_completion#d" "$bind_file" && rm "$bind_file.~fzf_bak"
|
||||
append_line $update_config " $fzf_key_bindings" "$bind_file" "" "$lno_func"
|
||||
elif [[ $auto_completion -eq 1 ]]; then
|
||||
sed -i.~fzf_bak "\#$fzf_key_bindings#d" "$bind_file" && rm "$bind_file.~fzf_bak"
|
||||
append_line $update_config " $fzf_completion" "$bind_file" "" "$lno_func"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$shells" =~ nushell ]]; then
|
||||
if [[ $key_bindings -eq 1 || $auto_completion -eq 1 ]]; then
|
||||
echo "Setting up Nushell integration ..."
|
||||
mkdir -p "$nushell_autoload_dir"
|
||||
echo -n " Generate _fzf_integration.nu ... "
|
||||
if [[ $key_bindings -eq 1 && $auto_completion -eq 1 ]]; then
|
||||
"$fzf_base"/bin/fzf --nushell > "$nushell_autoload_dir/_fzf_integration.nu"
|
||||
elif [[ $key_bindings -eq 1 ]]; then
|
||||
cp "$fzf_base/shell/key-bindings.nu" "$nushell_autoload_dir/_fzf_integration.nu"
|
||||
else
|
||||
cp "$fzf_base/shell/completion.nu" "$nushell_autoload_dir/_fzf_integration.nu"
|
||||
fi
|
||||
echo "OK"
|
||||
echo
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $update_config -eq 1 ]; then
|
||||
echo 'Finished. Restart your shell or reload config file.'
|
||||
[[ "$shells" =~ bash ]] && echo ' source ~/.bashrc # bash'
|
||||
[[ "$shells" =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh"
|
||||
[[ "$shells" =~ fish ]] && [ $key_bindings -eq 1 ] && echo ' fzf_key_bindings # fish'
|
||||
if [[ $shells =~ bash ]]; then
|
||||
echo -n ' source ~/.bashrc # bash'
|
||||
[[ $archi =~ Darwin ]] && echo -n ' (.bashrc should be loaded from .bash_profile)'
|
||||
echo
|
||||
fi
|
||||
[[ $shells =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh"
|
||||
[[ $shells =~ fish && $lno_func -ne 0 ]] && echo ' fish_user_key_bindings # fish'
|
||||
[[ $shells =~ nushell ]] && echo ' # nushell: files are loaded automatically from autoload directory'
|
||||
echo
|
||||
echo 'Use uninstall script to remove fzf.'
|
||||
echo
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
$version="0.72.0"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
function check_binary () {
|
||||
Write-Host " - Checking fzf executable ... " -NoNewline
|
||||
$output=cmd /c $fzf_base\bin\fzf.exe --version 2>&1
|
||||
if (-not $?) {
|
||||
Write-Host "Error: $output"
|
||||
$binary_error="Invalid binary"
|
||||
} else {
|
||||
$output=(-Split $output)[0]
|
||||
if ($version -ne $output) {
|
||||
Write-Host "$output != $version"
|
||||
$binary_error="Invalid version"
|
||||
} else {
|
||||
Write-Host "$output"
|
||||
$binary_error=""
|
||||
return 1
|
||||
}
|
||||
}
|
||||
Remove-Item "$fzf_base\bin\fzf.exe"
|
||||
return 0
|
||||
}
|
||||
|
||||
function download {
|
||||
param($file)
|
||||
Write-Host "Downloading bin/fzf ..."
|
||||
if (Test-Path "$fzf_base\bin\fzf.exe") {
|
||||
Write-Host " - Already exists"
|
||||
if (check_binary) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (-not (Test-Path "$fzf_base\bin")) {
|
||||
md "$fzf_base\bin"
|
||||
}
|
||||
if (-not $?) {
|
||||
$binary_error="Failed to create bin directory"
|
||||
return
|
||||
}
|
||||
cd "$fzf_base\bin"
|
||||
$url="https://github.com/junegunn/fzf/releases/download/v$version/$file"
|
||||
$temp=$env:TMP + "\fzf.zip"
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
if ($PSVersionTable.PSVersion.Major -ge 3) {
|
||||
Invoke-WebRequest -Uri $url -OutFile $temp
|
||||
} else {
|
||||
(New-Object Net.WebClient).DownloadFile($url, $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath("$temp"))
|
||||
}
|
||||
if ($?) {
|
||||
(Microsoft.PowerShell.Archive\Expand-Archive -Path $temp -DestinationPath .); (Remove-Item $temp)
|
||||
} else {
|
||||
$binary_error="Failed to download with powershell"
|
||||
}
|
||||
if (-not (Test-Path fzf.exe)) {
|
||||
$binary_error="Failed to download $file"
|
||||
return
|
||||
}
|
||||
echo y | icacls $fzf_base\bin\fzf.exe /grant Administrator:F ; check_binary >$null
|
||||
}
|
||||
|
||||
download "fzf-$version-windows_amd64.zip"
|
||||
|
||||
Write-Host 'For more information, see: https://github.com/junegunn/fzf'
|
||||
@@ -1,9 +1,115 @@
|
||||
package main
|
||||
|
||||
import "github.com/junegunn/fzf/src"
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
var revision string
|
||||
fzf "github.com/junegunn/fzf/src"
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version = "0.72"
|
||||
var revision = "devel"
|
||||
|
||||
//go:embed shell/key-bindings.bash
|
||||
var bashKeyBindings []byte
|
||||
|
||||
//go:embed shell/completion.bash
|
||||
var bashCompletion []byte
|
||||
|
||||
//go:embed shell/key-bindings.zsh
|
||||
var zshKeyBindings []byte
|
||||
|
||||
//go:embed shell/completion.zsh
|
||||
var zshCompletion []byte
|
||||
|
||||
//go:embed shell/key-bindings.fish
|
||||
var fishKeyBindings []byte
|
||||
|
||||
//go:embed shell/key-bindings.nu
|
||||
var nushellKeyBindings []byte
|
||||
|
||||
//go:embed shell/completion.nu
|
||||
var nushellCompletion []byte
|
||||
|
||||
//go:embed shell/completion.fish
|
||||
var fishCompletion []byte
|
||||
|
||||
//go:embed man/man1/fzf.1
|
||||
var manPage []byte
|
||||
|
||||
func printScript(label string, content []byte) {
|
||||
fmt.Println("### " + label + " ###")
|
||||
fmt.Println(strings.TrimSpace(string(content)))
|
||||
fmt.Println("### end: " + label + " ###")
|
||||
}
|
||||
|
||||
func exit(code int, err error) {
|
||||
if code == fzf.ExitError && err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
}
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func main() {
|
||||
fzf.Run(fzf.ParseOptions(), revision)
|
||||
protector.Protect()
|
||||
|
||||
options, err := fzf.ParseOptions(true, os.Args[1:])
|
||||
if err != nil {
|
||||
exit(fzf.ExitError, err)
|
||||
return
|
||||
}
|
||||
if options.Bash {
|
||||
printScript("key-bindings.bash", bashKeyBindings)
|
||||
printScript("completion.bash", bashCompletion)
|
||||
return
|
||||
}
|
||||
if options.Zsh {
|
||||
printScript("key-bindings.zsh", zshKeyBindings)
|
||||
printScript("completion.zsh", zshCompletion)
|
||||
return
|
||||
}
|
||||
if options.Fish {
|
||||
printScript("key-bindings.fish", fishKeyBindings)
|
||||
printScript("completion.fish", fishCompletion)
|
||||
return
|
||||
}
|
||||
if options.Nushell {
|
||||
printScript("key-bindings.nu", nushellKeyBindings)
|
||||
printScript("completion.nu", nushellCompletion)
|
||||
return
|
||||
}
|
||||
if options.Help {
|
||||
fmt.Print(fzf.Usage)
|
||||
return
|
||||
}
|
||||
if options.Version {
|
||||
if len(revision) > 0 {
|
||||
fmt.Printf("%s (%s)\n", version, revision)
|
||||
} else {
|
||||
fmt.Println(version)
|
||||
}
|
||||
return
|
||||
}
|
||||
if options.Man {
|
||||
file := fzf.WriteTemporaryFile([]string{string(manPage)}, "\n")
|
||||
if len(file) == 0 {
|
||||
fmt.Print(string(manPage))
|
||||
return
|
||||
}
|
||||
defer os.Remove(file)
|
||||
cmd := exec.Command("man", file)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Print(string(manPage))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
code, err := fzf.Run(options)
|
||||
exit(code, err)
|
||||
}
|
||||
|
||||
+29
-15
@@ -1,7 +1,7 @@
|
||||
.ig
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019 Junegunn Choi
|
||||
Copyright (c) 2013-2026 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -21,34 +21,48 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf-tmux 1 "Dec 2019" "fzf 0.20.0" "fzf-tmux - open fzf in tmux split pane"
|
||||
.TH fzf\-tmux 1 "Apr 2026" "fzf 0.72.0" "fzf\-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf-tmux - open fzf in tmux split pane
|
||||
fzf\-tmux - open fzf in tmux split pane
|
||||
|
||||
.SH SYNOPSIS
|
||||
.B fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
|
||||
.B fzf\-tmux [\fILAYOUT OPTIONS\fR] [\-\-] [\fIFZF OPTIONS\fR]
|
||||
|
||||
.SH DESCRIPTION
|
||||
fzf-tmux is a wrapper script for fzf that opens fzf in a tmux split pane. It is
|
||||
designed to work just like fzf except that it does not take up the whole
|
||||
screen. You can safely use fzf-tmux instead of fzf in your scripts as the extra
|
||||
options will be silently ignored if you're not on tmux.
|
||||
fzf\-tmux is a wrapper script for fzf that opens fzf in a tmux split pane or in
|
||||
a tmux popup window. It is designed to work just like fzf except that it does
|
||||
not take up the whole screen. You can safely use fzf\-tmux instead of fzf in
|
||||
your scripts as the extra options will be silently ignored if you're not on
|
||||
tmux.
|
||||
|
||||
.SH OPTIONS
|
||||
.SS Layout
|
||||
.SH LAYOUT OPTIONS
|
||||
|
||||
(default: \fB-d 50%\fR)
|
||||
(default layout: \fB\-d 50%\fR)
|
||||
|
||||
.SS Popup window
|
||||
(requires tmux 3.2 or above)
|
||||
.TP
|
||||
.B "-u [height[%]]"
|
||||
.B "\-p [WIDTH[%][,HEIGHT[%]]]"
|
||||
.TP
|
||||
.B "\-w WIDTH[%]"
|
||||
.TP
|
||||
.B "\-h WIDTH[%]"
|
||||
.TP
|
||||
.B "\-x COL"
|
||||
.TP
|
||||
.B "\-y ROW"
|
||||
|
||||
.SS Split pane
|
||||
.TP
|
||||
.B "\-u [height[%]]"
|
||||
Split above (up)
|
||||
.TP
|
||||
.B "-d [height[%]]"
|
||||
.B "\-d [height[%]]"
|
||||
Split below (down)
|
||||
.TP
|
||||
.B "-l [width[%]]"
|
||||
.B "\-l [width[%]]"
|
||||
Split left
|
||||
.TP
|
||||
.B "-r [width[%]]"
|
||||
.B "\-r [width[%]]"
|
||||
Split right
|
||||
|
||||
+1943
-380
File diff suppressed because it is too large
Load Diff
+460
-134
@@ -1,4 +1,4 @@
|
||||
" Copyright (c) 2017 Junegunn Choi
|
||||
" Copyright (c) 2013-2026 Junegunn Choi
|
||||
"
|
||||
" MIT License
|
||||
"
|
||||
@@ -49,17 +49,19 @@ if s:is_win
|
||||
|
||||
" Use utf-8 for fzf.vim commands
|
||||
" Return array of shell commands for cmd.exe
|
||||
let s:codepage = libcallnr('kernel32.dll', 'GetACP', 0)
|
||||
function! s:enc_to_cp(str)
|
||||
if !has('iconv')
|
||||
return a:str
|
||||
endif
|
||||
if !exists('s:codepage')
|
||||
let s:codepage = libcallnr('kernel32.dll', 'GetACP', 0)
|
||||
endif
|
||||
return iconv(a:str, &encoding, 'cp'.s:codepage)
|
||||
endfunction
|
||||
function! s:wrap_cmds(cmds)
|
||||
return map([
|
||||
\ '@echo off',
|
||||
\ 'setlocal enabledelayedexpansion']
|
||||
return map(['@echo off']
|
||||
\ + (has('gui_running') ? ['set TERM= > nul'] : [])
|
||||
\ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds])
|
||||
\ + ['endlocal'],
|
||||
\ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds]),
|
||||
\ '<SID>enc_to_cp(v:val."\r")')
|
||||
endfunction
|
||||
else
|
||||
@@ -79,11 +81,21 @@ else
|
||||
endif
|
||||
|
||||
function! s:shellesc_cmd(arg)
|
||||
let escaped = substitute(a:arg, '[&|<>()@^]', '^&', 'g')
|
||||
let escaped = substitute(escaped, '%', '%%', 'g')
|
||||
let escaped = substitute(escaped, '"', '\\^&', 'g')
|
||||
let escaped = substitute(escaped, '\(\\\+\)\(\\^\)', '\1\1\2', 'g')
|
||||
return '^"'.substitute(escaped, '\(\\\+\)$', '\1\1', '').'^"'
|
||||
let e = '"'
|
||||
let slashes = 0
|
||||
for c in split(a:arg, '\zs')
|
||||
if c ==# '\'
|
||||
let slashes += 1
|
||||
elseif c ==# '"'
|
||||
let e .= repeat('\', slashes + 1)
|
||||
let slashes = 0
|
||||
else
|
||||
let slashes = 0
|
||||
endif
|
||||
let e .= c
|
||||
endfor
|
||||
let e .= repeat('\', slashes) .'"'
|
||||
return substitute(substitute(e, '[&|<>()^!"]', '^&', 'g'), '%', '%%', 'g')
|
||||
endfunction
|
||||
|
||||
function! fzf#shellescape(arg, ...)
|
||||
@@ -91,7 +103,12 @@ function! fzf#shellescape(arg, ...)
|
||||
if shell =~# 'cmd.exe$'
|
||||
return s:shellesc_cmd(a:arg)
|
||||
endif
|
||||
return s:fzf_call('shellescape', a:arg)
|
||||
try
|
||||
let [shell, &shell] = [&shell, shell]
|
||||
return s:fzf_call('shellescape', a:arg)
|
||||
finally
|
||||
let [shell, &shell] = [&shell, shell]
|
||||
endtry
|
||||
endfunction
|
||||
|
||||
function! s:fzf_getcwd()
|
||||
@@ -110,45 +127,136 @@ function! s:fzf_tempname()
|
||||
return s:fzf_call('tempname')
|
||||
endfunction
|
||||
|
||||
let s:default_layout = { 'down': '~40%' }
|
||||
let s:layout_keys = ['window', 'up', 'down', 'left', 'right']
|
||||
let s:layout_keys = ['window', 'tmux', 'up', 'down', 'left', 'right']
|
||||
let s:fzf_go = s:base_dir.'/bin/fzf'
|
||||
let s:fzf_tmux = s:base_dir.'/bin/fzf-tmux'
|
||||
let s:install = s:base_dir.'/install'
|
||||
let s:installed = 0
|
||||
|
||||
let s:cpo_save = &cpo
|
||||
set cpo&vim
|
||||
|
||||
function! s:fzf_exec()
|
||||
function! s:popup_support()
|
||||
return has('nvim') ? has('nvim-0.4') : has('popupwin') && has('patch-8.2.191')
|
||||
endfunction
|
||||
|
||||
function! s:default_layout()
|
||||
return s:popup_support()
|
||||
\ ? { 'window' : { 'width': 0.9, 'height': 0.6 } }
|
||||
\ : { 'down': '~40%' }
|
||||
endfunction
|
||||
|
||||
function! fzf#install()
|
||||
if s:is_win && !has('win32unix')
|
||||
let script = s:base_dir.'/install.ps1'
|
||||
if !filereadable(script)
|
||||
throw script.' not found'
|
||||
endif
|
||||
let script = 'powershell -ExecutionPolicy Bypass -file ' . shellescape(script)
|
||||
else
|
||||
let script = s:base_dir.'/install'
|
||||
if !executable(script)
|
||||
throw script.' not found'
|
||||
endif
|
||||
let script .= ' --bin'
|
||||
endif
|
||||
|
||||
call s:warn('Running fzf installer ...')
|
||||
call system(script)
|
||||
if v:shell_error
|
||||
throw 'Failed to download fzf: '.script
|
||||
endif
|
||||
endfunction
|
||||
|
||||
let s:versions = {}
|
||||
function s:get_version(bin)
|
||||
if has_key(s:versions, a:bin)
|
||||
return s:versions[a:bin]
|
||||
end
|
||||
let command = (&shell =~ 'powershell\|pwsh' ? '&' : '') . s:fzf_call('shellescape', a:bin) . ' --version --no-height'
|
||||
let output = systemlist(command)
|
||||
if v:shell_error || empty(output)
|
||||
return ''
|
||||
endif
|
||||
let ver = matchstr(output[-1], '[0-9.]\+')
|
||||
let s:versions[a:bin] = ver
|
||||
return ver
|
||||
endfunction
|
||||
|
||||
function! s:compare_versions(a, b)
|
||||
let a = split(a:a, '\.')
|
||||
let b = split(a:b, '\.')
|
||||
for idx in range(0, max([len(a), len(b)]) - 1)
|
||||
let v1 = str2nr(get(a, idx, 0))
|
||||
let v2 = str2nr(get(b, idx, 0))
|
||||
if v1 < v2 | return -1
|
||||
elseif v1 > v2 | return 1
|
||||
endif
|
||||
endfor
|
||||
return 0
|
||||
endfunction
|
||||
|
||||
function! s:compare_binary_versions(a, b)
|
||||
return s:compare_versions(s:get_version(a:a), s:get_version(a:b))
|
||||
endfunction
|
||||
|
||||
let s:min_version = '0.53.0'
|
||||
let s:checked = {}
|
||||
function! fzf#exec(...)
|
||||
if !exists('s:exec')
|
||||
let binaries = []
|
||||
if executable('fzf')
|
||||
call add(binaries, 'fzf')
|
||||
endif
|
||||
if executable(s:fzf_go)
|
||||
let s:exec = s:fzf_go
|
||||
elseif executable('fzf')
|
||||
let s:exec = 'fzf'
|
||||
elseif s:is_win && !has('win32unix')
|
||||
call s:warn('fzf executable not found.')
|
||||
call s:warn('Download fzf binary for Windows from https://github.com/junegunn/fzf-bin/releases/')
|
||||
call s:warn('and place it as '.s:base_dir.'\bin\fzf.exe')
|
||||
throw 'fzf executable not found'
|
||||
elseif !s:installed && executable(s:install) &&
|
||||
\ input('fzf executable not found. Download binary? (y/n) ') =~? '^y'
|
||||
call add(binaries, s:fzf_go)
|
||||
endif
|
||||
|
||||
if empty(binaries)
|
||||
if input('fzf executable not found. Download binary? (y/n) ') =~? '^y'
|
||||
redraw
|
||||
call fzf#install()
|
||||
return fzf#exec()
|
||||
else
|
||||
redraw
|
||||
throw 'fzf executable not found'
|
||||
endif
|
||||
elseif len(binaries) > 1
|
||||
call sort(binaries, 's:compare_binary_versions')
|
||||
endif
|
||||
|
||||
let s:exec = binaries[-1]
|
||||
endif
|
||||
|
||||
let min_version = s:min_version
|
||||
if a:0 && s:compare_versions(a:1, min_version) > 0
|
||||
let min_version = a:1
|
||||
endif
|
||||
if !has_key(s:checked, min_version)
|
||||
let fzf_version = s:get_version(s:exec)
|
||||
if empty(fzf_version)
|
||||
let message = printf('Failed to run "%s --version"', s:exec)
|
||||
unlet s:exec
|
||||
throw message
|
||||
end
|
||||
|
||||
if s:compare_versions(fzf_version, min_version) >= 0
|
||||
let s:checked[min_version] = 1
|
||||
return s:exec
|
||||
elseif a:0 < 2 && input(printf('You need fzf %s or above. Found: %s. Download binary? (y/n) ', min_version, fzf_version)) =~? '^y'
|
||||
let s:versions = {}
|
||||
unlet s:exec
|
||||
redraw
|
||||
echo
|
||||
call s:warn('Downloading fzf binary. Please wait ...')
|
||||
let s:installed = 1
|
||||
call system(s:install.' --bin')
|
||||
return s:fzf_exec()
|
||||
call fzf#install()
|
||||
return fzf#exec(min_version, 1)
|
||||
else
|
||||
redraw
|
||||
throw 'fzf executable not found'
|
||||
throw printf('You need to upgrade fzf (required: %s or above)', min_version)
|
||||
endif
|
||||
endif
|
||||
return fzf#shellescape(s:exec)
|
||||
|
||||
return s:exec
|
||||
endfunction
|
||||
|
||||
function! s:tmux_enabled()
|
||||
if has('gui_running')
|
||||
if has('gui_running') || !exists('$TMUX')
|
||||
return 0
|
||||
endif
|
||||
|
||||
@@ -157,10 +265,16 @@ function! s:tmux_enabled()
|
||||
endif
|
||||
|
||||
let s:tmux = 0
|
||||
if exists('$TMUX') && executable(s:fzf_tmux)
|
||||
let output = system('tmux -V')
|
||||
let s:tmux = !v:shell_error && output >= 'tmux 1.7'
|
||||
if !executable(s:fzf_tmux)
|
||||
if executable('fzf-tmux')
|
||||
let s:fzf_tmux = 'fzf-tmux'
|
||||
else
|
||||
return 0
|
||||
endif
|
||||
endif
|
||||
|
||||
let output = system('tmux -V')
|
||||
let s:tmux = !v:shell_error && output >= 'tmux 1.7'
|
||||
return s:tmux
|
||||
endfunction
|
||||
|
||||
@@ -169,21 +283,6 @@ function! s:escape(path)
|
||||
return s:is_win ? escape(path, '$') : path
|
||||
endfunction
|
||||
|
||||
" Upgrade legacy options
|
||||
function! s:upgrade(dict)
|
||||
let copy = copy(a:dict)
|
||||
if has_key(copy, 'tmux')
|
||||
let copy.down = remove(copy, 'tmux')
|
||||
endif
|
||||
if has_key(copy, 'tmux_height')
|
||||
let copy.down = remove(copy, 'tmux_height')
|
||||
endif
|
||||
if has_key(copy, 'tmux_width')
|
||||
let copy.right = remove(copy, 'tmux_width')
|
||||
endif
|
||||
return copy
|
||||
endfunction
|
||||
|
||||
function! s:error(msg)
|
||||
echohl ErrorMsg
|
||||
echom a:msg
|
||||
@@ -229,9 +328,17 @@ function! s:common_sink(action, lines) abort
|
||||
endif
|
||||
try
|
||||
let empty = empty(s:fzf_expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified
|
||||
let autochdir = &autochdir
|
||||
set noautochdir
|
||||
" Preserve the current working directory in case it's changed during
|
||||
" the execution (e.g. `set autochdir` or `autocmd BufEnter * lcd ...`)
|
||||
let cwd = exists('w:fzf_pushd') ? w:fzf_pushd.dir : expand('%:p:h')
|
||||
for item in a:lines
|
||||
if has('win32unix') && item !~ '/'
|
||||
let item = substitute(item, '\', '/', 'g')
|
||||
end
|
||||
if item[0] != '~' && item !~ (s:is_win ? '^\([A-Z]:\)\?\' : '^/')
|
||||
let sep = s:is_win ? '\' : '/'
|
||||
let item = join([cwd, item], cwd[len(cwd)-1] == sep ? '' : sep)
|
||||
endif
|
||||
if empty
|
||||
execute 'e' s:escape(item)
|
||||
let empty = 0
|
||||
@@ -245,13 +352,13 @@ function! s:common_sink(action, lines) abort
|
||||
endfor
|
||||
catch /^Vim:Interrupt$/
|
||||
finally
|
||||
let &autochdir = autochdir
|
||||
silent! autocmd! fzf_swap
|
||||
endtry
|
||||
endfunction
|
||||
|
||||
function! s:get_color(attr, ...)
|
||||
let gui = !s:is_win && !has('win32unix') && has('termguicolors') && &termguicolors
|
||||
" Force 24 bit colors: g:fzf_force_termguicolors (temporary workaround for https://github.com/junegunn/fzf.vim/issues/1152)
|
||||
let gui = get(g:, 'fzf_force_termguicolors', 0) || (!s:is_win && !has('win32unix') && (has('gui_running') || has('termguicolors') && &termguicolors))
|
||||
let fam = gui ? 'gui' : 'cterm'
|
||||
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
|
||||
for group in a:000
|
||||
@@ -290,7 +397,7 @@ function! fzf#wrap(...)
|
||||
let expects = map(copy(args), 'type(v:val)')
|
||||
let tidx = 0
|
||||
for arg in copy(a:000)
|
||||
let tidx = index(expects, type(arg), tidx)
|
||||
let tidx = index(expects, type(arg) == 6 ? type(0) : type(arg), tidx)
|
||||
if tidx < 0
|
||||
throw 'Invalid arguments (expected: [name string] [opts dict] [fullscreen boolean])'
|
||||
endif
|
||||
@@ -315,7 +422,7 @@ function! fzf#wrap(...)
|
||||
if !exists('g:fzf_layout') && exists('g:fzf_height')
|
||||
let opts.down = g:fzf_height
|
||||
else
|
||||
let opts = extend(opts, s:validate_layout(get(g:, 'fzf_layout', s:default_layout)))
|
||||
let opts = extend(opts, s:validate_layout(get(g:, 'fzf_layout', s:default_layout())))
|
||||
endif
|
||||
endif
|
||||
|
||||
@@ -333,13 +440,13 @@ function! fzf#wrap(...)
|
||||
endif
|
||||
|
||||
" Action: g:fzf_action
|
||||
if !s:has_any(opts, ['sink', 'sink*'])
|
||||
if !s:has_any(opts, ['sink', 'sinklist', 'sink*'])
|
||||
let opts._action = get(g:, 'fzf_action', s:default_action)
|
||||
let opts.options .= ' --expect='.join(keys(opts._action), ',')
|
||||
function! opts.sink(lines) abort
|
||||
function! opts.sinklist(lines) abort
|
||||
return s:common_sink(self._action, a:lines)
|
||||
endfunction
|
||||
let opts['sink*'] = remove(opts, 'sink')
|
||||
let opts['sink*'] = opts.sinklist " For backward compatibility
|
||||
endif
|
||||
|
||||
return opts
|
||||
@@ -358,41 +465,67 @@ function! s:use_sh()
|
||||
return [shell, shellslash, shellcmdflag, shellxquote]
|
||||
endfunction
|
||||
|
||||
function! s:writefile(...)
|
||||
if call('writefile', a:000) == -1
|
||||
throw 'Failed to write temporary file. Check if you can write to the path tempname() returns.'
|
||||
endif
|
||||
endfunction
|
||||
|
||||
function! s:extract_option(opts, name)
|
||||
let opt = ''
|
||||
let expect = 0
|
||||
" There are a few cases where this function doesn't work as expected.
|
||||
" Let's just assume such cases are extremely unlikely in real world.
|
||||
" e.g. --query --border
|
||||
for word in split(a:opts)
|
||||
if expect && word !~ '^"\=-'
|
||||
let opt = opt . ' ' . word
|
||||
let expect = 0
|
||||
elseif word == '--no-'.a:name
|
||||
let opt = ''
|
||||
elseif word =~ '^--'.a:name.'='
|
||||
let opt = word
|
||||
elseif word =~ '^--'.a:name.'$'
|
||||
let opt = word
|
||||
let expect = 1
|
||||
elseif expect
|
||||
let expect = 0
|
||||
endif
|
||||
endfor
|
||||
return opt
|
||||
endfunction
|
||||
|
||||
let s:need_cmd_window = has('win32unix') && $TERM_PROGRAM ==# 'mintty' && s:compare_versions($TERM_PROGRAM_VERSION, '3.4.5') < 0 && !executable('winpty')
|
||||
|
||||
function! fzf#run(...) abort
|
||||
try
|
||||
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
|
||||
|
||||
let dict = exists('a:1') ? s:upgrade(a:1) : {}
|
||||
let dict = exists('a:1') ? copy(a:1) : {}
|
||||
let temps = { 'result': s:fzf_tempname() }
|
||||
let optstr = s:evaluate_opts(get(dict, 'options', ''))
|
||||
try
|
||||
let fzf_exec = s:fzf_exec()
|
||||
let fzf_exec = shellescape(fzf#exec())
|
||||
catch
|
||||
throw v:exception
|
||||
endtry
|
||||
|
||||
if !has_key(dict, 'dir')
|
||||
if !s:present(dict, 'dir')
|
||||
let dict.dir = s:fzf_getcwd()
|
||||
endif
|
||||
if has('win32unix') && has_key(dict, 'dir')
|
||||
if has('win32unix') && s:present(dict, 'dir')
|
||||
let dict.dir = fnamemodify(dict.dir, ':p')
|
||||
endif
|
||||
|
||||
if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) && !s:is_win
|
||||
let temps.source = s:fzf_tempname()
|
||||
call writefile(s:wrap_cmds(split($FZF_DEFAULT_COMMAND, "\n")), temps.source)
|
||||
let dict.source = (empty($SHELL) ? &shell : $SHELL).' '.fzf#shellescape(temps.source)
|
||||
endif
|
||||
|
||||
if has_key(dict, 'source')
|
||||
let source = dict.source
|
||||
let type = type(source)
|
||||
if type == 1
|
||||
let prefix = '( '.source.' )|'
|
||||
let prefix = '('.source.')|'
|
||||
elseif type == 3
|
||||
let temps.input = s:fzf_tempname()
|
||||
call writefile(map(source, '<SID>enc_to_cp(v:val)'), temps.input)
|
||||
let prefix = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input).'|'
|
||||
call s:writefile(source, temps.input)
|
||||
let prefix = (s:is_win ? 'type ' : 'command cat ').fzf#shellescape(temps.input).'|'
|
||||
else
|
||||
throw 'Invalid source type'
|
||||
endif
|
||||
@@ -400,25 +533,35 @@ try
|
||||
let prefix = ''
|
||||
endif
|
||||
|
||||
let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0)
|
||||
let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0) || has_key(dict, 'tmux')
|
||||
let use_height = has_key(dict, 'down') && !has('gui_running') &&
|
||||
\ !(has('nvim') || s:is_win || has('win32unix') || s:present(dict, 'up', 'left', 'right', 'window')) &&
|
||||
\ executable('tput') && filereadable('/dev/tty')
|
||||
let has_vim8_term = has('terminal') && has('patch-8.0.995')
|
||||
let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win
|
||||
let use_term = has_nvim_term ||
|
||||
\ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || !use_height && s:present(dict, 'down', 'up', 'left', 'right', 'window'))
|
||||
let use_tmux = (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:tmux_enabled() && s:splittable(dict)
|
||||
let use_term = has_nvim_term || has_vim8_term
|
||||
\ && !s:need_cmd_window
|
||||
\ && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window'))
|
||||
let use_tmux = (has_key(dict, 'tmux') || (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:splittable(dict)) && s:tmux_enabled()
|
||||
if prefer_tmux && use_tmux
|
||||
let use_height = 0
|
||||
let use_term = 0
|
||||
endif
|
||||
if use_height
|
||||
if use_term
|
||||
let optstr .= ' --no-height --no-tmux'
|
||||
elseif use_height
|
||||
let height = s:calc_size(&lines, dict.down, dict)
|
||||
let optstr .= ' --height='.height
|
||||
elseif use_term
|
||||
let optstr .= ' --no-height'
|
||||
let optstr .= ' --no-tmux --height='.height
|
||||
endif
|
||||
|
||||
if exists('&winborder') && &winborder !=# '' && &winborder !=# 'none'
|
||||
" Add 1-column horizontal margin
|
||||
let optstr = join(['--margin 0,1', optstr])
|
||||
else
|
||||
" Respect --border option given in $FZF_DEFAULT_OPTS and 'options'
|
||||
let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr])
|
||||
endif
|
||||
|
||||
let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
|
||||
|
||||
if use_term
|
||||
@@ -444,21 +587,25 @@ function! s:present(dict, ...)
|
||||
endfunction
|
||||
|
||||
function! s:fzf_tmux(dict)
|
||||
let size = ''
|
||||
for o in ['up', 'down', 'left', 'right']
|
||||
if s:present(a:dict, o)
|
||||
let spec = a:dict[o]
|
||||
if (o == 'up' || o == 'down') && spec[0] == '~'
|
||||
let size = '-'.o[0].s:calc_size(&lines, spec, a:dict)
|
||||
else
|
||||
" Legacy boolean option
|
||||
let size = '-'.o[0].(spec == 1 ? '' : substitute(spec, '^\~', '', ''))
|
||||
let size = get(a:dict, 'tmux', '')
|
||||
if empty(size)
|
||||
for o in ['up', 'down', 'left', 'right']
|
||||
if s:present(a:dict, o)
|
||||
let size = o . ',' . a:dict[o]
|
||||
break
|
||||
endif
|
||||
break
|
||||
endif
|
||||
endfor
|
||||
return printf('LINES=%d COLUMNS=%d %s %s %s --',
|
||||
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
|
||||
endfor
|
||||
endif
|
||||
|
||||
" Legacy fzf-tmux options
|
||||
if size =~ '-'
|
||||
return printf('LINES=%d COLUMNS=%d %s %s %s --',
|
||||
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
|
||||
end
|
||||
|
||||
" Using native --tmux option
|
||||
let in = (has_key(a:dict, 'source') ? '' : ' --force-tty-in')
|
||||
return printf('%s --tmux %s%s', fzf#shellescape(fzf#exec()), size, in)
|
||||
endfunction
|
||||
|
||||
function! s:splittable(dict)
|
||||
@@ -511,7 +658,7 @@ function! s:dopopd()
|
||||
if s:fzf_getcwd() ==# w:fzf_pushd.dir && (!&autochdir || w:fzf_pushd.bufname ==# bufname(''))
|
||||
execute w:fzf_pushd.command s:escape(w:fzf_pushd.origin)
|
||||
endif
|
||||
unlet w:fzf_pushd
|
||||
unlet! w:fzf_pushd
|
||||
endfunction
|
||||
|
||||
function! s:xterm_launcher()
|
||||
@@ -530,21 +677,17 @@ else
|
||||
let s:launcher = function('s:xterm_launcher')
|
||||
endif
|
||||
|
||||
function! s:exit_handler(code, command, ...)
|
||||
if a:code == 130
|
||||
return 0
|
||||
elseif has('nvim') && a:code == 129
|
||||
" When deleting the terminal buffer while fzf is still running,
|
||||
" Nvim sends SIGHUP.
|
||||
return 0
|
||||
elseif a:code > 1
|
||||
function! s:exit_handler(dict, code, command, ...)
|
||||
if has_key(a:dict, 'exit')
|
||||
call a:dict.exit(a:code)
|
||||
endif
|
||||
if a:code == 2
|
||||
call s:error('Error running ' . a:command)
|
||||
if !empty(a:000)
|
||||
sleep
|
||||
endif
|
||||
return 0
|
||||
endif
|
||||
return 1
|
||||
return a:code
|
||||
endfunction
|
||||
|
||||
function! s:execute(dict, command, use_height, temps) abort
|
||||
@@ -565,7 +708,7 @@ function! s:execute(dict, command, use_height, temps) abort
|
||||
endif
|
||||
if s:is_win
|
||||
let batchfile = s:fzf_tempname().'.bat'
|
||||
call writefile(s:wrap_cmds(command), batchfile)
|
||||
call s:writefile(s:wrap_cmds(command), batchfile)
|
||||
let command = batchfile
|
||||
let a:temps.batchfile = batchfile
|
||||
if has('nvim')
|
||||
@@ -581,10 +724,10 @@ function! s:execute(dict, command, use_height, temps) abort
|
||||
call jobstart(cmd, fzf)
|
||||
return []
|
||||
endif
|
||||
elseif has('win32unix') && $TERM !=# 'cygwin'
|
||||
elseif s:need_cmd_window
|
||||
let shellscript = s:fzf_tempname()
|
||||
call writefile([command], shellscript)
|
||||
let command = 'cmd.exe /C '.fzf#shellescape('set "TERM=" & start /WAIT sh -c '.shellscript)
|
||||
call s:writefile([command], shellscript)
|
||||
let command = 'start //WAIT sh -c '.shellscript
|
||||
let a:temps.shellscript = shellscript
|
||||
endif
|
||||
if a:use_height
|
||||
@@ -595,7 +738,8 @@ function! s:execute(dict, command, use_height, temps) abort
|
||||
endif
|
||||
let exit_status = v:shell_error
|
||||
redraw!
|
||||
return s:exit_handler(exit_status, command) ? s:collect(a:temps) : []
|
||||
let lines = s:collect(a:temps)
|
||||
return s:exit_handler(a:dict, exit_status, command) < 2 ? lines : []
|
||||
endfunction
|
||||
|
||||
function! s:execute_tmux(dict, command, temps) abort
|
||||
@@ -609,7 +753,8 @@ function! s:execute_tmux(dict, command, temps) abort
|
||||
call system(command)
|
||||
let exit_status = v:shell_error
|
||||
redraw!
|
||||
return s:exit_handler(exit_status, command) ? s:collect(a:temps) : []
|
||||
let lines = s:collect(a:temps)
|
||||
return s:exit_handler(a:dict, exit_status, command) < 2 ? lines : []
|
||||
endfunction
|
||||
|
||||
function! s:calc_size(max, val, dict)
|
||||
@@ -626,14 +771,45 @@ function! s:calc_size(max, val, dict)
|
||||
endif
|
||||
|
||||
let opts = $FZF_DEFAULT_OPTS.' '.s:evaluate_opts(get(a:dict, 'options', ''))
|
||||
let margin = stridx(opts, '--inline-info') > stridx(opts, '--no-inline-info') ? 1 : 2
|
||||
let margin += stridx(opts, '--border') > stridx(opts, '--no-border') ? 2 : 0
|
||||
let margin += stridx(opts, '--header') > stridx(opts, '--no-header')
|
||||
if opts =~ 'preview'
|
||||
return size
|
||||
endif
|
||||
let margin = match(opts, '--inline-info\|--info[^-]\{-}inline') > match(opts, '--no-inline-info\|--info[^-]\{-}\(default\|hidden\)') ? 1 : 2
|
||||
let margin += match(opts, '--border\([^-]\|$\)') > match(opts, '--no-border\([^-]\|$\)') ? 2 : 0
|
||||
if stridx(opts, '--header') > stridx(opts, '--no-header')
|
||||
let margin += len(split(opts, "\n"))
|
||||
endif
|
||||
return srcsz >= 0 ? min([srcsz + margin, size]) : size
|
||||
endfunction
|
||||
|
||||
function! s:getpos()
|
||||
return {'tab': tabpagenr(), 'win': winnr(), 'cnt': winnr('$'), 'tcnt': tabpagenr('$')}
|
||||
return {'tab': tabpagenr(), 'win': winnr(), 'winid': win_getid(), 'cnt': winnr('$'), 'tcnt': tabpagenr('$')}
|
||||
endfunction
|
||||
|
||||
function! s:border_opt(window)
|
||||
if type(a:window) != type({})
|
||||
return ''
|
||||
endif
|
||||
|
||||
" Border style
|
||||
let style = tolower(get(a:window, 'border', ''))
|
||||
if !has_key(a:window, 'border') && has_key(a:window, 'rounded')
|
||||
let style = a:window.rounded ? 'rounded' : 'sharp'
|
||||
endif
|
||||
if style == 'none' || style == 'no'
|
||||
return ''
|
||||
endif
|
||||
|
||||
" For --border styles, we need fzf 0.24.0 or above
|
||||
call fzf#exec('0.24.0')
|
||||
let opt = ' --border ' . style
|
||||
if has_key(a:window, 'highlight')
|
||||
let color = s:get_color('fg', a:window.highlight)
|
||||
if len(color)
|
||||
let opt .= ' --color=border:' . color
|
||||
endif
|
||||
endif
|
||||
return opt
|
||||
endfunction
|
||||
|
||||
function! s:split(dict)
|
||||
@@ -643,9 +819,18 @@ function! s:split(dict)
|
||||
\ 'left': ['vertical topleft', 'vertical resize', &columns],
|
||||
\ 'right': ['vertical botright', 'vertical resize', &columns] }
|
||||
let ppos = s:getpos()
|
||||
let is_popup = 0
|
||||
try
|
||||
if s:present(a:dict, 'window')
|
||||
execute 'keepalt' a:dict.window
|
||||
if type(a:dict.window) == type({})
|
||||
if !s:popup_support()
|
||||
throw 'Nvim 0.4+ or Vim 8.2.191+ with popupwin feature is required for pop-up window'
|
||||
end
|
||||
call s:popup(a:dict.window)
|
||||
let is_popup = 1
|
||||
else
|
||||
execute 'keepalt' a:dict.window
|
||||
endif
|
||||
elseif !s:splittable(a:dict)
|
||||
execute (tabpagenr()-1).'tabnew'
|
||||
else
|
||||
@@ -660,20 +845,40 @@ function! s:split(dict)
|
||||
endif
|
||||
execute cmd sz.'new'
|
||||
execute resz sz
|
||||
return [ppos, {}]
|
||||
return [ppos, {}, is_popup]
|
||||
endif
|
||||
endfor
|
||||
endif
|
||||
return [ppos, { '&l:wfw': &l:wfw, '&l:wfh': &l:wfh }]
|
||||
return [ppos, is_popup ? {} : { '&l:wfw': &l:wfw, '&l:wfh': &l:wfh }, is_popup]
|
||||
finally
|
||||
setlocal winfixwidth winfixheight
|
||||
if !is_popup
|
||||
setlocal winfixwidth winfixheight
|
||||
endif
|
||||
endtry
|
||||
endfunction
|
||||
|
||||
nnoremap <silent> <Plug>(fzf-insert) i
|
||||
nnoremap <silent> <Plug>(fzf-normal) <Nop>
|
||||
if exists(':tnoremap')
|
||||
tnoremap <silent> <Plug>(fzf-insert) <C-\><C-n>i
|
||||
tnoremap <silent> <Plug>(fzf-normal) <C-\><C-n>
|
||||
endif
|
||||
|
||||
let s:warned = 0
|
||||
function! s:handle_ambidouble(dict)
|
||||
if &ambiwidth == 'double'
|
||||
let a:dict.env = { 'RUNEWIDTH_EASTASIAN': '1' }
|
||||
elseif !s:warned && $RUNEWIDTH_EASTASIAN == '1' && &ambiwidth !=# 'double'
|
||||
call s:warn("$RUNEWIDTH_EASTASIAN is '1' but &ambiwidth is not 'double'")
|
||||
2sleep
|
||||
let s:warned = 1
|
||||
endif
|
||||
endfunction
|
||||
|
||||
function! s:execute_term(dict, command, temps) abort
|
||||
let winrest = winrestcmd()
|
||||
let pbuf = bufnr('')
|
||||
let [ppos, winopts] = s:split(a:dict)
|
||||
let [ppos, winopts, is_popup] = s:split(a:dict)
|
||||
call s:use_sh()
|
||||
let b:fzf = a:dict
|
||||
let fzf = { 'buf': bufnr(''), 'pbuf': pbuf, 'ppos': ppos, 'dict': a:dict, 'temps': a:temps,
|
||||
@@ -682,7 +887,7 @@ function! s:execute_term(dict, command, temps) abort
|
||||
function! fzf.switch_back(inplace)
|
||||
if a:inplace && bufnr('') == self.buf
|
||||
if bufexists(self.pbuf)
|
||||
execute 'keepalt b' self.pbuf
|
||||
execute 'keepalt keepjumps b' self.pbuf
|
||||
endif
|
||||
" No other listed buffer
|
||||
if bufnr('') == self.buf
|
||||
@@ -691,6 +896,7 @@ function! s:execute_term(dict, command, temps) abort
|
||||
endif
|
||||
endfunction
|
||||
function! fzf.on_exit(id, code, ...)
|
||||
silent! autocmd! fzf_popup_resize
|
||||
if s:getpos() == self.ppos " {'window': 'enew'}
|
||||
for [opt, val] in items(self.winopts)
|
||||
execute 'let' opt '=' val
|
||||
@@ -702,8 +908,8 @@ function! s:execute_term(dict, command, temps) abort
|
||||
" there's no other listed buffer (nvim +'set nobuflisted')
|
||||
close
|
||||
endif
|
||||
execute 'tabnext' self.ppos.tab
|
||||
execute self.ppos.win.'wincmd w'
|
||||
silent! execute 'tabnext' self.ppos.tab
|
||||
silent! execute self.ppos.win.'wincmd w'
|
||||
endif
|
||||
|
||||
if bufexists(self.buf)
|
||||
@@ -714,34 +920,56 @@ function! s:execute_term(dict, command, temps) abort
|
||||
execute self.winrest
|
||||
endif
|
||||
|
||||
if !s:exit_handler(a:code, self.command, 1)
|
||||
let lines = s:collect(self.temps)
|
||||
if s:exit_handler(self.dict, a:code, self.command, 1) >= 2
|
||||
return
|
||||
endif
|
||||
|
||||
call s:pushd(self.dict)
|
||||
let lines = s:collect(self.temps)
|
||||
call s:callback(self.dict, lines)
|
||||
call self.switch_back(s:getpos() == self.ppos)
|
||||
|
||||
if &buftype == 'terminal'
|
||||
call feedkeys(&filetype == 'fzf' ? "\<Plug>(fzf-insert)" : "\<Plug>(fzf-normal)")
|
||||
endif
|
||||
endfunction
|
||||
|
||||
try
|
||||
call s:pushd(a:dict)
|
||||
if s:is_win
|
||||
let fzf.temps.batchfile = s:fzf_tempname().'.bat'
|
||||
call writefile(s:wrap_cmds(a:command), fzf.temps.batchfile)
|
||||
call s:writefile(s:wrap_cmds(a:command), fzf.temps.batchfile)
|
||||
let command = fzf.temps.batchfile
|
||||
else
|
||||
let command = a:command
|
||||
endif
|
||||
let command .= s:term_marker
|
||||
if has('nvim')
|
||||
call s:handle_ambidouble(fzf)
|
||||
call termopen(command, fzf)
|
||||
else
|
||||
let fzf.buf = term_start([&shell, &shellcmdflag, command], {'curwin': 1, 'exit_cb': function(fzf.on_exit)})
|
||||
if !has('patch-8.0.1261') && !has('nvim') && !s:is_win
|
||||
let term_opts = {'exit_cb': function(fzf.on_exit)}
|
||||
if v:version >= 802
|
||||
let term_opts.term_kill = 'term'
|
||||
endif
|
||||
if is_popup
|
||||
let term_opts.hidden = 1
|
||||
else
|
||||
let term_opts.curwin = 1
|
||||
endif
|
||||
call s:handle_ambidouble(term_opts)
|
||||
keepjumps let fzf.buf = term_start([&shell, &shellcmdflag, command], term_opts)
|
||||
if is_popup && exists('#TerminalWinOpen')
|
||||
doautocmd <nomodeline> TerminalWinOpen
|
||||
endif
|
||||
if !has('patch-8.0.1261') && !s:is_win
|
||||
call term_wait(fzf.buf, 20)
|
||||
endif
|
||||
endif
|
||||
tnoremap <buffer> <c-z> <nop>
|
||||
if exists('&termwinkey') && (empty(&termwinkey) || &termwinkey =~? '<c-w>')
|
||||
tnoremap <buffer> <c-w> <c-w>.
|
||||
endif
|
||||
finally
|
||||
call s:dopopd()
|
||||
endtry
|
||||
@@ -779,6 +1007,8 @@ function! s:callback(dict, lines) abort
|
||||
endif
|
||||
if has_key(a:dict, 'sink*')
|
||||
call a:dict['sink*'](a:lines)
|
||||
elseif has_key(a:dict, 'sinklist')
|
||||
call a:dict['sinklist'](a:lines)
|
||||
endif
|
||||
catch
|
||||
if stridx(v:exception, ':E325:') < 0
|
||||
@@ -793,6 +1023,102 @@ function! s:callback(dict, lines) abort
|
||||
endif
|
||||
endfunction
|
||||
|
||||
if has('nvim')
|
||||
function! s:create_popup() abort
|
||||
let opts = s:popup_bounds()
|
||||
let opts = extend({'relative': 'editor', 'style': 'minimal'}, opts)
|
||||
|
||||
let buf = nvim_create_buf(v:false, v:true)
|
||||
let s:popup_id = nvim_open_win(buf, v:true, opts)
|
||||
call setwinvar(s:popup_id, '&colorcolumn', '')
|
||||
|
||||
" Colors
|
||||
try
|
||||
call setwinvar(s:popup_id, '&winhighlight', 'Pmenu:,Normal:Normal')
|
||||
let rules = get(g:, 'fzf_colors', {})
|
||||
if has_key(rules, 'bg')
|
||||
let color = call('s:get_color', rules.bg)
|
||||
if len(color)
|
||||
let ns = nvim_create_namespace('fzf_popup')
|
||||
let hl = nvim_set_hl(ns, 'Normal',
|
||||
\ &termguicolors ? { 'bg': color } : { 'ctermbg': str2nr(color) })
|
||||
call nvim_win_set_hl_ns(s:popup_id, ns)
|
||||
endif
|
||||
endif
|
||||
catch
|
||||
endtry
|
||||
return buf
|
||||
endfunction
|
||||
|
||||
function! s:resize_popup() abort
|
||||
if !exists('s:popup_id') || !nvim_win_is_valid(s:popup_id)
|
||||
return
|
||||
endif
|
||||
let opts = s:popup_bounds()
|
||||
let opts = extend({'relative': 'editor'}, opts)
|
||||
call nvim_win_set_config(s:popup_id, opts)
|
||||
endfunction
|
||||
else
|
||||
function! s:create_popup() abort
|
||||
function! s:popup_create(buf)
|
||||
let s:popup_id = popup_create(a:buf, #{zindex: 1000})
|
||||
call s:resize_popup()
|
||||
endfunction
|
||||
autocmd TerminalOpen * ++once call s:popup_create(str2nr(expand('<abuf>')))
|
||||
endfunction
|
||||
|
||||
function! s:resize_popup() abort
|
||||
if !exists('s:popup_id') || empty(popup_getpos(s:popup_id))
|
||||
return
|
||||
endif
|
||||
let opts = s:popup_bounds()
|
||||
call popup_move(s:popup_id, {
|
||||
\ 'line': opts.row,
|
||||
\ 'col': opts.col,
|
||||
\ 'minwidth': opts.width,
|
||||
\ 'maxwidth': opts.width,
|
||||
\ 'minheight': opts.height,
|
||||
\ 'maxheight': opts.height,
|
||||
\ })
|
||||
endfunction
|
||||
endif
|
||||
|
||||
function! s:popup_bounds() abort
|
||||
let opts = s:popup_opts
|
||||
|
||||
let xoffset = get(opts, 'xoffset', 0.5)
|
||||
let yoffset = get(opts, 'yoffset', 0.5)
|
||||
let relative = get(opts, 'relative', 0)
|
||||
|
||||
" Use current window size for positioning relatively positioned popups
|
||||
let columns = relative ? winwidth(0) : &columns
|
||||
let lines = relative ? winheight(0) : (&lines - has('nvim'))
|
||||
|
||||
" Size and position
|
||||
let width = min([max([8, opts.width > 1 ? opts.width : float2nr(columns * opts.width)]), columns])
|
||||
let height = min([max([4, opts.height > 1 ? opts.height : float2nr(lines * opts.height)]), lines])
|
||||
let row = float2nr(yoffset * (lines - height)) + (relative ? win_screenpos(0)[0] - 1 : 0)
|
||||
let col = float2nr(xoffset * (columns - width)) + (relative ? win_screenpos(0)[1] - 1 : 0)
|
||||
|
||||
" Managing the differences
|
||||
let row = min([max([0, row]), &lines - has('nvim') - height])
|
||||
let col = min([max([0, col]), &columns - width])
|
||||
let row += !has('nvim')
|
||||
let col += !has('nvim')
|
||||
|
||||
return { 'row': row, 'col': col, 'width': width, 'height': height }
|
||||
endfunction
|
||||
|
||||
function! s:popup(opts) abort
|
||||
let s:popup_opts = a:opts
|
||||
call s:create_popup()
|
||||
|
||||
augroup fzf_popup_resize
|
||||
autocmd!
|
||||
autocmd VimResized * call s:resize_popup()
|
||||
augroup END
|
||||
endfunction
|
||||
|
||||
let s:default_action = {
|
||||
\ 'ctrl-t': 'tab split',
|
||||
\ 'ctrl-x': 'split',
|
||||
@@ -809,7 +1135,7 @@ endfunction
|
||||
|
||||
function! s:cmd(bang, ...) abort
|
||||
let args = copy(a:000)
|
||||
let opts = { 'options': ['--multi'] }
|
||||
let opts = { 'options': ['--multi', '--scheme', 'path'] }
|
||||
if len(args) && isdirectory(expand(args[-1]))
|
||||
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
|
||||
if s:is_win && !&shellslash
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
__fzf_defaults() {
|
||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
builtin printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
builtin printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
# This function performs `exec awk "$@"` safely by working around awk
|
||||
# compatibility issues.
|
||||
#
|
||||
# To reduce an extra fork, this function performs "exec" so is expected to be
|
||||
# run as the last command in a subshell.
|
||||
if [[ -z ${__fzf_awk-} ]]; then
|
||||
__fzf_awk=awk
|
||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||
# Note: Solaris awk at /usr/bin/awk is meant for backward compatibility
|
||||
# with an ancient implementation of 1977 awk in the original UNIX. It
|
||||
# lacks many features of POSIX awk, so it is essentially useless in the
|
||||
# modern point of view. To use a standard-conforming version in Solaris,
|
||||
# one needs to explicitly use /usr/xpg4/bin/awk.
|
||||
__fzf_awk=/usr/xpg4/bin/awk
|
||||
elif command -v mawk > /dev/null 2>&1; then
|
||||
# choose the faster mawk if: it's installed && build date >= 20230322 &&
|
||||
# version >= 1.3.4
|
||||
local n x y z d
|
||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] &&
|
||||
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
|
||||
((d >= 20230302)) 2> /dev/null &&
|
||||
__fzf_awk=mawk
|
||||
fi
|
||||
fi
|
||||
# Note: macOS awk has a quirk that it stops processing at all when it sees
|
||||
# any data not following UTF-8 in the input stream when the current LC_CTYPE
|
||||
# specifies the UTF-8 encoding. To work around this quirk, one needs to
|
||||
# specify LC_ALL=C to change the current encoding to the plain one.
|
||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ completion-examples.nu
|
||||
#
|
||||
# Example custom completers for fzf's Nushell integration.
|
||||
#
|
||||
# To use these, add the desired entries to $env.FZF_COMPLETERS in your
|
||||
# config.nu. Each closure receives two arguments:
|
||||
# - prefix: the text before the trigger (e.g. "vim" in "vim **<TAB>")
|
||||
# - spans: the full command as a list of words (e.g. ["pacman", "-S", "vim**"])
|
||||
#
|
||||
# A closure can return either:
|
||||
# - a list of candidate strings (fzf will use default options), or
|
||||
# - a record with the following optional fields:
|
||||
# candidates: list<string> # candidates to feed to fzf
|
||||
# opts: list<string> # custom fzf options (default: ["-m"])
|
||||
# post: closure (|sel| ...) # post-processing of the selected item
|
||||
#
|
||||
# Simple example:
|
||||
# $env.FZF_COMPLETERS = {
|
||||
# git: {|prefix, spans| ["branch-main", "branch-dev", "branch-feature"]}
|
||||
# }
|
||||
|
||||
# --- pacman / paru ---
|
||||
# Completes package names for pacman and paru.
|
||||
# Uses the spans to distinguish between subcommands:
|
||||
# -S (sync), -F (files): list available packages from repos
|
||||
# -Q (query), -R (remove): list installed packages
|
||||
# Returns a record with custom fzf options for package preview.
|
||||
|
||||
$env.FZF_COMPLETERS = {}
|
||||
|
||||
$env.FZF_COMPLETERS.pacman = {|prefix, spans|
|
||||
let sub = $spans | skip 1 | first
|
||||
let candidates = (if ($sub =~ "-[SF]") {
|
||||
^pacman -Slq | lines
|
||||
} else if ($sub =~ "-[QR]") {
|
||||
^pacman -Qq | lines
|
||||
} else {
|
||||
[]
|
||||
})
|
||||
{
|
||||
candidates: $candidates
|
||||
opts: ["-m", "--preview", "pacman -Si {}", "--prompt", "Package > "]
|
||||
}
|
||||
}
|
||||
|
||||
$env.FZF_COMPLETERS.paru = {|prefix, spans|
|
||||
let sub = $spans | skip 1 | first
|
||||
let candidates = (if ($sub =~ "-[SF]") {
|
||||
^pacman -Slq | lines
|
||||
} else if ($sub =~ "-[QR]") {
|
||||
^pacman -Qq | lines
|
||||
} else {
|
||||
[]
|
||||
})
|
||||
{
|
||||
candidates: $candidates
|
||||
opts: ["-m", "--preview", "pacman -Si {}", "--prompt", "Package > "]
|
||||
}
|
||||
}
|
||||
|
||||
# --- pass (password-store) ---
|
||||
# Completes entry names from ~/.password-store.
|
||||
# Returns a simple list (no custom fzf options needed).
|
||||
|
||||
$env.FZF_COMPLETERS.pass = {|prefix, spans|
|
||||
try {
|
||||
ls ~/.password-store/**/*.gpg
|
||||
| get name
|
||||
| each {$in | str replace -r '^.*?\.password-store/(.*).gpg' '${1}'}
|
||||
} catch {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
# --- Example with post-processing hook ---
|
||||
# The "post" closure transforms the selected line after fzf returns.
|
||||
# This is useful when the displayed line contains more information than
|
||||
# what you want inserted on the command line (e.g. extracting a PID from
|
||||
# a full "ps" output line).
|
||||
#
|
||||
# $env.FZF_COMPLETERS.mycommand = {|prefix, spans|
|
||||
# {
|
||||
# candidates: (^some-command | lines)
|
||||
# opts: ["+m", "--header-lines=1"]
|
||||
# post: {|selection| $selection | split row ' ' | get 0}
|
||||
# }
|
||||
# }
|
||||
+573
-186
@@ -2,111 +2,311 @@
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/-completion.bash
|
||||
# /_/ /___/_/ completion.bash
|
||||
#
|
||||
# - $FZF_TMUX (default: 0)
|
||||
# - $FZF_TMUX_HEIGHT (default: '40%')
|
||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
|
||||
# - $FZF_COMPLETION_DIR_OPTS (default: empty)
|
||||
|
||||
if [[ $- =~ i ]]; then
|
||||
|
||||
# To use custom commands instead of find, override _fzf_compgen_{path,dir}
|
||||
if ! declare -f _fzf_compgen_path > /dev/null; then
|
||||
_fzf_compgen_path() {
|
||||
echo "$1"
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
|
||||
if ! declare -f _fzf_compgen_dir > /dev/null; then
|
||||
_fzf_compgen_dir() {
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
# To use custom commands instead of find, override _fzf_compgen_{path,dir}
|
||||
#
|
||||
# _fzf_compgen_path() {
|
||||
# echo "$1"
|
||||
# command find -L "$1" \
|
||||
# -name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
|
||||
# -a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@'
|
||||
# }
|
||||
#
|
||||
# _fzf_compgen_dir() {
|
||||
# command find -L "$1" \
|
||||
# -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
|
||||
# -a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@'
|
||||
# }
|
||||
|
||||
###########################################################
|
||||
|
||||
# To redraw line after fzf closes (printf '\e[5n')
|
||||
bind '"\e[0n": redraw-current-line'
|
||||
#----BEGIN shfmt
|
||||
#----BEGIN INCLUDE common.sh
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
|
||||
__fzfcmd_complete() {
|
||||
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] &&
|
||||
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
|
||||
__fzf_defaults() {
|
||||
builtin printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
builtin printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_orig_completion_filter() {
|
||||
sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2"; [[ "\1" = *" -o nospace "* ]] \&\& [[ ! "$__fzf_nospace_commands" = *" \3 "* ]] \&\& __fzf_nospace_commands="$__fzf_nospace_commands \3 ";/' |
|
||||
awk -F= '{OFS = FS} {gsub(/[^A-Za-z0-9_= ;]/, "_", $1);}1'
|
||||
__fzf_exec_awk() {
|
||||
if [[ -z ${__fzf_awk-} ]]; then
|
||||
__fzf_awk=awk
|
||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||
__fzf_awk=/usr/xpg4/bin/awk
|
||||
elif command -v mawk > /dev/null 2>&1; then
|
||||
local n x y z d
|
||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] &&
|
||||
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
|
||||
((d >= 20230302)) 2> /dev/null &&
|
||||
__fzf_awk=mawk
|
||||
fi
|
||||
fi
|
||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||
}
|
||||
#----END INCLUDE
|
||||
|
||||
__fzf_comprun() {
|
||||
if [[ "$(type -t _fzf_comprun 2>&1)" == function ]]; then
|
||||
_fzf_comprun "$@"
|
||||
elif [[ -n ${TMUX_PANE-} ]] && { [[ ${FZF_TMUX:-0} != 0 ]] || [[ -n ${FZF_TMUX_OPTS-} ]]; }; then
|
||||
shift
|
||||
fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- "$@"
|
||||
else
|
||||
shift
|
||||
fzf "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
__fzf_orig_completion() {
|
||||
local l comp f cmd
|
||||
while read -r l; do
|
||||
if [[ $l =~ ^(.*\ -F)\ *([^ ]*).*\ ([^ ]*)$ ]]; then
|
||||
comp="${BASH_REMATCH[1]}"
|
||||
f="${BASH_REMATCH[2]}"
|
||||
cmd="${BASH_REMATCH[3]}"
|
||||
[[ $f == _fzf_* ]] && continue
|
||||
builtin printf -v "_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" "%s" "${comp} %s ${cmd} #${f}"
|
||||
if [[ $l == *" -o nospace "* ]] && [[ ${__fzf_nospace_commands-} != *" $cmd "* ]]; then
|
||||
__fzf_nospace_commands="${__fzf_nospace_commands-} $cmd "
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# @param $1 cmd - Command name for which the original completion is searched
|
||||
# @var[out] REPLY - Original function name is returned
|
||||
__fzf_orig_completion_get_orig_func() {
|
||||
local cmd orig_var orig
|
||||
cmd=$1
|
||||
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
|
||||
orig="${!orig_var-}"
|
||||
REPLY="${orig##*#}"
|
||||
[[ $REPLY ]] && type "$REPLY" &> /dev/null
|
||||
}
|
||||
|
||||
# @param $1 cmd - Command name for which the original completion is searched
|
||||
# @param $2 func - Fzf's completion function to replace the original function
|
||||
# @var[out] REPLY - Completion setting is returned as a string to "eval"
|
||||
__fzf_orig_completion_instantiate() {
|
||||
local cmd func orig_var orig
|
||||
cmd=$1
|
||||
func=$2
|
||||
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
|
||||
orig="${!orig_var-}"
|
||||
orig="${orig%#*}"
|
||||
[[ $orig == *' %s '* ]] || return 1
|
||||
builtin printf -v REPLY "$orig" "$func"
|
||||
}
|
||||
|
||||
_fzf_opts_completion() {
|
||||
local cur prev opts
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD - 1]}"
|
||||
opts="
|
||||
-x --extended
|
||||
-e --exact
|
||||
--algo
|
||||
-i +i
|
||||
-n --nth
|
||||
--with-nth
|
||||
-d --delimiter
|
||||
+c --no-color
|
||||
+i --no-ignore-case
|
||||
+s --no-sort
|
||||
--tac
|
||||
--tiebreak
|
||||
-m --multi
|
||||
--no-mouse
|
||||
--bind
|
||||
--cycle
|
||||
--no-hscroll
|
||||
--jump-labels
|
||||
--height
|
||||
--literal
|
||||
--reverse
|
||||
--margin
|
||||
--inline-info
|
||||
--prompt
|
||||
--header
|
||||
--header-lines
|
||||
+x --no-extended
|
||||
--accept-nth
|
||||
--ansi
|
||||
--tabstop
|
||||
--bash
|
||||
--bind
|
||||
--border
|
||||
--border-label
|
||||
--border-label-pos
|
||||
--color
|
||||
--no-bold
|
||||
--cycle
|
||||
--disabled
|
||||
--ellipsis
|
||||
--expect
|
||||
--filepath-word
|
||||
--fish
|
||||
--footer
|
||||
--footer-border
|
||||
--footer-label
|
||||
--footer-label-pos
|
||||
--freeze-left
|
||||
--freeze-right
|
||||
--gap
|
||||
--gap-line
|
||||
--ghost
|
||||
--gutter
|
||||
--gutter-raw
|
||||
--header
|
||||
--header-border
|
||||
--header-first
|
||||
--header-label
|
||||
--header-label-pos
|
||||
--header-lines
|
||||
--header-lines-border
|
||||
--height
|
||||
--highlight-line
|
||||
--history
|
||||
--history-size
|
||||
--hscroll-off
|
||||
--id-nth
|
||||
--info
|
||||
--info-command
|
||||
--input-border
|
||||
--input-label
|
||||
--input-label-pos
|
||||
--jump-labels
|
||||
--keep-right
|
||||
--layout
|
||||
--listen
|
||||
--listen-unsafe
|
||||
--list-border
|
||||
--list-label
|
||||
--list-label-pos
|
||||
--literal
|
||||
--man
|
||||
--margin
|
||||
--marker
|
||||
--marker-multi-line
|
||||
--min-height
|
||||
--no-bold
|
||||
--no-hscroll
|
||||
--no-input
|
||||
--no-multi-line
|
||||
--no-scrollbar
|
||||
--no-separator
|
||||
--padding
|
||||
--pointer
|
||||
--preview
|
||||
--preview-border
|
||||
--preview-label
|
||||
--preview-label-pos
|
||||
--preview-window
|
||||
-q --query
|
||||
-1 --select-1
|
||||
-0 --exit-0
|
||||
-f --filter
|
||||
--print-query
|
||||
--expect
|
||||
--sync"
|
||||
--print0
|
||||
--prompt
|
||||
--raw
|
||||
--read0
|
||||
--scheme
|
||||
--scroll-off
|
||||
--scrollbar
|
||||
--separator
|
||||
--smart-case
|
||||
--style
|
||||
--sync
|
||||
--tabstop
|
||||
--tac
|
||||
--tail
|
||||
--tiebreak
|
||||
--tmux
|
||||
--track
|
||||
--version
|
||||
--walker
|
||||
--walker-root
|
||||
--walker-skip
|
||||
--with-nth
|
||||
--with-shell
|
||||
--wrap
|
||||
--wrap-sign
|
||||
--preview-wrap-sign
|
||||
--zsh
|
||||
-0 --exit-0
|
||||
-1 --select-1
|
||||
-d --delimiter
|
||||
-e --exact
|
||||
-f --filter
|
||||
-h --help
|
||||
-i --ignore-case
|
||||
-m --multi
|
||||
-n --nth
|
||||
-q --query
|
||||
--"
|
||||
|
||||
case "${prev}" in
|
||||
--tiebreak)
|
||||
COMPREPLY=( $(compgen -W "length begin end index" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--color)
|
||||
COMPREPLY=( $(compgen -W "dark light 16 bw" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--history)
|
||||
COMPREPLY=()
|
||||
return 0
|
||||
;;
|
||||
--scheme)
|
||||
COMPREPLY=($(compgen -W "default path history" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--tiebreak)
|
||||
COMPREPLY=($(compgen -W "length chunk pathname begin end index" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--color)
|
||||
COMPREPLY=($(compgen -W "dark light base16 16 bw no" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--layout)
|
||||
COMPREPLY=($(compgen -W "default reverse reverse-list" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--info)
|
||||
COMPREPLY=($(compgen -W "default right hidden inline inline-right" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--wrap)
|
||||
COMPREPLY=($(compgen -W "char word" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--style)
|
||||
COMPREPLY=($(compgen -W "default minimal full" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--preview-window)
|
||||
COMPREPLY=($(compgen -W "
|
||||
default
|
||||
hidden
|
||||
nohidden
|
||||
wrap
|
||||
wrap-word
|
||||
nowrap
|
||||
cycle
|
||||
nocycle
|
||||
up top
|
||||
down bottom
|
||||
left
|
||||
right
|
||||
rounded border border-rounded
|
||||
border-line
|
||||
sharp border-sharp
|
||||
border-bold
|
||||
border-block
|
||||
border-thinblock
|
||||
border-double
|
||||
noborder border-none
|
||||
border-horizontal
|
||||
border-vertical
|
||||
border-up border-top
|
||||
border-down border-bottom
|
||||
border-left
|
||||
border-right
|
||||
follow
|
||||
nofollow
|
||||
info
|
||||
noinfo" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--border | --list-border | --header-border | --header-lines-border | --footer-border | --input-border | --preview-border)
|
||||
COMPREPLY=($(compgen -W "line rounded sharp bold block thinblock double horizontal vertical top bottom left right none" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--border-label-pos | --preview-label-pos | --list-label-pos | --header-label-pos | --footer-label-pos | --input-label-pos)
|
||||
COMPREPLY=($(compgen -W "center bottom top" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$cur" =~ ^-|\+ ]]; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "$cur") )
|
||||
if [[ $cur =~ ^-|\+ ]]; then
|
||||
COMPREPLY=($(compgen -W "${opts}" -- "$cur"))
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -114,64 +314,93 @@ _fzf_opts_completion() {
|
||||
}
|
||||
|
||||
_fzf_handle_dynamic_completion() {
|
||||
local cmd orig_var orig ret orig_cmd orig_complete
|
||||
local cmd ret REPLY orig_cmd orig_complete
|
||||
cmd="$1"
|
||||
shift
|
||||
orig_cmd="$1"
|
||||
orig_var="_fzf_orig_completion_$cmd"
|
||||
orig="${!orig_var##*#}"
|
||||
if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then
|
||||
$orig "$@"
|
||||
elif [ -n "$_fzf_completion_loader" ]; then
|
||||
if __fzf_orig_completion_get_orig_func "$cmd"; then
|
||||
"$REPLY" "$@"
|
||||
elif [[ -n ${_fzf_completion_loader-} ]]; then
|
||||
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
|
||||
_completion_loader "$@"
|
||||
$_fzf_completion_loader "$@"
|
||||
ret=$?
|
||||
# _completion_loader may not have updated completion for the command
|
||||
if [ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]; then
|
||||
eval "$(complete | command grep " -F.* $orig_cmd$" | __fzf_orig_completion_filter)"
|
||||
if [[ "$__fzf_nospace_commands" = *" $orig_cmd "* ]]; then
|
||||
if [[ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]]; then
|
||||
__fzf_orig_completion < <(complete -p "$orig_cmd" 2> /dev/null)
|
||||
__fzf_orig_completion_get_orig_func "$cmd" || ret=1
|
||||
|
||||
# Update orig_complete by _fzf_orig_completion entry
|
||||
[[ $orig_complete =~ ' -F '(_fzf_[^ ]+)' ' ]] &&
|
||||
__fzf_orig_completion_instantiate "$cmd" "${BASH_REMATCH[1]}" &&
|
||||
orig_complete=$REPLY
|
||||
|
||||
if [[ ${__fzf_nospace_commands-} == *" $orig_cmd "* ]]; then
|
||||
eval "${orig_complete/ -F / -o nospace -F }"
|
||||
else
|
||||
eval "$orig_complete"
|
||||
fi
|
||||
fi
|
||||
[[ $ret -eq 0 ]] && return 124
|
||||
return $ret
|
||||
fi
|
||||
}
|
||||
|
||||
__fzf_generic_path_completion() {
|
||||
local cur base dir leftover matches trigger cmd fzf
|
||||
fzf="$(__fzfcmd_complete)"
|
||||
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
|
||||
local cur base dir leftover matches trigger cmd
|
||||
cmd="${COMP_WORDS[0]}"
|
||||
if [[ $cmd == \\* ]]; then
|
||||
cmd="${cmd:1}"
|
||||
fi
|
||||
COMPREPLY=()
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == *"$trigger" ]]; then
|
||||
[[ $COMP_CWORD -ge 0 ]] && cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ $cur == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||
base=${cur:0:${#cur}-${#trigger}}
|
||||
eval "base=$base"
|
||||
eval "base=$base" 2> /dev/null || return
|
||||
|
||||
[[ $base = *"/"* ]] && dir="$base"
|
||||
dir=
|
||||
[[ $base == *"/"* ]] && dir="$base"
|
||||
while true; do
|
||||
if [ -z "$dir" ] || [ -d "$dir" ]; then
|
||||
leftover=${base/#"$dir"}
|
||||
leftover=${leftover/#\/}
|
||||
[ -z "$dir" ] && dir='.'
|
||||
[ "$dir" != "/" ] && dir="${dir/%\//}"
|
||||
matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $2 -q "$leftover" | while read -r item; do
|
||||
printf "%q$3 " "$item"
|
||||
done)
|
||||
if [[ -z $dir ]] || [[ -d $dir ]]; then
|
||||
leftover=${base/#"$dir"/}
|
||||
leftover=${leftover/#\//}
|
||||
[[ -z $dir ]] && dir='.'
|
||||
[[ $dir != "/" ]] && dir="${dir/%\//}"
|
||||
matches=$(
|
||||
export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-} $2")
|
||||
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
|
||||
if [[ $1 =~ dir ]]; then
|
||||
eval "rest=(${FZF_COMPLETION_DIR_OPTS-})"
|
||||
else
|
||||
eval "rest=(${FZF_COMPLETION_PATH_OPTS-})"
|
||||
fi
|
||||
if declare -F "$1" > /dev/null; then
|
||||
eval "$1 $(builtin printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover" "${rest[@]}"
|
||||
else
|
||||
if [[ $1 =~ dir ]]; then
|
||||
walker=dir,follow
|
||||
else
|
||||
walker=file,dir,follow,hidden
|
||||
fi
|
||||
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" "${rest[@]}"
|
||||
fi | while read -r item; do
|
||||
builtin printf "%q " "${item%$3}$3"
|
||||
done
|
||||
)
|
||||
matches=${matches% }
|
||||
[[ -z "$3" ]] && [[ "$__fzf_nospace_commands" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
|
||||
if [ -n "$matches" ]; then
|
||||
COMPREPLY=( "$matches" )
|
||||
[[ -z $3 ]] && [[ ${__fzf_nospace_commands-} == *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
|
||||
if [[ -n $matches ]]; then
|
||||
COMPREPLY=("$matches")
|
||||
else
|
||||
COMPREPLY=( "$cur" )
|
||||
COMPREPLY=("$cur")
|
||||
fi
|
||||
printf '\e[5n'
|
||||
# To redraw line after fzf closes (builtin printf '\e[5n')
|
||||
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
||||
builtin printf '\e[5n'
|
||||
return 0
|
||||
fi
|
||||
dir=$(dirname "$dir")
|
||||
[[ "$dir" =~ /$ ]] || dir="$dir"/
|
||||
dir=$(command dirname "$dir")
|
||||
[[ $dir =~ /$ ]] || dir="$dir"/
|
||||
done
|
||||
else
|
||||
shift
|
||||
@@ -182,29 +411,53 @@ __fzf_generic_path_completion() {
|
||||
}
|
||||
|
||||
_fzf_complete() {
|
||||
local cur selected trigger cmd fzf post
|
||||
post="$(caller 0 | awk '{print $2}')_post"
|
||||
type -t "$post" > /dev/null 2>&1 || post=cat
|
||||
fzf="$(__fzfcmd_complete)"
|
||||
# Split arguments around --
|
||||
local args rest str_arg i sep
|
||||
args=("$@")
|
||||
sep=
|
||||
for i in "${!args[@]}"; do
|
||||
if [[ ${args[$i]} == -- ]]; then
|
||||
sep=$i
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ -n $sep ]]; then
|
||||
str_arg=
|
||||
rest=("${args[@]:$((sep + 1)):${#args[@]}}")
|
||||
args=("${args[@]:0:sep}")
|
||||
else
|
||||
str_arg=$1
|
||||
args=()
|
||||
shift
|
||||
rest=("$@")
|
||||
fi
|
||||
|
||||
local cur selected trigger cmd post
|
||||
post="$(caller 0 | __fzf_exec_awk '{print $2}')_post"
|
||||
type -t "$post" > /dev/null 2>&1 || post='command cat'
|
||||
|
||||
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
cmd="${COMP_WORDS[0]}"
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == *"$trigger" ]]; then
|
||||
if [[ $cur == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||
cur=${cur:0:${#cur}-${#trigger}}
|
||||
|
||||
selected=$(cat | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $1 -q "$cur" | $post | tr '\n' ' ')
|
||||
selected=$(
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
|
||||
FZF_DEFAULT_OPTS_FILE='' \
|
||||
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | eval "$post" | command tr '\n' ' '
|
||||
)
|
||||
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
|
||||
if [ -n "$selected" ]; then
|
||||
if [[ -n $selected ]]; then
|
||||
COMPREPLY=("$selected")
|
||||
else
|
||||
COMPREPLY=("$cur")
|
||||
fi
|
||||
printf '\e[5n'
|
||||
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
||||
builtin printf '\e[5n'
|
||||
return 0
|
||||
else
|
||||
shift
|
||||
_fzf_handle_dynamic_completion "$cmd" "$@"
|
||||
_fzf_handle_dynamic_completion "$cmd" "${rest[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -222,89 +475,218 @@ _fzf_dir_completion() {
|
||||
}
|
||||
|
||||
_fzf_complete_kill() {
|
||||
[ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1
|
||||
|
||||
local selected fzf
|
||||
fzf="$(__fzfcmd_complete)"
|
||||
selected=$(command ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ')
|
||||
printf '\e[5n'
|
||||
|
||||
if [ -n "$selected" ]; then
|
||||
COMPREPLY=( "$selected" )
|
||||
return 0
|
||||
fi
|
||||
_fzf_proc_completion "$@"
|
||||
}
|
||||
|
||||
_fzf_complete_telnet() {
|
||||
_fzf_complete '+m' "$@" < <(
|
||||
command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' |
|
||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
||||
)
|
||||
_fzf_proc_completion() {
|
||||
local transformer
|
||||
transformer='
|
||||
if [[ $FZF_KEY =~ ctrl|alt|shift ]] && [[ -n $FZF_NTH ]]; then
|
||||
nths=( ${FZF_NTH//,/ } )
|
||||
new_nths=()
|
||||
found=0
|
||||
for nth in ${nths[@]}; do
|
||||
if [[ $nth = $FZF_CLICK_HEADER_NTH ]]; then
|
||||
found=1
|
||||
else
|
||||
new_nths+=($nth)
|
||||
fi
|
||||
done
|
||||
[[ $found = 0 ]] && new_nths+=($FZF_CLICK_HEADER_NTH)
|
||||
new_nths=${new_nths[*]}
|
||||
new_nths=${new_nths// /,}
|
||||
echo "change-nth($new_nths)+change-prompt($new_nths> )"
|
||||
else
|
||||
if [[ $FZF_NTH = $FZF_CLICK_HEADER_NTH ]]; then
|
||||
echo "change-nth()+change-prompt(> )"
|
||||
else
|
||||
echo "change-nth($FZF_CLICK_HEADER_NTH)+change-prompt($FZF_CLICK_HEADER_WORD> )"
|
||||
fi
|
||||
fi
|
||||
'
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap --color fg:dim,nth:regular \
|
||||
--bind "click-header:transform:$transformer" -- "$@" < <(
|
||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
||||
command ps --everyone --full --windows # For cygwin
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_proc_completion_post() {
|
||||
__fzf_exec_awk '{print $2}'
|
||||
}
|
||||
|
||||
# To use custom hostname lists, override __fzf_list_hosts.
|
||||
# The function is expected to print hostnames, one per line as well as in the
|
||||
# desired sorting and with any duplicates removed, to standard output.
|
||||
#
|
||||
# e.g.
|
||||
# # Use bash-completions’s _known_hosts_real() for getting the list of hosts
|
||||
# __fzf_list_hosts() {
|
||||
# # Set the local attribute for any non-local variable that is set by _known_hosts_real()
|
||||
# local COMPREPLY=()
|
||||
# _known_hosts_real ''
|
||||
# builtin printf '%s\n' "${COMPREPLY[@]}" | command sort -u --version-sort
|
||||
# }
|
||||
if ! declare -F __fzf_list_hosts > /dev/null; then
|
||||
__fzf_list_hosts() {
|
||||
command sort -u \
|
||||
<(
|
||||
# Note: To make the pathname expansion of "~/.ssh/config.d/*" work
|
||||
# properly, we need to adjust the related shell options. We need to
|
||||
# unset "set -f" and "GLOBIGNORE", which disable the pathname expansion
|
||||
# totally or partially. We need to unset "dotglob" and "nocaseglob" to
|
||||
# avoid matching unwanted files. We need to unset "failglob" to avoid
|
||||
# outputting the error messages to the terminal when no matching is
|
||||
# found. We need to set "nullglob" to avoid attempting to read the
|
||||
# literal filename '~/.ssh/config.d/*' when no matching is found.
|
||||
set +f
|
||||
GLOBIGNORE=
|
||||
shopt -u dotglob nocaseglob failglob
|
||||
shopt -s nullglob
|
||||
|
||||
__fzf_exec_awk '
|
||||
# Note: mawk <= 1.3.3-20090705 does not support the POSIX brackets of
|
||||
# the form [[:blank:]], and Ubuntu 18.04 LTS still uses this
|
||||
# 16-year-old mawk unfortunately. We need to use [ \t] instead.
|
||||
match(tolower($0), /^[ \t]*host(name)?[ \t]*[ \t=]/) {
|
||||
$0 = substr($0, RLENGTH + 1) # Remove "Host(name)?=?"
|
||||
sub(/#.*/, "")
|
||||
for (i = 1; i <= NF; i++)
|
||||
if ($i !~ /[*?%]/)
|
||||
print $i
|
||||
}
|
||||
' ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null
|
||||
) \
|
||||
<(
|
||||
__fzf_exec_awk -F ',' '
|
||||
match($0, /^[][a-zA-Z0-9.,:-]+/) {
|
||||
$0 = substr($0, 1, RLENGTH)
|
||||
gsub(/[][]|:[^,]*/, "")
|
||||
for (i = 1; i <= NF; i++)
|
||||
print $i
|
||||
}
|
||||
' ~/.ssh/known_hosts 2> /dev/null
|
||||
) \
|
||||
<(
|
||||
__fzf_exec_awk '
|
||||
{
|
||||
sub(/#.*/, "")
|
||||
for (i = 2; i <= NF; i++)
|
||||
if ($i != "0.0.0.0")
|
||||
print $i
|
||||
}
|
||||
' /etc/hosts 2> /dev/null
|
||||
)
|
||||
}
|
||||
fi
|
||||
|
||||
_fzf_host_completion() {
|
||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts)
|
||||
}
|
||||
|
||||
# Values for $1 $2 $3 are described here
|
||||
# https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html
|
||||
# > the first argument ($1) is the name of the command whose arguments are being completed,
|
||||
# > the second argument ($2) is the word being completed,
|
||||
# > and the third argument ($3) is the word preceding the word being completed on the current command line.
|
||||
_fzf_complete_ssh() {
|
||||
_fzf_complete '+m' "$@" < <(
|
||||
cat <(cat ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \
|
||||
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
|
||||
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
|
||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
||||
case $3 in
|
||||
-i | -F | -E)
|
||||
_fzf_path_completion "$@"
|
||||
;;
|
||||
*)
|
||||
local user=
|
||||
[[ $2 =~ '@' ]] && user="${2%%@*}@"
|
||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | __fzf_exec_awk -v user="$user" '{print user $0}')
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_fzf_var_completion() {
|
||||
_fzf_complete -m -- "$@" < <(
|
||||
declare -xp | command sed -En 's|^declare [^ ]+ ([^=]+).*|\1|p'
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_complete_unset() {
|
||||
_fzf_complete '-m' "$@" < <(
|
||||
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_complete_export() {
|
||||
_fzf_complete '-m' "$@" < <(
|
||||
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_complete_unalias() {
|
||||
_fzf_complete '-m' "$@" < <(
|
||||
alias | sed 's/=.*//' | sed 's/.* //'
|
||||
_fzf_alias_completion() {
|
||||
_fzf_complete -m -- "$@" < <(
|
||||
alias | command sed -En 's|^alias ([^=]+).*|\1|p'
|
||||
)
|
||||
}
|
||||
|
||||
# fzf options
|
||||
complete -o default -F _fzf_opts_completion fzf
|
||||
# fzf-tmux is a thin fzf wrapper that has only a few more options than fzf
|
||||
# itself. As a quick improvement we take fzf's completion. Adding the few extra
|
||||
# fzf-tmux specific options (like `-w WIDTH`) are left as a future patch.
|
||||
complete -o default -F _fzf_opts_completion fzf-tmux
|
||||
|
||||
d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
|
||||
a_cmds="
|
||||
awk cat diff diff3
|
||||
emacs emacsclient ex file ftp g++ gcc gvim head hg java
|
||||
# Default path completion
|
||||
__fzf_default_completion() {
|
||||
__fzf_generic_path_completion _fzf_compgen_path "-m" "" "$@"
|
||||
|
||||
# Dynamic completion loader has updated the completion for the command
|
||||
if [[ $? -eq 124 ]]; then
|
||||
# We trigger _fzf_setup_completion so that fuzzy completion for the command
|
||||
# still works. However, loader can update the completion for multiple
|
||||
# commands at once, and fuzzy completion will no longer work for those
|
||||
# other commands. e.g. pytest -> py.test, pytest-2, pytest-3, etc
|
||||
_fzf_setup_completion path "$1"
|
||||
return 124
|
||||
fi
|
||||
}
|
||||
|
||||
# Set fuzzy path completion as the default completion for all commands.
|
||||
# We can't set up default completion,
|
||||
# 1. if it's already set up by another script
|
||||
# 2. or if the current version of bash doesn't support -D option
|
||||
complete | command grep -q __fzf_default_completion ||
|
||||
complete | command grep -- '-D$' | command grep -qv _comp_complete_load ||
|
||||
complete -D -F __fzf_default_completion -o default -o bashdefault 2> /dev/null
|
||||
|
||||
d_cmds="${FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir}"
|
||||
|
||||
# NOTE: $FZF_COMPLETION_PATH_COMMANDS and $FZF_COMPLETION_VAR_COMMANDS are
|
||||
# undocumented and subject to change in the future.
|
||||
#
|
||||
# NOTE: Although we have default completion, we still need to set up completion
|
||||
# for each command in case they already have completion set up by another script.
|
||||
a_cmds="${FZF_COMPLETION_PATH_COMMANDS-"
|
||||
awk bat cat code diff diff3
|
||||
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java
|
||||
javac ld less more mvim nvim patch perl python ruby
|
||||
sed sftp sort source tail tee uniq vi view vim wc xdg-open
|
||||
basename bunzip2 bzip2 chmod chown curl cp dirname du
|
||||
find git grep gunzip gzip hg jar
|
||||
ln ls mv open rm rsync scp
|
||||
svn tar unzip zip"
|
||||
x_cmds="kill ssh telnet unset unalias export"
|
||||
svn tar unzip zip"}"
|
||||
v_cmds="${FZF_COMPLETION_VAR_COMMANDS-export unset printenv}"
|
||||
|
||||
# Preserve existing completion
|
||||
eval "$(complete |
|
||||
sed -E '/-F/!d; / _fzf/d; '"/ ($(echo $d_cmds $a_cmds $x_cmds | sed 's/ /|/g; s/+/\\+/g'))$/"'!d' |
|
||||
__fzf_orig_completion_filter)"
|
||||
__fzf_orig_completion < <(complete -p $d_cmds $a_cmds $v_cmds unalias kill ssh 2> /dev/null)
|
||||
|
||||
if type _completion_loader > /dev/null 2>&1; then
|
||||
_fzf_completion_loader=1
|
||||
if type _comp_load > /dev/null 2>&1; then
|
||||
# _comp_load was added in bash-completion 2.12 to replace _completion_loader.
|
||||
# We use it without -D option so that it does not use _comp_complete_minimal as the fallback.
|
||||
_fzf_completion_loader=_comp_load
|
||||
elif type __load_completion > /dev/null 2>&1; then
|
||||
# In bash-completion 2.11, _completion_loader internally calls __load_completion
|
||||
# and if it returns a non-zero status, it sets the default 'minimal' completion.
|
||||
_fzf_completion_loader=__load_completion
|
||||
elif type _completion_loader > /dev/null 2>&1; then
|
||||
_fzf_completion_loader=_completion_loader
|
||||
fi
|
||||
|
||||
__fzf_defc() {
|
||||
local cmd func opts orig_var orig def
|
||||
local cmd func opts REPLY
|
||||
cmd="$1"
|
||||
func="$2"
|
||||
opts="$3"
|
||||
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
|
||||
orig="${!orig_var}"
|
||||
if [ -n "$orig" ]; then
|
||||
printf -v def "$orig" "$func"
|
||||
eval "$def"
|
||||
if __fzf_orig_completion_instantiate "$cmd" "$func"; then
|
||||
eval "$REPLY"
|
||||
else
|
||||
complete -F "$func" $opts "$cmd"
|
||||
eval "complete -F \"$func\" $opts \"$cmd\""
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -315,39 +697,44 @@ done
|
||||
|
||||
# Directory
|
||||
for cmd in $d_cmds; do
|
||||
__fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames"
|
||||
__fzf_defc "$cmd" _fzf_dir_completion "-o bashdefault -o nospace -o dirnames"
|
||||
done
|
||||
|
||||
# Kill completion
|
||||
complete -F _fzf_complete_kill -o nospace -o default -o bashdefault kill
|
||||
# Variables
|
||||
for cmd in $v_cmds; do
|
||||
__fzf_defc "$cmd" _fzf_var_completion "-o default -o nospace -v"
|
||||
done
|
||||
|
||||
# Host completion
|
||||
complete -F _fzf_complete_ssh -o default -o bashdefault ssh
|
||||
complete -F _fzf_complete_telnet -o default -o bashdefault telnet
|
||||
# Aliases
|
||||
__fzf_defc unalias _fzf_alias_completion "-a"
|
||||
|
||||
# Environment variables / Aliases
|
||||
complete -F _fzf_complete_unset -o default -o bashdefault unset
|
||||
complete -F _fzf_complete_export -o default -o bashdefault export
|
||||
complete -F _fzf_complete_unalias -o default -o bashdefault unalias
|
||||
# Processes
|
||||
__fzf_defc kill _fzf_proc_completion "-o default -o bashdefault"
|
||||
|
||||
unset cmd d_cmds a_cmds x_cmds
|
||||
# ssh
|
||||
__fzf_defc ssh _fzf_complete_ssh "-o default -o bashdefault"
|
||||
|
||||
unset cmd d_cmds a_cmds v_cmds
|
||||
|
||||
_fzf_setup_completion() {
|
||||
local kind fn cmd
|
||||
kind=$1
|
||||
fn=_fzf_${1}_completion
|
||||
if [[ $# -lt 2 ]] || ! type -t "$fn" > /dev/null; then
|
||||
echo "usage: ${FUNCNAME[0]} path|dir COMMANDS..."
|
||||
echo "usage: ${FUNCNAME[0]} path|dir|var|alias|host|proc COMMANDS..."
|
||||
return 1
|
||||
fi
|
||||
shift
|
||||
__fzf_orig_completion < <(complete -p "$@" 2> /dev/null)
|
||||
for cmd in "$@"; do
|
||||
eval "$(complete -p "$cmd" 2> /dev/null | grep -v "$fn" | __fzf_orig_completion_filter)"
|
||||
case "$kind" in
|
||||
dir) __fzf_defc "$cmd" "$fn" "-o nospace -o dirnames" ;;
|
||||
*) __fzf_defc "$cmd" "$fn" "-o default -o bashdefault" ;;
|
||||
var) __fzf_defc "$cmd" "$fn" "-o default -o nospace -v" ;;
|
||||
alias) __fzf_defc "$cmd" "$fn" "-a" ;;
|
||||
*) __fzf_defc "$cmd" "$fn" "-o default -o bashdefault" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
#----END shfmt
|
||||
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ completion.fish
|
||||
#
|
||||
# - $FZF_COMPLETION_OPTS
|
||||
# - $FZF_EXPANSION_OPTS
|
||||
|
||||
# The oldest supported fish version is 3.4.0. For this message being able to be
|
||||
# displayed on older versions, the command substitution syntax $() should not
|
||||
# be used anywhere in the script, otherwise the source command will fail.
|
||||
if string match -qr -- '^[12]\\.|^3\\.[0-3]' $version
|
||||
echo "fzf completion script requires fish version 3.4.0 or newer." >&2
|
||||
return 1
|
||||
else if not command -q fzf
|
||||
echo "fzf was not found in path." >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
function fzf_complete -w fzf -d 'fzf command completion and wildcard expansion search'
|
||||
# Restore the default shift-tab behavior on tab completions
|
||||
if commandline --paging-mode
|
||||
commandline -f complete-and-search
|
||||
return
|
||||
end
|
||||
|
||||
# Remove any trailing unescaped backslash from token and update command line
|
||||
set -l -- token (string replace -r -- '(?<!\\\\)(?:\\\\\\\\)*\\K\\\\$' '' (commandline -t | string collect) | string collect)
|
||||
commandline -rt -- $token
|
||||
|
||||
# Remove any line breaks from token
|
||||
set -- token (string replace -ra -- '\\\\\\n' '' $token | string collect)
|
||||
|
||||
# regex: Match token with unescaped/unquoted glob character
|
||||
set -l -- r_glob '^(?:[^\'"\\\\*]|\\\\[\\S\\s]|\'(?:\\\\[\\S\\s]|[^\'\\\\])*\'|"(?:\\\\[\\S\\s]|[^"\\\\])*")*\\*[\\S\\s]*$'
|
||||
|
||||
# regex: Match any unbalanced quote character
|
||||
set -l -- r_quote '^(?>(?:\\\\[\\s\\S]|"(?:[^"\\\\]|\\\\[\\s\\S])*"|\'(?:[^\'\\\\]|\\\\[\\s\\S])*\'|[^\'"\\\\]+)*)\\K[\'"]'
|
||||
|
||||
# The expansion pattern is the token with any open quote closed, or is empty.
|
||||
set -l -- glob_pattern (string match -r -- $r_glob $token | string collect)(string match -r -- $r_quote $token | string collect -a)
|
||||
|
||||
set -l -- cl_tokenize_opt '--tokens-expanded'
|
||||
string match -q -- '3.*' $version
|
||||
and set -- cl_tokenize_opt '--tokenize'
|
||||
|
||||
# Set command line tokens without any leading variable definitions or launcher
|
||||
# commands (including their options, but not any option arguments).
|
||||
set -l -- r_cmd '^(?:(?:builtin|command|doas|env|sudo|\\w+=\\S*|-\\S+)\\s+)*\\K[\\s\\S]+'
|
||||
set -l -- cmd (commandline $cl_tokenize_opt --input=(commandline -pc | string match -r $r_cmd))
|
||||
test -z "$token"
|
||||
and set -a -- cmd ''
|
||||
|
||||
# Set fzf options
|
||||
test -z "$FZF_TMUX_HEIGHT"
|
||||
and set -l -- FZF_TMUX_HEIGHT 40%
|
||||
|
||||
set -lax -- FZF_DEFAULT_OPTS \
|
||||
"--height=$FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" \
|
||||
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
|
||||
$FZF_DEFAULT_OPTS '--bind=alt-r:toggle-raw --multi --wrap=word --reverse' \
|
||||
(if test -n "$glob_pattern"; string collect -- $FZF_EXPANSION_OPTS; else;
|
||||
string collect -- $FZF_COMPLETION_OPTS; end; string escape -n -- $argv) \
|
||||
--with-shell=(status fish-path)\\ -c
|
||||
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
|
||||
set -l -- fzf_cmd fzf
|
||||
test "$FZF_TMUX" = 1
|
||||
and set -- fzf_cmd fzf-tmux $FZF_TMUX_OPTS -d$FZF_TMUX_HEIGHT --
|
||||
|
||||
set -l result
|
||||
|
||||
# Get the completion list from stdin when it's not a tty
|
||||
if not isatty stdin
|
||||
set -l -- custom_post_func _fzf_post_complete_$cmd[1]
|
||||
functions -q $custom_post_func
|
||||
or set -- custom_post_func _fzf_complete_$cmd[1]_post
|
||||
|
||||
if functions -q $custom_post_func
|
||||
$fzf_cmd | $custom_post_func $cmd | while read -l r; set -a -- result $r; end
|
||||
else if string match -q -- '*--print0*' "$FZF_DEFAULT_OPTS"
|
||||
$fzf_cmd | while read -lz r; set -a -- result $r; end
|
||||
else
|
||||
$fzf_cmd | while read -l r; set -a -- result $r; end
|
||||
end
|
||||
|
||||
# Wildcard expansion
|
||||
else if test -n "$glob_pattern"
|
||||
# Set the command to be run by fzf, so there is a visual indicator and an
|
||||
# easy way to abort on long recursive searches.
|
||||
set -lx -- FZF_DEFAULT_COMMAND "for i in $glob_pattern;" \
|
||||
'test -d "$i"; and string match -qv -- "*/" $i; and set -- i $i/;' \
|
||||
'string join0 -- $i; end'
|
||||
|
||||
set -- result (string escape -n -- ($fzf_cmd --read0 --print0 --scheme=path --no-multi-line | string split0))
|
||||
|
||||
# Command completion
|
||||
else
|
||||
# Call custom function if defined
|
||||
set -l -- custom_func _fzf_complete_$cmd[1]
|
||||
if functions -q $custom_func; and not set -q __fzf_no_custom_complete
|
||||
set -lx __fzf_no_custom_complete
|
||||
$custom_func $cmd
|
||||
return
|
||||
end
|
||||
|
||||
# Workaround for complete not having newlines in results
|
||||
if string match -qr -- '\\n' $token
|
||||
set -- token (string replace -ra -- '(?<!\\\\)(?:\\\\\\\\)*\\K\\\\\$' '\\\\\\\\\$' $token | string collect)
|
||||
set -- token (string unescape -- $token | string collect)
|
||||
set -- token (string replace -ra -- '\\n' '\\\\n' $token | string collect)
|
||||
end
|
||||
|
||||
set -- list (complete -C --escape -- (string join -- ' ' (commandline -pc $cl_tokenize_opt) $token | string collect))
|
||||
if test -n "$list"
|
||||
# Get the initial tabstop value
|
||||
if set -l -- tabstop (string match -rga -- '--tabstop[= ](?:0*)([1-9]\\d+|[4-9])' "$FZF_DEFAULT_OPTS")[-1]
|
||||
set -- tabstop (math $tabstop - 4)
|
||||
else
|
||||
set -- tabstop 4
|
||||
end
|
||||
|
||||
# Determine the tabstop length for description alignment
|
||||
set -l -- max_columns (math $COLUMNS - 40)
|
||||
for i in $list[1..500]
|
||||
set -l -- item (string split -f 1 -- \t $i)
|
||||
and set -l -- len (string length -V -- $item)
|
||||
and test "$len" -gt "$tabstop" -a "$len" -lt "$max_columns"
|
||||
and set -- tabstop $len
|
||||
end
|
||||
set -- tabstop (math $tabstop + 4)
|
||||
|
||||
set -- result (string collect -- $list | $fzf_cmd --delimiter="\t" --tabstop=$tabstop --wrap-sign=\t"↳ " --accept-nth=1)
|
||||
end
|
||||
end
|
||||
|
||||
# Update command line
|
||||
if test -n "$result"
|
||||
# No extra space after single selection that ends with path separator
|
||||
set -l -- tail ' '
|
||||
test (count $result) -eq 1
|
||||
and string match -q -- '*/' "$result"
|
||||
and set -- tail ''
|
||||
|
||||
commandline -rt -- (string join -- ' ' $result)$tail
|
||||
end
|
||||
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
function _fzf_complete
|
||||
set -l fzf_args
|
||||
for i in $argv
|
||||
string match -q -- '--' $i; and break
|
||||
set -a -- fzf_args $i
|
||||
end
|
||||
fzf_complete $fzf_args
|
||||
end
|
||||
|
||||
# Bind to shift-tab
|
||||
if string match -qr -- '^\\d\\d+|^[4-9]' $version
|
||||
bind shift-tab fzf_complete
|
||||
bind -M insert shift-tab fzf_complete
|
||||
else
|
||||
bind -k btab fzf_complete
|
||||
bind -M insert -k btab fzf_complete
|
||||
end
|
||||
@@ -0,0 +1,489 @@
|
||||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ completion.nu
|
||||
|
||||
|
||||
# An implementation of completion.nu
|
||||
# This loads FZF as a Nushell External Completer
|
||||
# https://www.nushell.sh/cookbook/external_completers.html
|
||||
|
||||
|
||||
# --- Default Environment Variables ---
|
||||
# These can be overridden in your config.nu or environment.
|
||||
# Example: $env.FZF_COMPLETION_TRIGGER = "!<TAB>"
|
||||
|
||||
# - $env.FZF_TMUX (default: 0)
|
||||
# - $env.FZF_TMUX_OPTS (default: empty)
|
||||
# - $env.FZF_TMUX_HEIGHT (default: 40%)
|
||||
# - $env.FZF_COMPLETION_TRIGGER (default: '**')
|
||||
# - $env.FZF_COMPLETION_OPTS (default: empty)
|
||||
# - $env.FZF_COMPLETION_PATH_OPTS (default: empty)
|
||||
# - $env.FZF_COMPLETION_DIR_OPTS (default: empty)
|
||||
|
||||
|
||||
|
||||
$env.FZF_COMPLETION_TRIGGER = $env.FZF_COMPLETION_TRIGGER? | default '**'
|
||||
|
||||
# Options for fzf completion in general. e.g. '--border'
|
||||
$env.FZF_COMPLETION_OPTS = $env.FZF_COMPLETION_OPTS? | default ''
|
||||
|
||||
# Options specific to path completion. e.g. '--extended'
|
||||
$env.FZF_COMPLETION_PATH_OPTS = $env.FZF_COMPLETION_PATH_OPTS? | default ''
|
||||
# Options specific to directory completion. e.g. '--extended'
|
||||
$env.FZF_COMPLETION_DIR_OPTS = $env.FZF_COMPLETION_DIR_OPTS? | default ''
|
||||
|
||||
$env.FZF_COMPLETION_DIR_COMMANDS = $env.FZF_COMPLETION_DIR_COMMANDS? | default ['cd', 'pushd', 'rmdir']
|
||||
|
||||
# --- Helper Functions ---
|
||||
|
||||
# Helper to build default fzf options list
|
||||
def __fzf_defaults [prepend: string, append: string]: nothing -> string {
|
||||
let base = $"--height ($env.FZF_TMUX_HEIGHT? | default '40%') --min-height 20+ --bind=ctrl-z:ignore ($prepend)"
|
||||
let opts_file = if ($env.FZF_DEFAULT_OPTS_FILE? | default '' | is-not-empty) {
|
||||
try { open --raw ($env.FZF_DEFAULT_OPTS_FILE) | str trim } catch { '' }
|
||||
} else {
|
||||
''
|
||||
}
|
||||
let default_opts = $env.FZF_DEFAULT_OPTS? | default ''
|
||||
$"($base) ($opts_file) ($default_opts) ($append)" | str trim
|
||||
}
|
||||
|
||||
# Wrapper for running fzf or fzf-tmux
|
||||
def __fzf_comprun [ context_name: string # e.g., "fzf-completion" , "fzf-helper" - mainly for potential debugging
|
||||
, query: string # The initial query string for fzf
|
||||
, fzf_opts_arg: list<string> # Remaining options for fzf/fzf-tmux
|
||||
] {
|
||||
let stdin_content = try {
|
||||
# Collect stdin into a single string. Adjust if structured data is expected.
|
||||
$in | into string
|
||||
} catch {
|
||||
null # Set to null if there's no stdin or an error occurs reading it
|
||||
}
|
||||
|
||||
let fzf_default_opts = (__fzf_defaults "" ($env.FZF_COMPLETION_OPTS | default ''))
|
||||
let fzf_prefinal_opt = ['--query', $query, '--reverse'] | append $fzf_opts_arg
|
||||
|
||||
# Get the configured height, defaulting to '40%'
|
||||
let height_opt = $env.FZF_TMUX_HEIGHT? | default '40%'
|
||||
|
||||
# Determine if fzf should generate its own candidates via walker
|
||||
let has_walker = ($fzf_prefinal_opt | find '--walker' | is-not-empty)
|
||||
|
||||
# Check for custom comprun function (Nu equivalent)
|
||||
if (which _fzf_comprun | is-not-empty) {
|
||||
# Note: Nushell doesn't have a direct equivalent to Zsh/Bash `type -t _fzf_comprun`.
|
||||
# This check assumes a user might define a custom command named `_fzf_comprun`.
|
||||
_fzf_comprun $context_name $query ...$fzf_prefinal_opt # Pass args correctly to custom function
|
||||
} else if ($env.TMUX_PANE? | default '' | into string | is-not-empty) and (($env.FZF_TMUX? | default 0) != 0 or ($env.FZF_TMUX_OPTS? | is-not-empty)) {
|
||||
# Running inside tmux, use fzf-tmux
|
||||
let final_fzf_opts = if ($env.FZF_TMUX_OPTS? | is-not-empty) {
|
||||
$env.FZF_TMUX_OPTS | split row ' ' | append ['--'] | append $fzf_prefinal_opt
|
||||
} else {
|
||||
# Use the default -d option with the configured height for fzf-tmux
|
||||
['-d', $height_opt, '--'] | append $fzf_prefinal_opt
|
||||
}
|
||||
|
||||
if $has_walker or ($stdin_content == null) {
|
||||
with-env { FZF_DEFAULT_OPTS: $fzf_default_opts, FZF_DEFAULT_OPTS_FILE: '' } { fzf-tmux ...$final_fzf_opts }
|
||||
} else {
|
||||
$stdin_content | with-env { FZF_DEFAULT_OPTS: $fzf_default_opts, FZF_DEFAULT_OPTS_FILE: '' } { fzf-tmux ...$final_fzf_opts }
|
||||
}
|
||||
|
||||
} else {
|
||||
# Not in tmux or not configured for fzf-tmux, use fzf directly
|
||||
let final_fzf_opts = $fzf_prefinal_opt
|
||||
|
||||
if $has_walker or ($stdin_content == null) {
|
||||
with-env { FZF_DEFAULT_OPTS: $fzf_default_opts, FZF_DEFAULT_OPTS_FILE: '' } { fzf ...$final_fzf_opts }
|
||||
} else {
|
||||
$stdin_content | with-env { FZF_DEFAULT_OPTS: $fzf_default_opts, FZF_DEFAULT_OPTS_FILE: '' } { fzf ...$final_fzf_opts }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Generate host list for ssh/telnet
|
||||
def __fzf_list_hosts [] {
|
||||
# Translate the Zsh pipeline using Nu commands and external tools
|
||||
let ssh_configs = try { open ~/.ssh/config | lines } catch { [] }
|
||||
let ssh_configs_d = try { open ~/.ssh/config.d/* | lines } catch { [] }
|
||||
let ssh_config_global = try { open /etc/ssh/ssh_config | lines } catch { [] }
|
||||
let known_hosts = try { open ~/.ssh/known_hosts | lines } catch { [] }
|
||||
let hosts_file = try { open /etc/hosts | lines } catch { [] }
|
||||
|
||||
[
|
||||
(
|
||||
# Process ssh config files
|
||||
$ssh_configs | append $ssh_configs_d | append $ssh_config_global
|
||||
| where {|it| ($it | str downcase | str starts-with 'host') or ($it | str downcase | str starts-with 'hostname') }
|
||||
| parse --regex '^\s*host(?:name)?\s+(?<hosts>.+)' # Extract hosts after keyword
|
||||
| default { hosts: null } # Handle lines that don't match regex
|
||||
| get hosts
|
||||
| where {|it| $it != null }
|
||||
| split row ' '
|
||||
| where {|it| not ($it =~ '[*?%]') } # Exclude patterns containing *, ?, or %
|
||||
)
|
||||
(
|
||||
# Process known_hosts file
|
||||
$known_hosts | parse --regex '^(?:\[)?(?<hosts>[a-z0-9.,:_-]+)' # Extract hostnames (possibly in [], possibly comma-separated) - added underscore
|
||||
| default { hosts: null }
|
||||
| get hosts
|
||||
| where {|it| $it != null }
|
||||
| each { |it| $it | split row ',' } # Split comma-separated hosts if any
|
||||
| flatten
|
||||
)
|
||||
(
|
||||
# Process /etc/hosts file
|
||||
$hosts_file | where { |it| not ($it | str starts-with '#') } # Ignore comments
|
||||
| where { |it| not ($it | str trim | is-empty) } # Ignore empty lines
|
||||
| where { |it| not ($it | str contains '0.0.0.0') } # Ignore 0.0.0.0
|
||||
| str replace --regex '#.*$' '' # Remove trailing comments
|
||||
| parse --regex '^\s*\S+\s+(?<hosts>.+)' # Extract hosts part (after IP)
|
||||
| default { hosts: null }
|
||||
| get hosts
|
||||
| where {|it| $it != null }
|
||||
| split row ' ' # Split multiple hosts on the same line
|
||||
)
|
||||
]
|
||||
| flatten # Combine all lists into a single stream
|
||||
| where {|it| not ($it | is-empty) } # Remove empty entries
|
||||
| sort | uniq # Sort and remove duplicates
|
||||
}
|
||||
|
||||
|
||||
# Base function for path/directory completion
|
||||
def __fzf_generic_path_completion [ prefix: string # The text before the trigger
|
||||
, fzf_opts_arg: list<string> # Extra options for fzf
|
||||
, suffix: string # Suffix to add to selection (e.g. , "/")
|
||||
] {
|
||||
# --- Determine walker root and initial query from the prefix ---
|
||||
|
||||
mut walker_root = "."
|
||||
mut initial_query = ""
|
||||
|
||||
if ($prefix | is-empty) {
|
||||
# Case: "**"
|
||||
$walker_root = "."
|
||||
$initial_query = ""
|
||||
} else if ($prefix | str contains (char separator)) {
|
||||
# Case: "dir/subdir/partial**" or "dir/**"
|
||||
$walker_root = $prefix | path dirname
|
||||
$initial_query = $prefix | path basename
|
||||
# Handle edge case where prefix ends with separator, e.g., "dir/"
|
||||
if ($prefix | str ends-with (char separator)) {
|
||||
# Remove trailing separator to get the intended directory
|
||||
$walker_root = $prefix | str substring 0..-2
|
||||
$initial_query = ""
|
||||
}
|
||||
# Ensure walker_root isn't empty if prefix was like "/file**"
|
||||
# or if path dirname returned empty string for some reason (e.g. prefix="file/")
|
||||
if ($walker_root | is-empty) {
|
||||
if ($prefix | str starts-with (char separator)) {
|
||||
$walker_root = (char separator)
|
||||
} else if ($prefix | str ends-with (char separator)) {
|
||||
$walker_root = $prefix | str substring 0..-2
|
||||
} else { $walker_root = "." } # Fallback if dirname weirdly fails
|
||||
}
|
||||
} else {
|
||||
# Case: "partial**" (no slashes)
|
||||
$walker_root = "."
|
||||
$initial_query = $prefix
|
||||
}
|
||||
|
||||
# --- Prepare FZF options ---
|
||||
let completion_type_opts = if $suffix == '/' {
|
||||
$env.FZF_COMPLETION_DIR_OPTS? | default '' | split row ' ' | where {not ($in | is-empty)}
|
||||
} else {
|
||||
$env.FZF_COMPLETION_PATH_OPTS? | default '' | split row ' ' | where {not ($in | is-empty)}
|
||||
}
|
||||
|
||||
let walker_type = if ($suffix == '/') {
|
||||
"dir,follow"
|
||||
} else {
|
||||
"file,dir,follow,hidden"
|
||||
}
|
||||
# Expand tilde so fzf receives a valid absolute path as walker-root
|
||||
let needs_tilde_rewrite = ($walker_root | str starts-with '~')
|
||||
let walker_root_expanded = ($walker_root | path expand)
|
||||
|
||||
# Use the 'walker_root' calculated at the beginning
|
||||
let fzf_all_opts = ["--scheme=path", "--walker", $walker_type, "--walker-root", $walker_root_expanded] | append $fzf_opts_arg
|
||||
| append $completion_type_opts
|
||||
|
||||
# Call FZF run
|
||||
let fzf_selection = ( __fzf_comprun "fzf-path-completion-walker" $initial_query $fzf_all_opts ) | str trim
|
||||
|
||||
|
||||
# --- Return Result ---
|
||||
if ($fzf_selection | is-not-empty) {
|
||||
# Restore tilde prefix if the user originally typed ~/
|
||||
let home = $nu.home-dir | path expand
|
||||
let result = if $needs_tilde_rewrite {
|
||||
$fzf_selection | lines | each {|line| $line | str replace $home '~' } | str join ' '
|
||||
} else {
|
||||
$fzf_selection | lines | str join ' '
|
||||
}
|
||||
[$result]
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
# Specific path completion wrapper
|
||||
def _fzf_path_completion [prefix: string] {
|
||||
# Zsh args: base, lbuf, _fzf_compgen_path, "-m", "", " "
|
||||
# Nu: prefix, empty command name (use find), ["-m"], "", " "
|
||||
__fzf_generic_path_completion $prefix ["-m"] ""
|
||||
}
|
||||
|
||||
# General completion helper for commands that feed a list to fzf
|
||||
# This is called by ssh, kill, and user-defined completers.
|
||||
def _fzf_complete [ query: string # The initial query string for fzf
|
||||
, data_gen_closure: closure # Closure that generates candidates
|
||||
, fzf_opts_arg: list<string> # Extra options for fzf (like -m, +m)
|
||||
, --post_process_closure: closure # Closure to process the selected item (optional)
|
||||
] {
|
||||
# Generate candidates using the provided command
|
||||
let candidates = try {
|
||||
do $data_gen_closure
|
||||
} catch {
|
||||
# Capture the actual error object provided by the catch block
|
||||
let actual_error = $in
|
||||
# Print a more informative error message including the actual error details
|
||||
print -e $"Error executing data_gen closure. Closure code: ($data_gen_closure). Actual error: ($actual_error)"
|
||||
[]
|
||||
}
|
||||
|
||||
# Run fzf and get selection
|
||||
let fzf_selection = $candidates | to text
|
||||
| __fzf_comprun "fzf-helper" $query $fzf_opts_arg
|
||||
| str trim # Trim potential trailing newline from fzf
|
||||
|
||||
# Apply post-processing if closure provided and selection is not empty
|
||||
let processed_selection = if ($fzf_selection | is-not-empty) and ($post_process_closure | is-not-empty) {
|
||||
# Call the post-processing closure with the selection
|
||||
try {
|
||||
do $post_process_closure $fzf_selection
|
||||
} catch {
|
||||
print -e $"Error executing post_process closure: ($post_process_closure)"
|
||||
$fzf_selection # Return original selection on error
|
||||
}
|
||||
} else {
|
||||
$fzf_selection
|
||||
}
|
||||
|
||||
if not ($processed_selection | is-empty) {
|
||||
[($processed_selection | lines | str join ' ')]
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
# SSH/Telnet completion
|
||||
def _fzf_complete_ssh [ prefix: string
|
||||
, input_line_before_trigger: string
|
||||
] {
|
||||
let words = ($input_line_before_trigger | split row ' ')
|
||||
let word_count = $words | length
|
||||
|
||||
# Find the index of the word being completed (which is the prefix)
|
||||
# If prefix is empty, completion happens after a space, index is word_count
|
||||
# If prefix is not empty, it's the last word, index is word_count - 1
|
||||
let completion_index = if ($prefix | is-empty) { $word_count } else { $word_count - 1 }
|
||||
|
||||
mut handled = false
|
||||
mut completion_result = [] # List of completion strings to return
|
||||
|
||||
# Check for -i, -F, -E flags immediately preceding the cursor position
|
||||
if $completion_index > 0 {
|
||||
let prev_arg = ($words | get ($completion_index - 1))
|
||||
if ($prev_arg in ['-i', '-F', '-E']) {
|
||||
$handled = true
|
||||
# Call path completion with the current prefix
|
||||
$completion_result = (_fzf_path_completion $prefix)
|
||||
}
|
||||
}
|
||||
|
||||
# If not handled by path completion, do host completion
|
||||
if not $handled {
|
||||
let user_part = if ($prefix | str contains "@") { ($prefix | split row "@" | first) + "@" } else { "" }
|
||||
# The part after '@' (or the whole prefix if no '@') is the initial query for fzf
|
||||
let query = if ($prefix | str contains "@") { $prefix | split row "@" | last } else { $prefix }
|
||||
|
||||
let host_candidates_gen = {||
|
||||
__fzf_list_hosts
|
||||
| each {|host_item| $user_part + $host_item } # Prepend user@ if present in prefix
|
||||
}
|
||||
|
||||
# Zsh options: +m -- ; Nu: pass ["+m"]
|
||||
# Pass the host part of the prefix to _fzf_complete for the initial query
|
||||
let selected_host = (_fzf_complete $query $host_candidates_gen ["+m"]) # Pass host_prefix here
|
||||
if not ($selected_host | is-empty) {
|
||||
$completion_result = $selected_host # _fzf_complete returns a list
|
||||
}
|
||||
}
|
||||
|
||||
$completion_result
|
||||
}
|
||||
|
||||
# Kill completion post-processor (extracts PID)
|
||||
def _fzf_complete_kill_post_get_pid [selected_line: string] {
|
||||
# Assuming standard ps output where PID is the second column
|
||||
$selected_line | lines | each { $in | from ssv --noheaders | get 0.column1 } | to text
|
||||
}
|
||||
|
||||
# Kill completion to get process PID
|
||||
def _fzf_complete_kill [query: string] {
|
||||
let ps_gen_closure = {|| # Define ps generator as a closure
|
||||
# Try standard ps, then busybox, then cygwin format approximation
|
||||
# Use `^ps` to ensure external command execution
|
||||
try {
|
||||
^ps -eo user,pid,ppid,start,time,command | complete | if $in.exit_code == 0 { $in.stdout | lines } else { error make {msg: "ps failed"} }
|
||||
} catch {
|
||||
try {
|
||||
^ps -eo user,pid,ppid,time,args | complete | if $in.exit_code == 0 { $in.stdout | lines } else { error make {msg: "ps failed"} }
|
||||
} catch {
|
||||
try {
|
||||
^ps --everyone --full --windows | complete | if $in.exit_code == 0 { $in.stdout | lines } else { error make {msg: "ps failed"} }
|
||||
} catch {
|
||||
print -e "Error: ps command failed."
|
||||
[] # Return empty list on failure
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Note: Complex Zsh FZF bindings for kill (click-header transformer) are omitted for simplicity.
|
||||
# Users can set custom bindings via FZF_DEFAULT_OPTS if needed.
|
||||
let kill_post_closure = {|selected_line| _fzf_complete_kill_post_get_pid $selected_line }
|
||||
|
||||
let fzf_opts = ["-m", "--header-lines=1", "--no-preview", "--wrap", "--color", "fg:dim,nth:regular"]
|
||||
|
||||
_fzf_complete $query $ps_gen_closure $fzf_opts --post_process_closure $kill_post_closure
|
||||
}
|
||||
|
||||
|
||||
# --- Main FZF External Completer ---
|
||||
|
||||
# This function is registered with Nushell's external completion system.
|
||||
# It gets called when Tab is pressed.
|
||||
let fzf_external_completer = {|spans|
|
||||
let trigger: string = $env.FZF_COMPLETION_TRIGGER? | default '**'
|
||||
|
||||
if ($trigger | is-empty) { return null } # Cannot work with empty trigger
|
||||
if (($spans | length ) == 0) { return null } # Nothing to complete
|
||||
|
||||
let last_span = $spans | last
|
||||
|
||||
if ($last_span | str ends-with $trigger) {
|
||||
# --- Trigger Found ---
|
||||
|
||||
# Skip sudo to determine the actual command
|
||||
let cmd_spans = if ($spans | first) == "sudo" { $spans | skip 1 } else { $spans }
|
||||
let cmd_word = ($cmd_spans | first | default "")
|
||||
|
||||
# Calculate the prefix (part before the trigger in the last span)
|
||||
let prefix = $last_span | str substring 0..(-1 * ($trigger | str length) - 1)
|
||||
|
||||
# Reconstruct the line content *before* the trigger for context
|
||||
# This is an approximation based on spans
|
||||
let line_without_trigger = $cmd_spans | take (($cmd_spans | length) - 1) | append $prefix | str join ' '
|
||||
|
||||
# --- Dispatch to Completer ---
|
||||
mut completion_results = [] # Will hold the list of strings from the completer
|
||||
|
||||
# Check for user-defined completer in $env.FZF_COMPLETERS first.
|
||||
# Users can define custom completers in their config.nu as a record of closures:
|
||||
# $env.FZF_COMPLETERS = { git: {|prefix, spans| ... }, docker: {|prefix, spans| ... } }
|
||||
# Each closure receives the prefix (text before the trigger) and the full
|
||||
# command spans (e.g. ["pacman", "-S", "vim**"]), and should return either:
|
||||
# - a list of candidate strings, or
|
||||
# - a record { candidates: [...], opts: [...], post: {|sel| ...} } to pass
|
||||
# custom fzf options and/or a post-processing closure.
|
||||
# See shell/completion-examples.nu for examples.
|
||||
let user_completers = ($env.FZF_COMPLETERS? | default {})
|
||||
if ($cmd_word in $user_completers) {
|
||||
let user_gen = ($user_completers | get $cmd_word)
|
||||
let user_result = (do $user_gen $prefix $cmd_spans)
|
||||
if ($user_result | describe | str starts-with 'record') {
|
||||
let candidates = ($user_result | get candidates)
|
||||
let fzf_opts = ($user_result | get opts? | default ["-m"])
|
||||
let post = ($user_result | get post? | default null)
|
||||
if ($post != null) {
|
||||
$completion_results = (_fzf_complete $prefix {|| $candidates} $fzf_opts --post_process_closure $post)
|
||||
} else {
|
||||
$completion_results = (_fzf_complete $prefix {|| $candidates} $fzf_opts)
|
||||
}
|
||||
} else {
|
||||
$completion_results = (_fzf_complete $prefix {|| $user_result} ["-m"])
|
||||
}
|
||||
} else {
|
||||
match $cmd_word {
|
||||
"ssh" | "scp" | "sftp" | "telnet" => { $completion_results = (_fzf_complete_ssh $prefix $line_without_trigger) }
|
||||
"kill" => { $completion_results = (_fzf_complete_kill $prefix) }
|
||||
_ if ($cmd_word in $env.FZF_COMPLETION_DIR_COMMANDS) => {
|
||||
$completion_results = (__fzf_generic_path_completion $prefix [] "/")
|
||||
}
|
||||
_ => {
|
||||
# Default to path completion if no specific command matches
|
||||
$completion_results = (_fzf_path_completion $prefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Return Results ---
|
||||
# The _fzf_... functions return a list of completion strings.
|
||||
# Nushell's completer expects the suggestions for the token being completed (prefix + trigger).
|
||||
# The results from the helper functions should be the final desired strings.
|
||||
# We don't need to manually add spaces; Nushell handles that.
|
||||
$completion_results # Return the list directly
|
||||
} else {
|
||||
# --- Trigger Not Found ---
|
||||
# Return null to let Nushell fall back to other completers (e.g., default file completion).
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
# --- WRAPPER AND REGISTRATION ---
|
||||
|
||||
# Guard against re-sourcing: wrapping the completer multiple times would
|
||||
# nest wrappers and grow the call chain on every reload.
|
||||
if ($env.__fzf_completer_registered? | default false) != true {
|
||||
|
||||
# Get the currently configured external completer, if any exists
|
||||
let previous_external_completer = $env.config? | get completions? | get external? | get completer?
|
||||
|
||||
# Define the new wrapper completer
|
||||
let fzf_wrapper_completer = {|spans|
|
||||
# 1. Try the FZF completer logic first
|
||||
let fzf_result = do $fzf_external_completer $spans
|
||||
|
||||
# 2. If FZF returned a result (a list, even an empty one), return it.
|
||||
# `null` means FZF didn't handle it because the trigger wasn't present.
|
||||
if $fzf_result != null {
|
||||
$fzf_result
|
||||
} else {
|
||||
# 3. FZF didn't handle it, so call the previous completer (if it exists).
|
||||
if $previous_external_completer != null {
|
||||
do $previous_external_completer $spans
|
||||
} else {
|
||||
# 4. No previous completer, and FZF didn't handle it. Return null.
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Register the new wrapper completer
|
||||
# This ensures external completions are enabled and sets our wrapper.
|
||||
$env.config = $env.config | upsert completions {
|
||||
external: {
|
||||
enable: true
|
||||
completer: $fzf_wrapper_completer
|
||||
}
|
||||
}
|
||||
|
||||
$env.__fzf_completer_registered = true
|
||||
}
|
||||
|
||||
# vim: set sts=2 ts=2 sw=2 tw=120 et :
|
||||
+348
-77
@@ -2,52 +2,165 @@
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/-completion.zsh
|
||||
# /_/ /___/_/ completion.zsh
|
||||
#
|
||||
# - $FZF_TMUX (default: 0)
|
||||
# - $FZF_TMUX_HEIGHT (default: '40%')
|
||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
|
||||
# - $FZF_COMPLETION_DIR_OPTS (default: empty)
|
||||
|
||||
if [[ $- =~ i ]]; then
|
||||
|
||||
# Both branches of the following `if` do the same thing -- define
|
||||
# __fzf_completion_options such that `eval $__fzf_completion_options` sets
|
||||
# all options to the same values they currently have. We'll do just that at
|
||||
# the bottom of the file after changing options to what we prefer.
|
||||
#
|
||||
# IMPORTANT: Until we get to the `emulate` line, all words that *can* be quoted
|
||||
# *must* be quoted in order to prevent alias expansion. In addition, code must
|
||||
# be written in a way works with any set of zsh options. This is very tricky, so
|
||||
# careful when you change it.
|
||||
#
|
||||
# Start by loading the builtin zsh/parameter module. It provides `options`
|
||||
# associative array that stores current shell options.
|
||||
if 'zmodload' 'zsh/parameter' 2>'/dev/null' && (( ${+options} )); then
|
||||
# This is the fast branch and it gets taken on virtually all Zsh installations.
|
||||
#
|
||||
# ${(kv)options[@]} expands to array of keys (option names) and values ("on"
|
||||
# or "off"). The subsequent expansion# with (j: :) flag joins all elements
|
||||
# together separated by spaces. __fzf_completion_options ends up with a value
|
||||
# like this: "options=(shwordsplit off aliases on ...)".
|
||||
__fzf_completion_options="options=(${(j: :)${(kv)options[@]}})"
|
||||
else
|
||||
# This branch is much slower because it forks to get the names of all
|
||||
# zsh options. It's possible to eliminate this fork but it's not worth the
|
||||
# trouble because this branch gets taken only on very ancient or broken
|
||||
# zsh installations.
|
||||
() {
|
||||
# That `()` above defines an anonymous function. This is essentially a scope
|
||||
# for local parameters. We use it to avoid polluting global scope.
|
||||
'local' '__fzf_opt'
|
||||
__fzf_completion_options="setopt"
|
||||
# `set -o` prints one line for every zsh option. Each line contains option
|
||||
# name, some spaces, and then either "on" or "off". We just want option names.
|
||||
# Expansion with (@f) flag splits a string into lines. The outer expansion
|
||||
# removes spaces and everything that follow them on every line. __fzf_opt
|
||||
# ends up iterating over option names: shwordsplit, aliases, etc.
|
||||
for __fzf_opt in "${(@)${(@f)$(set -o)}%% *}"; do
|
||||
if [[ -o "$__fzf_opt" ]]; then
|
||||
# Option $__fzf_opt is currently on, so remember to set it back on.
|
||||
__fzf_completion_options+=" -o $__fzf_opt"
|
||||
else
|
||||
# Option $__fzf_opt is currently off, so remember to set it back off.
|
||||
__fzf_completion_options+=" +o $__fzf_opt"
|
||||
fi
|
||||
done
|
||||
# The value of __fzf_completion_options here looks like this:
|
||||
# "setopt +o shwordsplit -o aliases ..."
|
||||
}
|
||||
fi
|
||||
|
||||
# Enable the default zsh options (those marked with <Z> in `man zshoptions`)
|
||||
# but without `aliases`. Aliases in functions are expanded when functions are
|
||||
# defined, so if we disable aliases here, we'll be sure to have no pesky
|
||||
# aliases in any of our functions. This way we won't need prefix every
|
||||
# command with `command` or to quote every word to defend against global
|
||||
# aliases. Note that `aliases` is not the only option that's important to
|
||||
# control. There are several others that could wreck havoc if they are set
|
||||
# to values we don't expect. With the following `emulate` command we
|
||||
# sidestep this issue entirely.
|
||||
'builtin' 'emulate' 'zsh' && 'builtin' 'setopt' 'no_aliases'
|
||||
|
||||
# This brace is the start of try-always block. The `always` part is like
|
||||
# `finally` in lesser languages. We use it to *always* restore user options.
|
||||
{
|
||||
# The 'emulate' command should not be placed inside the interactive if check;
|
||||
# placing it there fails to disable alias expansion. See #3731.
|
||||
if [[ -o interactive ]]; then
|
||||
|
||||
# To use custom commands instead of find, override _fzf_compgen_{path,dir}
|
||||
if ! declare -f _fzf_compgen_path > /dev/null; then
|
||||
_fzf_compgen_path() {
|
||||
echo "$1"
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
|
||||
if ! declare -f _fzf_compgen_dir > /dev/null; then
|
||||
_fzf_compgen_dir() {
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
#
|
||||
# _fzf_compgen_path() {
|
||||
# echo "$1"
|
||||
# command find -L "$1" \
|
||||
# -name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
|
||||
# -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
# }
|
||||
#
|
||||
# _fzf_compgen_dir() {
|
||||
# command find -L "$1" \
|
||||
# -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
|
||||
# -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
# }
|
||||
|
||||
###########################################################
|
||||
|
||||
__fzfcmd_complete() {
|
||||
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] &&
|
||||
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
|
||||
#----BEGIN INCLUDE common.sh
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
|
||||
__fzf_defaults() {
|
||||
builtin printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
builtin printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
if [[ -z ${__fzf_awk-} ]]; then
|
||||
__fzf_awk=awk
|
||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||
__fzf_awk=/usr/xpg4/bin/awk
|
||||
elif command -v mawk > /dev/null 2>&1; then
|
||||
local n x y z d
|
||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] &&
|
||||
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
|
||||
((d >= 20230302)) 2> /dev/null &&
|
||||
__fzf_awk=mawk
|
||||
fi
|
||||
fi
|
||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||
}
|
||||
#----END INCLUDE
|
||||
|
||||
__fzf_comprun() {
|
||||
if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then
|
||||
_fzf_comprun "$@"
|
||||
elif [ -n "${TMUX_PANE-}" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "${FZF_TMUX_OPTS-}" ]; }; then
|
||||
shift
|
||||
if [ -n "${FZF_TMUX_OPTS-}" ]; then
|
||||
fzf-tmux ${(Q)${(Z+n+)FZF_TMUX_OPTS}} -- "$@"
|
||||
else
|
||||
fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%} -- "$@"
|
||||
fi
|
||||
else
|
||||
shift
|
||||
fzf "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Extract the name of the command. e.g. ls; foo=1 ssh **<tab>
|
||||
__fzf_extract_command() {
|
||||
# Control completion with the "compstate" parameter, insert and list nothing
|
||||
compstate[insert]=
|
||||
compstate[list]=
|
||||
cmd_word="${(Q)words[1]}"
|
||||
}
|
||||
|
||||
__fzf_generic_path_completion() {
|
||||
local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches
|
||||
local base lbuf compgen fzf_opts suffix tail dir leftover matches
|
||||
base=$1
|
||||
lbuf=$2
|
||||
compgen=$3
|
||||
fzf_opts=$4
|
||||
suffix=$5
|
||||
tail=$6
|
||||
fzf="$(__fzfcmd_complete)"
|
||||
|
||||
setopt localoptions nonomatch
|
||||
eval "base=$base"
|
||||
if [[ $base = *'$('* ]] || [[ $base = *'<('* ]] || [[ $base = *'>('* ]] || [[ $base = *':='* ]] || [[ $base = *'`'* ]]; then
|
||||
return
|
||||
fi
|
||||
eval "base=$base" 2> /dev/null || return
|
||||
[[ $base = *"/"* ]] && dir="$base"
|
||||
while [ 1 ]; do
|
||||
if [[ -z "$dir" || -d ${dir} ]]; then
|
||||
@@ -55,9 +168,29 @@ __fzf_generic_path_completion() {
|
||||
leftover=${leftover/#\/}
|
||||
[ -z "$dir" ] && dir='.'
|
||||
[ "$dir" != "/" ] && dir="${dir/%\//}"
|
||||
matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "$leftover" | while read item; do
|
||||
echo -n "${(q)item}$suffix "
|
||||
done)
|
||||
matches=$(
|
||||
export FZF_DEFAULT_OPTS
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
|
||||
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
|
||||
if [[ $compgen =~ dir ]]; then
|
||||
rest=${FZF_COMPLETION_DIR_OPTS-}
|
||||
else
|
||||
rest=${FZF_COMPLETION_PATH_OPTS-}
|
||||
fi
|
||||
if declare -f "$compgen" > /dev/null; then
|
||||
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" ${(Q)${(Z+n+)rest}}
|
||||
else
|
||||
if [[ $compgen =~ dir ]]; then
|
||||
walker=dir,follow
|
||||
else
|
||||
walker=file,dir,follow,hidden
|
||||
fi
|
||||
__fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
|
||||
fi | while read -r item; do
|
||||
item="${item%$suffix}$suffix"
|
||||
echo -n -E "${(q)item} "
|
||||
done
|
||||
)
|
||||
matches=${matches% }
|
||||
if [ -n "$matches" ]; then
|
||||
LBUFFER="$lbuf$matches$tail"
|
||||
@@ -80,68 +213,183 @@ _fzf_dir_completion() {
|
||||
"" "/" ""
|
||||
}
|
||||
|
||||
_fzf_feed_fifo() (
|
||||
_fzf_feed_fifo() {
|
||||
command rm -f "$1"
|
||||
mkfifo "$1"
|
||||
cat <&0 > "$1" &
|
||||
)
|
||||
cat <&0 > "$1" &|
|
||||
}
|
||||
|
||||
_fzf_complete() {
|
||||
local fifo fzf_opts lbuf fzf matches post
|
||||
setopt localoptions ksh_arrays
|
||||
# Split arguments around --
|
||||
local args rest str_arg i sep
|
||||
args=("$@")
|
||||
sep=
|
||||
for i in {0..${#args[@]}}; do
|
||||
if [[ "${args[$i]-}" = -- ]]; then
|
||||
sep=$i
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ -n "$sep" ]]; then
|
||||
str_arg=
|
||||
rest=("${args[@]:$((sep + 1)):${#args[@]}}")
|
||||
args=("${args[@]:0:$sep}")
|
||||
else
|
||||
str_arg=$1
|
||||
args=()
|
||||
shift
|
||||
rest=("$@")
|
||||
fi
|
||||
|
||||
local fifo lbuf matches post
|
||||
fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$"
|
||||
fzf_opts=$1
|
||||
lbuf=$2
|
||||
post="${funcstack[2]}_post"
|
||||
lbuf=${rest[0]}
|
||||
post="${funcstack[1]}_post"
|
||||
type $post > /dev/null 2>&1 || post=cat
|
||||
|
||||
fzf="$(__fzfcmd_complete)"
|
||||
|
||||
_fzf_feed_fifo "$fifo"
|
||||
matches=$(cat "$fifo" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ')
|
||||
matches=$(
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
|
||||
FZF_DEFAULT_OPTS_FILE='' \
|
||||
__fzf_comprun "$cmd_word" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
|
||||
if [ -n "$matches" ]; then
|
||||
LBUFFER="$lbuf$matches"
|
||||
fi
|
||||
zle reset-prompt
|
||||
command rm -f "$fifo"
|
||||
}
|
||||
|
||||
# To use custom hostname lists, override __fzf_list_hosts.
|
||||
# The function is expected to print hostnames, one per line as well as in the
|
||||
# desired sorting and with any duplicates removed, to standard output.
|
||||
if ! declare -f __fzf_list_hosts > /dev/null; then
|
||||
__fzf_list_hosts() {
|
||||
command sort -u \
|
||||
<(
|
||||
# Note: To make the pathname expansion of "~/.ssh/config.d/*" work
|
||||
# properly, we need to adjust the related shell options. We need to
|
||||
# unset "NO_GLOB" (or reset "GLOB"), which disable the pathname
|
||||
# expansion totally. We need to unset "DOT_GLOB" and set "CASE_GLOB"
|
||||
# to avoid matching unwanted files. We need to set "NULL_GLOB" to
|
||||
# avoid attempting to read the literal filename '~/.ssh/config.d/*'
|
||||
# when no matching is found.
|
||||
setopt GLOB NO_DOT_GLOB CASE_GLOB NO_NOMATCH NULL_GLOB
|
||||
|
||||
__fzf_exec_awk '
|
||||
# Note: mawk <= 1.3.3-20090705 does not support the POSIX brackets of
|
||||
# the form [[:blank:]], and Ubuntu 18.04 LTS still uses this
|
||||
# 16-year-old mawk unfortunately. We need to use [ \t] instead.
|
||||
match(tolower($0), /^[ \t]*host(name)?[ \t]*[ \t=]/) {
|
||||
$0 = substr($0, RLENGTH + 1) # Remove "Host(name)?=?"
|
||||
sub(/#.*/, "")
|
||||
for (i = 1; i <= NF; i++)
|
||||
if ($i !~ /[*?%]/)
|
||||
print $i
|
||||
}
|
||||
' ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null
|
||||
) \
|
||||
<(
|
||||
__fzf_exec_awk -F ',' '
|
||||
match($0, /^[][a-zA-Z0-9.,:-]+/) {
|
||||
$0 = substr($0, 1, RLENGTH)
|
||||
gsub(/[][]|:[^,]*/, "")
|
||||
for (i = 1; i <= NF; i++)
|
||||
print $i
|
||||
}
|
||||
' ~/.ssh/known_hosts 2> /dev/null
|
||||
) \
|
||||
<(
|
||||
__fzf_exec_awk '
|
||||
{
|
||||
sub(/#.*/, "")
|
||||
for (i = 2; i <= NF; i++)
|
||||
if ($i != "0.0.0.0")
|
||||
print $i
|
||||
}
|
||||
' /etc/hosts 2> /dev/null
|
||||
)
|
||||
}
|
||||
fi
|
||||
|
||||
_fzf_complete_telnet() {
|
||||
_fzf_complete '+m' "$@" < <(
|
||||
command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' |
|
||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
||||
)
|
||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts)
|
||||
}
|
||||
|
||||
# The first and the only argument is the LBUFFER without the current word that contains the trigger.
|
||||
# The current word without the trigger is in the $prefix variable passed from the caller.
|
||||
_fzf_complete_ssh() {
|
||||
_fzf_complete '+m' "$@" < <(
|
||||
setopt localoptions nonomatch
|
||||
command cat <(cat ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \
|
||||
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
|
||||
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
|
||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
||||
)
|
||||
local -a tokens
|
||||
tokens=(${(z)1})
|
||||
case ${tokens[-1]} in
|
||||
-i|-F|-E)
|
||||
_fzf_path_completion "$prefix" "$1"
|
||||
;;
|
||||
*)
|
||||
local user
|
||||
[[ $prefix =~ @ ]] && user="${prefix%%@*}@"
|
||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | __fzf_exec_awk -v user="$user" '{print user $0}')
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_fzf_complete_export() {
|
||||
_fzf_complete '-m' "$@" < <(
|
||||
_fzf_complete -m -- "$@" < <(
|
||||
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_complete_unset() {
|
||||
_fzf_complete '-m' "$@" < <(
|
||||
_fzf_complete -m -- "$@" < <(
|
||||
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_complete_unalias() {
|
||||
_fzf_complete '+m' "$@" < <(
|
||||
_fzf_complete +m -- "$@" < <(
|
||||
alias | sed 's/=.*//'
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_complete_kill() {
|
||||
local transformer
|
||||
transformer='
|
||||
if [[ $FZF_KEY =~ ctrl|alt|shift ]] && [[ -n $FZF_NTH ]]; then
|
||||
nths=( ${FZF_NTH//,/ } )
|
||||
new_nths=()
|
||||
found=0
|
||||
for nth in ${nths[@]}; do
|
||||
if [[ $nth = $FZF_CLICK_HEADER_NTH ]]; then
|
||||
found=1
|
||||
else
|
||||
new_nths+=($nth)
|
||||
fi
|
||||
done
|
||||
[[ $found = 0 ]] && new_nths+=($FZF_CLICK_HEADER_NTH)
|
||||
new_nths=${new_nths[*]}
|
||||
new_nths=${new_nths// /,}
|
||||
echo "change-nth($new_nths)+change-prompt($new_nths> )"
|
||||
else
|
||||
if [[ $FZF_NTH = $FZF_CLICK_HEADER_NTH ]]; then
|
||||
echo "change-nth()+change-prompt(> )"
|
||||
else
|
||||
echo "change-nth($FZF_CLICK_HEADER_NTH)+change-prompt($FZF_CLICK_HEADER_WORD> )"
|
||||
fi
|
||||
fi
|
||||
'
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap --color fg:dim,nth:regular \
|
||||
--bind "click-header:transform:$transformer" -- "$@" < <(
|
||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
||||
command ps --everyone --full --windows # For cygwin
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_complete_kill_post() {
|
||||
__fzf_exec_awk '{print $2}'
|
||||
}
|
||||
|
||||
fzf-completion() {
|
||||
local tokens cmd prefix trigger tail fzf matches lbuf d_cmds
|
||||
local tokens prefix trigger tail matches lbuf d_cmds cursor_pos cmd_word
|
||||
setopt localoptions noshwordsplit noksh_arrays noposixbuiltins
|
||||
|
||||
# http://zsh.sourceforge.net/FAQ/zshfaq03.html
|
||||
@@ -152,37 +400,54 @@ fzf-completion() {
|
||||
return
|
||||
fi
|
||||
|
||||
cmd=${tokens[1]}
|
||||
|
||||
# Explicitly allow for empty trigger.
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
[ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("")
|
||||
[[ -z $trigger && ${LBUFFER[-1]} == ' ' ]] && tokens+=("")
|
||||
|
||||
# When the trigger starts with ';', it becomes a separate token
|
||||
if [[ ${LBUFFER} = *"${tokens[-2]}${tokens[-1]}" ]]; then
|
||||
tokens[-2]="${tokens[-2]}${tokens[-1]}"
|
||||
if [[ ${LBUFFER} = *"${tokens[-2]-}${tokens[-1]}" ]]; then
|
||||
tokens[-2]="${tokens[-2]-}${tokens[-1]}"
|
||||
tokens=(${tokens[0,-2]})
|
||||
fi
|
||||
|
||||
lbuf=$LBUFFER
|
||||
tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
|
||||
# Kill completion (do not require trigger sequence)
|
||||
if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then
|
||||
fzf="$(__fzfcmd_complete)"
|
||||
matches=$(command ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" ${=fzf} -m | awk '{print $2}' | tr '\n' ' ')
|
||||
if [ -n "$matches" ]; then
|
||||
LBUFFER="$LBUFFER$matches"
|
||||
fi
|
||||
zle reset-prompt
|
||||
|
||||
# Trigger sequence given
|
||||
elif [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
|
||||
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir})
|
||||
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
|
||||
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir})
|
||||
|
||||
{
|
||||
cursor_pos=$CURSOR
|
||||
# Move the cursor before the trigger to preserve word array elements when
|
||||
# trigger chars like ';' or '`' would otherwise reset the 'words' array.
|
||||
CURSOR=$((cursor_pos - ${#trigger} - 1))
|
||||
# Check if at least one completion system (old or new) is active.
|
||||
# If at least one user-defined completion widget is detected, nothing will
|
||||
# be completed if neither the old nor the new completion system is enabled.
|
||||
# In such cases, the 'zsh/compctl' module is loaded as a fallback.
|
||||
if ! zmodload -F zsh/parameter p:functions 2>/dev/null || ! (( ${+functions[compdef]} )); then
|
||||
zmodload -F zsh/compctl 2>/dev/null
|
||||
fi
|
||||
# Create a completion widget to access the 'words' array (man zshcompwid)
|
||||
zle -C __fzf_extract_command .complete-word __fzf_extract_command
|
||||
zle __fzf_extract_command
|
||||
} always {
|
||||
CURSOR=$cursor_pos
|
||||
# Delete the completion widget
|
||||
zle -D __fzf_extract_command 2>/dev/null
|
||||
}
|
||||
|
||||
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
|
||||
[ -z "${tokens[-1]}" ] && lbuf=$LBUFFER || lbuf=${LBUFFER:0:-${#tokens[-1]}}
|
||||
if [[ $prefix = *'$('* ]] || [[ $prefix = *'<('* ]] || [[ $prefix = *'>('* ]] || [[ $prefix = *':='* ]] || [[ $prefix = *'`'* ]]; then
|
||||
return
|
||||
fi
|
||||
[ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}}
|
||||
|
||||
if eval "type _fzf_complete_${cmd} > /dev/null"; then
|
||||
eval "prefix=\"$prefix\" _fzf_complete_${cmd} \"$lbuf\""
|
||||
elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
|
||||
if eval "noglob type _fzf_complete_${cmd_word} >/dev/null"; then
|
||||
prefix="$prefix" eval _fzf_complete_${cmd_word} ${(q)lbuf}
|
||||
zle reset-prompt
|
||||
elif [ ${d_cmds[(i)$cmd_word]} -le ${#d_cmds} ]; then
|
||||
_fzf_dir_completion "$prefix" "$lbuf"
|
||||
else
|
||||
_fzf_path_completion "$prefix" "$lbuf"
|
||||
@@ -199,7 +464,13 @@ fzf-completion() {
|
||||
unset binding
|
||||
}
|
||||
|
||||
# Normal widget
|
||||
zle -N fzf-completion
|
||||
bindkey '^I' fzf-completion
|
||||
|
||||
fi
|
||||
|
||||
} always {
|
||||
# Restore the original options.
|
||||
eval $__fzf_completion_options
|
||||
'unset' '__fzf_completion_options'
|
||||
}
|
||||
|
||||
+165
-91
@@ -1,121 +1,195 @@
|
||||
# Key bindings
|
||||
# ------------
|
||||
__fzf_select__() {
|
||||
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type f -print \
|
||||
-o -type d -print \
|
||||
-o -type l -print 2> /dev/null | cut -b3-"}"
|
||||
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" fzf -m "$@" | while read -r item; do
|
||||
printf '%q ' "$item"
|
||||
done
|
||||
echo
|
||||
}
|
||||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ key-bindings.bash
|
||||
#
|
||||
# - $FZF_CTRL_T_COMMAND
|
||||
# - $FZF_CTRL_T_OPTS
|
||||
# - $FZF_CTRL_R_COMMAND
|
||||
# - $FZF_CTRL_R_OPTS
|
||||
# - $FZF_ALT_C_COMMAND
|
||||
# - $FZF_ALT_C_OPTS
|
||||
|
||||
if [[ $- =~ i ]]; then
|
||||
|
||||
__fzf_use_tmux__() {
|
||||
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ]
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
|
||||
#----BEGIN shfmt
|
||||
#----BEGIN INCLUDE common.sh
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
|
||||
__fzf_defaults() {
|
||||
builtin printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
builtin printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
if [[ -z ${__fzf_awk-} ]]; then
|
||||
__fzf_awk=awk
|
||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||
__fzf_awk=/usr/xpg4/bin/awk
|
||||
elif command -v mawk > /dev/null 2>&1; then
|
||||
local n x y z d
|
||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] &&
|
||||
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
|
||||
((d >= 20230302)) 2> /dev/null &&
|
||||
__fzf_awk=mawk
|
||||
fi
|
||||
fi
|
||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||
}
|
||||
#----END INCLUDE
|
||||
|
||||
__fzf_select__() {
|
||||
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" |
|
||||
while read -r item; do
|
||||
printf '%q ' "$item" # escape special chars
|
||||
done
|
||||
}
|
||||
|
||||
__fzfcmd() {
|
||||
__fzf_use_tmux__ &&
|
||||
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
|
||||
}
|
||||
|
||||
__fzf_select_tmux__() {
|
||||
local height
|
||||
height=${FZF_TMUX_HEIGHT:-40%}
|
||||
if [[ $height =~ %$ ]]; then
|
||||
height="-p ${height%\%}"
|
||||
else
|
||||
height="-l $height"
|
||||
fi
|
||||
|
||||
tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") bash -c 'source \"${BASH_SOURCE[0]}\"; RESULT=\"\$(__fzf_select__ --no-height)\"; tmux setb -b fzf \"\$RESULT\" \\; pasteb -b fzf -t $TMUX_PANE \\; deleteb -b fzf || tmux send-keys -t $TMUX_PANE \"\$RESULT\"'"
|
||||
[[ -n ${TMUX_PANE-} ]] && { [[ ${FZF_TMUX:-0} != 0 ]] || [[ -n ${FZF_TMUX_OPTS-} ]]; } &&
|
||||
echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf"
|
||||
}
|
||||
|
||||
fzf-file-widget() {
|
||||
if __fzf_use_tmux__; then
|
||||
__fzf_select_tmux__
|
||||
else
|
||||
local selected="$(__fzf_select__)"
|
||||
READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}"
|
||||
READLINE_POINT=$(( READLINE_POINT + ${#selected} ))
|
||||
fi
|
||||
local selected="$(__fzf_select__ "$@")"
|
||||
READLINE_LINE="${READLINE_LINE:0:READLINE_POINT}$selected${READLINE_LINE:READLINE_POINT}"
|
||||
READLINE_POINT=$((READLINE_POINT + ${#selected}))
|
||||
}
|
||||
|
||||
__fzf_cd__() {
|
||||
local cmd dir
|
||||
cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type d -print 2> /dev/null | cut -b3-"}"
|
||||
dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd %q' "$dir"
|
||||
local dir
|
||||
dir=$(
|
||||
FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path" "${FZF_ALT_C_OPTS-} +m") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd)
|
||||
) && printf 'builtin cd -- %q' "$(builtin unset CDPATH && builtin cd -- "$dir" && builtin pwd)"
|
||||
}
|
||||
|
||||
__fzf_history__() (
|
||||
local line
|
||||
shopt -u nocaseglob nocasematch
|
||||
line=$(
|
||||
HISTTIMEFORMAT= builtin history |
|
||||
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS --tac --sync -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m" $(__fzfcmd) |
|
||||
command grep '^ *[0-9]') &&
|
||||
if [[ $- =~ H ]]; then
|
||||
sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line"
|
||||
__fzf_history_delete() {
|
||||
[[ -s $1 ]] || return
|
||||
|
||||
local offsets
|
||||
offsets=($(sort -rnu "$1"))
|
||||
for offset in "${offsets[@]}"; do
|
||||
builtin history -d "$offset"
|
||||
done
|
||||
|
||||
if [[ ${#offsets[@]} -gt 0 ]] && shopt -q histappend; then
|
||||
builtin history -w
|
||||
fi
|
||||
}
|
||||
|
||||
if command -v perl > /dev/null; then
|
||||
__fzf_history__() {
|
||||
local output script deletefile
|
||||
deletefile=$(mktemp)
|
||||
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; s/\n/\n\t/gm; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
|
||||
output=$(
|
||||
set +o pipefail
|
||||
builtin fc -lnr -2147483648 |
|
||||
last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '"$'\t'"↳ ' --highlight-line --bind 'shift-delete:execute-silent(cat {+f1} >> \"$deletefile\")+exclude-multi' --multi ${FZF_CTRL_R_OPTS-} --read0") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
||||
)
|
||||
__fzf_history_delete "$deletefile"
|
||||
command rm -f "$deletefile"
|
||||
[[ -n $output ]] || return
|
||||
READLINE_LINE=$(command perl -pe 's/^\d*\t//' <<< "$output")
|
||||
if [[ -z $READLINE_POINT ]]; then
|
||||
echo "$READLINE_LINE"
|
||||
else
|
||||
sed 's/^ *\([0-9]*\)\** *//' <<< "$line"
|
||||
READLINE_POINT=0x7fffffff
|
||||
fi
|
||||
)
|
||||
}
|
||||
else # awk - fallback for POSIX systems
|
||||
__fzf_history__() {
|
||||
local output script deletefile
|
||||
deletefile=$(mktemp)
|
||||
[[ $(HISTTIMEFORMAT='' builtin history 1) =~ [[:digit:]]+ ]] # how many history entries
|
||||
script='function P(b) { ++n; sub(/^[ *]/, "", b); if (!seen[b]++) { printf "%d\t%s%c", '$((BASH_REMATCH + 1))' - n, b, 0 } }
|
||||
NR==1 { b = substr($0, 2); next }
|
||||
/^\t/ { P(b); b = substr($0, 2); next }
|
||||
{ b = b RS $0 }
|
||||
END { if (NR) P(b) }'
|
||||
output=$(
|
||||
set +o pipefail
|
||||
builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
|
||||
__fzf_exec_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '"$'\t'"↳ ' --highlight-line --bind 'shift-delete:execute-silent(cat {+f1} >> \"$deletefile\")+exclude-multi' --multi ${FZF_CTRL_R_OPTS-} --read0") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
||||
)
|
||||
__fzf_history_delete "$deletefile"
|
||||
command rm -f "$deletefile"
|
||||
[[ -n $output ]] || return
|
||||
READLINE_LINE=${output#*$'\t'}
|
||||
if [[ -z $READLINE_POINT ]]; then
|
||||
echo "$READLINE_LINE"
|
||||
else
|
||||
READLINE_POINT=0x7fffffff
|
||||
fi
|
||||
}
|
||||
fi
|
||||
|
||||
if [[ ! -o vi ]]; then
|
||||
# Required to refresh the prompt after fzf
|
||||
bind '"\er": redraw-current-line'
|
||||
bind '"\e^": history-expand-line'
|
||||
# Required to refresh the prompt after fzf
|
||||
bind -m emacs-standard '"\C-\e(": redraw-current-line'
|
||||
|
||||
bind -m vi-command '"\C-z": emacs-editing-mode'
|
||||
bind -m vi-insert '"\C-z": emacs-editing-mode'
|
||||
bind -m emacs-standard '"\C-z": vi-editing-mode'
|
||||
|
||||
if ((BASH_VERSINFO[0] < 4)); then
|
||||
# CTRL-T - Paste the selected file path into the command line
|
||||
if [ $BASH_VERSINFO -gt 3 ]; then
|
||||
bind -x '"\C-t": "fzf-file-widget"'
|
||||
elif __fzf_use_tmux__; then
|
||||
bind '"\C-t": " \C-u \C-a\C-k`__fzf_select_tmux__`\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"'
|
||||
else
|
||||
bind '"\C-t": " \C-u \C-a\C-k`__fzf_select__`\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"'
|
||||
if [[ ${FZF_CTRL_T_COMMAND-x} != "" ]]; then
|
||||
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\C-\e(\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f\C-y\ey\C-_"'
|
||||
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
|
||||
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
|
||||
fi
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
bind '"\C-r": " \C-e\C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\er\e^"'
|
||||
|
||||
# ALT-C - cd into the selected directory
|
||||
bind '"\ec": " \C-e\C-u`__fzf_cd__`\e\C-e\er\C-m"'
|
||||
if [[ ${FZF_CTRL_R_COMMAND-x} != "" ]]; then
|
||||
if [[ -n ${FZF_CTRL_R_COMMAND-} ]]; then
|
||||
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
|
||||
fi
|
||||
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\C-\e("'
|
||||
bind -m vi-command '"\C-r": "\C-z\C-r\C-z"'
|
||||
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
|
||||
fi
|
||||
else
|
||||
# We'd usually use "\e" to enter vi-movement-mode so we can do our magic,
|
||||
# but this incurs a very noticeable delay of a half second or so,
|
||||
# because many other commands start with "\e".
|
||||
# Instead, we bind an unused key, "\C-x\C-a",
|
||||
# to also enter vi-movement-mode,
|
||||
# and then use that thereafter.
|
||||
# (We imagine that "\C-x\C-a" is relatively unlikely to be in use.)
|
||||
bind '"\C-x\C-a": vi-movement-mode'
|
||||
|
||||
bind '"\C-x\C-e": shell-expand-line'
|
||||
bind '"\C-x\C-r": redraw-current-line'
|
||||
bind '"\C-x^": history-expand-line'
|
||||
|
||||
# CTRL-T - Paste the selected file path into the command line
|
||||
# - FIXME: Selected items are attached to the end regardless of cursor position
|
||||
if [ $BASH_VERSINFO -gt 3 ]; then
|
||||
bind -x '"\C-t": "fzf-file-widget"'
|
||||
elif __fzf_use_tmux__; then
|
||||
bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select_tmux__`\C-x\C-e\C-x\C-a0P$xa"'
|
||||
else
|
||||
bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select__`\C-x\C-e\C-x\C-a0Px$a \C-x\C-r\C-x\C-axa "'
|
||||
if [[ ${FZF_CTRL_T_COMMAND-x} != "" ]]; then
|
||||
bind -m emacs-standard -x '"\C-t": fzf-file-widget'
|
||||
bind -m vi-command -x '"\C-t": fzf-file-widget'
|
||||
bind -m vi-insert -x '"\C-t": fzf-file-widget'
|
||||
fi
|
||||
bind -m vi-command '"\C-t": "i\C-t"'
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
bind '"\C-r": "\C-x\C-addi`__fzf_history__`\C-x\C-e\C-x\C-r\C-x^\C-x\C-a$a"'
|
||||
bind -m vi-command '"\C-r": "i\C-r"'
|
||||
|
||||
# ALT-C - cd into the selected directory
|
||||
bind '"\ec": "\C-x\C-addi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"'
|
||||
bind -m vi-command '"\ec": "ddi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"'
|
||||
if [[ ${FZF_CTRL_R_COMMAND-x} != "" ]]; then
|
||||
if [[ -n ${FZF_CTRL_R_COMMAND-} ]]; then
|
||||
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
|
||||
fi
|
||||
bind -m emacs-standard -x '"\C-r": __fzf_history__'
|
||||
bind -m vi-command -x '"\C-r": __fzf_history__'
|
||||
bind -m vi-insert -x '"\C-r": __fzf_history__'
|
||||
fi
|
||||
fi
|
||||
|
||||
# ALT-C - cd into the selected directory
|
||||
if [[ ${FZF_ALT_C_COMMAND-x} != "" ]]; then
|
||||
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\C-\e(\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d\C-y\ey\C-_"'
|
||||
bind -m vi-command '"\ec": "\C-z\ec\C-z"'
|
||||
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
|
||||
fi
|
||||
#----END shfmt
|
||||
|
||||
fi
|
||||
|
||||
+186
-113
@@ -1,146 +1,219 @@
|
||||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ key-bindings.fish
|
||||
#
|
||||
# - $FZF_CTRL_T_COMMAND
|
||||
# - $FZF_CTRL_T_OPTS
|
||||
# - $FZF_CTRL_R_COMMAND
|
||||
# - $FZF_CTRL_R_OPTS
|
||||
# - $FZF_ALT_C_COMMAND
|
||||
# - $FZF_ALT_C_OPTS
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
function fzf_key_bindings
|
||||
|
||||
# Store current token in $dir as root for the 'find' command
|
||||
function fzf-file-widget -d "List files and folders"
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
set -l dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
|
||||
# "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not
|
||||
# $dir itself, even if hidden.
|
||||
set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND "
|
||||
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
|
||||
-o -type f -print \
|
||||
-o -type d -print \
|
||||
-o -type l -print 2> /dev/null | sed 's@^\./@@'"
|
||||
|
||||
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS"
|
||||
eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
|
||||
end
|
||||
if [ -z "$result" ]
|
||||
commandline -f repaint
|
||||
return
|
||||
else
|
||||
# Remove last token from commandline.
|
||||
commandline -t ""
|
||||
end
|
||||
for i in $result
|
||||
commandline -it -- (string escape $i)
|
||||
commandline -it -- ' '
|
||||
end
|
||||
commandline -f repaint
|
||||
# The oldest supported fish version is 3.4.0. For this message being able to be
|
||||
# displayed on older versions, the command substitution syntax $() should not
|
||||
# be used anywhere in the script, otherwise the source command will fail.
|
||||
if string match -qr -- '^[12]\\.|^3\\.[0-3]' $version
|
||||
echo "fzf key bindings script requires fish version 3.4.0 or newer." >&2
|
||||
return 1
|
||||
else if not command -q fzf
|
||||
echo "fzf was not found in path." >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
function fzf-history-widget -d "Show command history"
|
||||
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m"
|
||||
|
||||
set -l FISH_MAJOR (echo $version | cut -f1 -d.)
|
||||
set -l FISH_MINOR (echo $version | cut -f2 -d.)
|
||||
|
||||
# history's -z flag is needed for multi-line support.
|
||||
# history's -z flag was added in fish 2.4.0, so don't use it for versions
|
||||
# before 2.4.0.
|
||||
if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ];
|
||||
history -z | eval (__fzfcmd) --read0 --print0 -q '(commandline)' | read -lz result
|
||||
and commandline -- $result
|
||||
else
|
||||
history | eval (__fzfcmd) -q '(commandline)' | read -l result
|
||||
and commandline -- $result
|
||||
end
|
||||
end
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
function fzf-cd-widget -d "Change directory"
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
set -l dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
|
||||
set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND "
|
||||
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
|
||||
-o -type d -print 2> /dev/null | sed 's@^\./@@'"
|
||||
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"
|
||||
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
|
||||
|
||||
if [ -n "$result" ]
|
||||
cd $result
|
||||
|
||||
# Remove last token from commandline.
|
||||
commandline -t ""
|
||||
end
|
||||
end
|
||||
|
||||
commandline -f repaint
|
||||
function __fzf_defaults
|
||||
# $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $argv[2..]: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||
string join ' ' -- \
|
||||
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
|
||||
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
|
||||
$FZF_DEFAULT_OPTS $argv[2..]
|
||||
end
|
||||
|
||||
function __fzfcmd
|
||||
set -q FZF_TMUX; or set FZF_TMUX 0
|
||||
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
|
||||
if [ $FZF_TMUX -eq 1 ]
|
||||
echo "fzf-tmux -d$FZF_TMUX_HEIGHT"
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||
if test -n "$FZF_TMUX_OPTS"
|
||||
echo "fzf-tmux $FZF_TMUX_OPTS -- "
|
||||
else if test "$FZF_TMUX" = "1"
|
||||
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
|
||||
else
|
||||
echo "fzf"
|
||||
end
|
||||
end
|
||||
|
||||
bind \ct fzf-file-widget
|
||||
bind \cr fzf-history-widget
|
||||
bind \ec fzf-cd-widget
|
||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||
set -l fzf_query ''
|
||||
set -l prefix ''
|
||||
set -l dir '.'
|
||||
|
||||
if bind -M insert > /dev/null 2>&1
|
||||
bind -M insert \ct fzf-file-widget
|
||||
bind -M insert \cr fzf-history-widget
|
||||
bind -M insert \ec fzf-cd-widget
|
||||
end
|
||||
set -l -- match_regex '(?<fzf_query>[\\s\\S]*?(?=\\n?$)$)'
|
||||
set -l -- prefix_regex '^-[^\\s=]+=|^-(?!-)\\S'
|
||||
|
||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath and rest of token'
|
||||
# eval is used to do shell expansion on paths
|
||||
set -l commandline (eval "printf '%s' "(commandline -t))
|
||||
# Don't use option prefix if " -- " is preceded.
|
||||
string match -qv -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
|
||||
and set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
|
||||
|
||||
if [ -z $commandline ]
|
||||
# Default to current directory with no --query
|
||||
set dir '.'
|
||||
set fzf_query ''
|
||||
# Set $prefix and expanded $fzf_query with preserved trailing newlines.
|
||||
if string match -qr -- '^\\d\\d+|^[4-9]' $version
|
||||
# fish v4.0.0 and newer
|
||||
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
|
||||
else
|
||||
set dir (__fzf_get_dir $commandline)
|
||||
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\\(?=~)|\\\\(?=\\$\\w)' '')
|
||||
end
|
||||
|
||||
if [ "$dir" = "." -a (string sub -l 1 $commandline) != '.' ]
|
||||
# if $dir is "." but commandline is not a relative path, this means no file path found
|
||||
set fzf_query $commandline
|
||||
if test -n "$fzf_query"
|
||||
# Normalize path in $fzf_query, set $dir to the longest existing directory.
|
||||
if string match -qr -- '^\\d\\d+|^4|^3\\.[5-9]' $version
|
||||
# fish v3.5.0 and newer
|
||||
set -- fzf_query (path normalize -- $fzf_query)
|
||||
set -- dir $fzf_query
|
||||
while not path is -d $dir
|
||||
set -- dir (path dirname $dir)
|
||||
end
|
||||
else
|
||||
# Also remove trailing slash after dir, to "split" input properly
|
||||
set fzf_query (string replace -r "^$dir/?" '' "$commandline")
|
||||
string match -q -r -- '(?<fzf_query>^[\\s\\S]*?(?=\\n?$)$)' \
|
||||
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\\n)$' '' $fzf_query | string collect -N)
|
||||
set -- dir $fzf_query
|
||||
while not test -d "$dir"
|
||||
set -- dir (dirname -z -- "$dir" | string split0)
|
||||
end
|
||||
end
|
||||
|
||||
if not string match -q -- '.' $dir; or string match -qr -- '^\\.(/|$)' $fzf_query
|
||||
# Strip $dir from $fzf_query - preserve trailing newlines.
|
||||
if string match -qr -- '^\\d\\d+|^[4-9]' $version
|
||||
# fish v4.0.0 and newer
|
||||
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\\s\\S]*)' $fzf_query
|
||||
else
|
||||
string match -q -r -- '^/?(?<fzf_query>[\\s\\S]*?(?=\\n?$)$)' \
|
||||
(string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
echo $dir
|
||||
echo $fzf_query
|
||||
string escape -n -- "$dir" "$fzf_query" "$prefix"
|
||||
end
|
||||
|
||||
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
|
||||
set dir $argv
|
||||
# Store current token in $dir as root for the 'find' command
|
||||
function fzf-file-widget -d "List files and folders"
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
set -lx dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
|
||||
# Strip all trailing slashes. Ignore if $dir is root dir (/)
|
||||
if [ (string length $dir) -gt 1 ]
|
||||
set dir (string replace -r '/*$' '' $dir)
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
|
||||
"--reverse --walker=file,dir,follow,hidden --scheme=path" \
|
||||
"--multi $FZF_CTRL_T_OPTS --print0")
|
||||
|
||||
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
|
||||
set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
|
||||
and commandline -rt -- (string join -- ' ' $prefix(string escape -n -- $result))' '
|
||||
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
function fzf-history-widget -d "Show command history"
|
||||
set -l -- command_line (commandline)
|
||||
set -l -- current_line (commandline -L)
|
||||
set -l -- total_lines (count $command_line)
|
||||
set -l -- fzf_query (string escape -- $command_line[$current_line])
|
||||
|
||||
set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults '' \
|
||||
'--with-nth=2.. --nth=2..,.. --scheme=history --multi --no-multi-line' \
|
||||
'--no-wrap --wrap-sign="\t\t\t↳ " --preview-wrap-sign="↳ " --freeze-left=1' \
|
||||
'--bind="alt-enter:become(set -g fzf_temp {+sf3..}; string join0 -- (string split0 -- <$fzf_temp | fish_indent -i); unlink $fzf_temp &>/dev/null)"' \
|
||||
'--bind="alt-t:change-with-nth(1,3..|3..|2..)"' \
|
||||
'--bind="shift-delete:execute-silent(eval builtin history delete -Ce -- (string escape -n -- (string split0 -- <{+sf3..})))+reload(eval $FZF_DEFAULT_COMMAND)"' \
|
||||
"--bind=ctrl-r:toggle-sort,alt-r:toggle-raw --highlight-line $FZF_CTRL_R_OPTS" \
|
||||
'--accept-nth=3.. --delimiter="\t" --tabstop=4 --read0 --print0 --with-shell='(status fish-path)\\ -c)
|
||||
|
||||
# Add dynamic preview options if preview command isn't already set by user
|
||||
if string match -qvr -- '--preview[= ]' "$FZF_DEFAULT_OPTS"
|
||||
# Prepend the options to allow user overrides
|
||||
set -p -- FZF_DEFAULT_OPTS \
|
||||
'--bind="focus,multi,resize:bg-transform:if test \\"$FZF_COLUMNS\\" -gt 100 -a \\\\( \\"$FZF_SELECT_COUNT\\" -gt 0 -o \\\\( -z \\"$FZF_WRAP\\" -a (string join0 -- <{f3..} | string length) -gt (math $FZF_COLUMNS - (switch $FZF_WITH_NTH; case 2..; echo 13; case 1,3..; echo 25; case 3..; echo 1; end)) \\\\) -o (string split0 -- <{sf3..} | fish_indent | count) -gt 1 \\\\); echo show-preview; else; echo hide-preview; end"' \
|
||||
'--preview="test \\"$FZF_SELECT_COUNT\\" -gt 0; and string split0 -- <{+sf3..} | fish_indent (string match -q -- 3.\\\\* $version; or echo -- --only-indent) --ansi; and echo -n \\\\n; string collect -- \\\\#\\\\ {1} (string split0 -- <{sf3..}) | fish_indent --ansi"' \
|
||||
'--preview-window="right,50%,wrap-word,follow,info,hidden"'
|
||||
end
|
||||
|
||||
# Iteratively check if dir exists and strip tail end of path
|
||||
while [ ! -d "$dir" ]
|
||||
# If path is absolute, this can keep going until ends up at /
|
||||
# If path is relative, this can keep going until entire input is consumed, dirname returns "."
|
||||
set dir (dirname "$dir")
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
|
||||
set -lx -- FZF_DEFAULT_COMMAND 'builtin history -z'
|
||||
|
||||
# Enable syntax highlighting colors on fish v4.3.3 and newer
|
||||
if string match -qr -- '^\\d\\d+|^4\\.[4-9]|^4\\.3\\.[3-9]' $version
|
||||
set -a -- FZF_DEFAULT_OPTS '--ansi'
|
||||
set -a -- FZF_DEFAULT_COMMAND '--color=always --show-time=(set_color $fish_color_comment)"%F %a %T%t%s%t"(set_color $fish_color_normal)'
|
||||
else
|
||||
set -a -- FZF_DEFAULT_COMMAND '--show-time="%F %a %T%t%s%t"'
|
||||
end
|
||||
|
||||
echo $dir
|
||||
# Merge history from other sessions before searching
|
||||
test -z "$fish_private_mode"; and builtin history merge
|
||||
|
||||
if set -l result (eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --query=$fzf_query | string split0)
|
||||
if test "$total_lines" -eq 1
|
||||
commandline -- $result
|
||||
else
|
||||
set -l a (math $current_line - 1)
|
||||
set -l b (math $current_line + 1)
|
||||
commandline -- $command_line[1..$a] $result
|
||||
commandline -a -- '' $command_line[$b..-1]
|
||||
end
|
||||
end
|
||||
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
function fzf-cd-widget -d "Change directory"
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
set -lx dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
|
||||
"--reverse --walker=dir,follow,hidden --scheme=path" \
|
||||
"$FZF_ALT_C_OPTS --no-multi --print0")
|
||||
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
|
||||
|
||||
if set -l result (eval (__fzfcmd) --query=$fzf_query --walker-root=$dir | string split0)
|
||||
cd -- $result
|
||||
commandline -rt -- $prefix
|
||||
end
|
||||
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
if not set -q FZF_CTRL_R_COMMAND; or test -n "$FZF_CTRL_R_COMMAND"
|
||||
if test -n "$FZF_CTRL_R_COMMAND"
|
||||
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
|
||||
end
|
||||
bind \cr fzf-history-widget
|
||||
bind -M insert \cr fzf-history-widget
|
||||
end
|
||||
|
||||
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
|
||||
bind \ct fzf-file-widget
|
||||
bind -M insert \ct fzf-file-widget
|
||||
end
|
||||
|
||||
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
|
||||
bind \ec fzf-cd-widget
|
||||
bind -M insert \ec fzf-cd-widget
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# Run setup
|
||||
fzf_key_bindings
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ key-bindings.nu
|
||||
#
|
||||
# - $FZF_TMUX (default: 0)
|
||||
# - $FZF_TMUX_OPTS
|
||||
# - $FZF_TMUX_HEIGHT (default: 40%)
|
||||
# - $FZF_CTRL_T_COMMAND (set to "" to disable)
|
||||
# - $FZF_CTRL_T_OPTS
|
||||
# - $FZF_CTRL_R_COMMAND (set to "" to disable)
|
||||
# - $FZF_CTRL_R_OPTS
|
||||
# - $FZF_ALT_C_COMMAND (set to "" to disable)
|
||||
# - $FZF_ALT_C_OPTS
|
||||
|
||||
# Code provided by @igor-ramazanov
|
||||
# Source: https://github.com/junegunn/fzf/issues/4122#issuecomment-2607368316
|
||||
|
||||
# Merge default options in the same order as bash/zsh:
|
||||
# 1. --height, --min-height, --bind=ctrl-z:ignore, $prepend
|
||||
# 2. $FZF_DEFAULT_OPTS_FILE contents
|
||||
# 3. $FZF_DEFAULT_OPTS, $append
|
||||
def __fzf_defaults [prepend: string, append: string]: nothing -> string {
|
||||
let base = $"--height ($env.FZF_TMUX_HEIGHT? | default '40%') --min-height 20+ --bind=ctrl-z:ignore ($prepend)"
|
||||
let opts_file = if ($env.FZF_DEFAULT_OPTS_FILE? | default '' | is-not-empty) {
|
||||
try { open --raw ($env.FZF_DEFAULT_OPTS_FILE) | str trim } catch { '' }
|
||||
} else {
|
||||
''
|
||||
}
|
||||
let default_opts = $env.FZF_DEFAULT_OPTS? | default ''
|
||||
$"($base) ($opts_file) ($default_opts) ($append)" | str trim
|
||||
}
|
||||
|
||||
# Return the fzf command to use: fzf-tmux when inside tmux and
|
||||
# FZF_TMUX is enabled or FZF_TMUX_OPTS is set, plain fzf otherwise.
|
||||
def __fzfcmd []: nothing -> list<string> {
|
||||
let in_tmux = ($env.TMUX_PANE? | default '' | into string | is-not-empty)
|
||||
if $in_tmux {
|
||||
let fzf_tmux = ($env.FZF_TMUX? | default 0 | into string)
|
||||
let fzf_tmux_opts = ($env.FZF_TMUX_OPTS? | default '' | into string)
|
||||
if ($fzf_tmux != '0') or ($fzf_tmux_opts | is-not-empty) {
|
||||
let opts = if ($fzf_tmux_opts | is-not-empty) { $fzf_tmux_opts } else { $"-d($env.FZF_TMUX_HEIGHT? | default '40%')" }
|
||||
return ['fzf-tmux' ...(($opts | split row ' ' | where { $in != '' })) '--']
|
||||
}
|
||||
}
|
||||
['fzf']
|
||||
}
|
||||
|
||||
|
||||
export-env {
|
||||
$env.FZF_CTRL_T_OPTS = $env.FZF_CTRL_T_OPTS? | default ""
|
||||
$env.FZF_CTRL_R_OPTS = $env.FZF_CTRL_R_OPTS? | default ""
|
||||
$env.FZF_ALT_C_OPTS = $env.FZF_ALT_C_OPTS? | default ""
|
||||
}
|
||||
|
||||
# Directories
|
||||
const alt_c = {
|
||||
name: fzf_dirs
|
||||
modifier: alt
|
||||
keycode: char_c
|
||||
mode: [emacs, vi_normal, vi_insert]
|
||||
event: [
|
||||
{
|
||||
send: executehostcommand
|
||||
cmd: "
|
||||
let fzf_opts = (__fzf_defaults '--reverse --walker=dir,follow,hidden --scheme=path' $'($env.FZF_ALT_C_OPTS) +m');
|
||||
let fzfcmd = (__fzfcmd);
|
||||
let fzf_args = ($fzfcmd | skip 1);
|
||||
let alt_c_cmd = ($env.FZF_ALT_C_COMMAND? | default null);
|
||||
let result = if ($alt_c_cmd == null) or ($alt_c_cmd | is-empty) {
|
||||
with-env { FZF_DEFAULT_OPTS: $fzf_opts, FZF_DEFAULT_OPTS_FILE: '' } { ^($fzfcmd | first) ...$fzf_args }
|
||||
} else {
|
||||
let fzf_cmd_str = ($fzfcmd | str join ' ');
|
||||
let sh_cmd = [$alt_c_cmd '|' $fzf_cmd_str] | str join ' ';
|
||||
with-env { FZF_DEFAULT_OPTS: $fzf_opts, FZF_DEFAULT_OPTS_FILE: '' } { ^sh -c $sh_cmd }
|
||||
};
|
||||
if ($result | is-not-empty) { cd $result };
|
||||
"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# History
|
||||
const ctrl_r = {
|
||||
name: fzf_history
|
||||
modifier: control
|
||||
keycode: char_r
|
||||
mode: [emacs, vi_insert, vi_normal]
|
||||
event: [
|
||||
{
|
||||
send: executehostcommand
|
||||
cmd: "commandline edit --replace (
|
||||
let fzf_opts = (__fzf_defaults '' $'--scheme=history --bind=ctrl-r:toggle-sort --wrap-sign \"\t↳ \" --highlight-line ($env.FZF_CTRL_R_OPTS) +m --read0');
|
||||
let fzfcmd = (__fzfcmd);
|
||||
let fzf_args = ($fzfcmd | skip 1);
|
||||
# reverse | uniq: show most recent first, deduplicate keeping the latest.
|
||||
# Nushell's `history` loads the full history as an in-memory table
|
||||
# (bounded by $env.config.history.max_size, default 100,000), so
|
||||
# reverse and uniq run on an already-materialized list. This is O(n)
|
||||
# but acceptable for typical history sizes; unlike bash/zsh `fc -r`,
|
||||
# there is no streaming primitive that would let fzf show the latest
|
||||
# entries before the full list is consumed.
|
||||
history
|
||||
| get command
|
||||
| reverse
|
||||
| uniq
|
||||
| str join (char -i 0)
|
||||
| with-env { FZF_DEFAULT_OPTS: $fzf_opts, FZF_DEFAULT_OPTS_FILE: '' } { ^($fzfcmd | first) ...$fzf_args --query (commandline) }
|
||||
| decode utf-8
|
||||
| str trim
|
||||
)"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Files
|
||||
const ctrl_t = {
|
||||
name: fzf_files
|
||||
modifier: control
|
||||
keycode: char_t
|
||||
mode: [emacs, vi_normal, vi_insert]
|
||||
event: [
|
||||
{
|
||||
send: executehostcommand
|
||||
cmd: "
|
||||
let fzf_opts = (__fzf_defaults '--reverse --walker=file,dir,follow,hidden --scheme=path' $'($env.FZF_CTRL_T_OPTS) -m');
|
||||
let fzfcmd = (__fzfcmd);
|
||||
let fzf_args = ($fzfcmd | skip 1);
|
||||
let ctrl_t_cmd = ($env.FZF_CTRL_T_COMMAND? | default null);
|
||||
let result = if ($ctrl_t_cmd == null) or ($ctrl_t_cmd | is-empty) {
|
||||
with-env { FZF_DEFAULT_OPTS: $fzf_opts, FZF_DEFAULT_OPTS_FILE: '' } { ^($fzfcmd | first) ...$fzf_args }
|
||||
} else {
|
||||
let fzf_cmd_str = ($fzfcmd | str join ' ');
|
||||
let sh_cmd = [$ctrl_t_cmd '|' $fzf_cmd_str] | str join ' ';
|
||||
with-env { FZF_DEFAULT_OPTS: $fzf_opts, FZF_DEFAULT_OPTS_FILE: '' } { ^sh -c $sh_cmd }
|
||||
};
|
||||
let result = ($result | str replace --all (char newline) ' ' | str trim);
|
||||
commandline edit --append $result;
|
||||
commandline set-cursor --end
|
||||
"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Helper to check if a binding is enabled. A binding is disabled when
|
||||
# the corresponding *_COMMAND variable is explicitly set to "".
|
||||
# When not defined (null), the binding is enabled (using fzf's built-in walker).
|
||||
def __fzf_binding_enabled [var_name: string]: nothing -> bool {
|
||||
let val = ($env | get -o $var_name)
|
||||
# null = not defined = enabled; "" = explicitly disabled
|
||||
$val == null or ($val | into string | is-not-empty)
|
||||
}
|
||||
|
||||
# Update the $env.config
|
||||
export-env {
|
||||
let fzf_names = ['fzf_files', 'fzf_dirs', 'fzf_history']
|
||||
# Filter out any existing fzf bindings, then re-add the enabled ones.
|
||||
# This allows re-sourcing to update bindings (e.g. after changing
|
||||
# FZF_CTRL_T_COMMAND) without creating duplicates.
|
||||
mut bindings = ($env.config.keybindings | where { |kb| $kb.name not-in $fzf_names })
|
||||
if (__fzf_binding_enabled 'FZF_ALT_C_COMMAND') { $bindings = ($bindings | append $alt_c) }
|
||||
if (__fzf_binding_enabled 'FZF_CTRL_R_COMMAND') { $bindings = ($bindings | append $ctrl_r) }
|
||||
if (__fzf_binding_enabled 'FZF_CTRL_T_COMMAND') { $bindings = ($bindings | append $ctrl_t) }
|
||||
$env.config.keybindings = $bindings
|
||||
}
|
||||
+156
-45
@@ -1,86 +1,197 @@
|
||||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ key-bindings.zsh
|
||||
#
|
||||
# - $FZF_CTRL_T_COMMAND
|
||||
# - $FZF_CTRL_T_OPTS
|
||||
# - $FZF_CTRL_R_COMMAND
|
||||
# - $FZF_CTRL_R_OPTS
|
||||
# - $FZF_ALT_C_COMMAND
|
||||
# - $FZF_ALT_C_OPTS
|
||||
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
if [[ $- == *i* ]]; then
|
||||
|
||||
# The code at the top and the bottom of this file is the same as in completion.zsh.
|
||||
# Refer to that file for explanation.
|
||||
if 'zmodload' 'zsh/parameter' 2>'/dev/null' && (( ${+options} )); then
|
||||
__fzf_key_bindings_options="options=(${(j: :)${(kv)options[@]}})"
|
||||
else
|
||||
() {
|
||||
__fzf_key_bindings_options="setopt"
|
||||
'local' '__fzf_opt'
|
||||
for __fzf_opt in "${(@)${(@f)$(set -o)}%% *}"; do
|
||||
if [[ -o "$__fzf_opt" ]]; then
|
||||
__fzf_key_bindings_options+=" -o $__fzf_opt"
|
||||
else
|
||||
__fzf_key_bindings_options+=" +o $__fzf_opt"
|
||||
fi
|
||||
done
|
||||
}
|
||||
fi
|
||||
|
||||
'builtin' 'emulate' 'zsh' && 'builtin' 'setopt' 'no_aliases'
|
||||
|
||||
{
|
||||
if [[ -o interactive ]]; then
|
||||
|
||||
#----BEGIN INCLUDE common.sh
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
|
||||
__fzf_defaults() {
|
||||
builtin printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
builtin printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
if [[ -z ${__fzf_awk-} ]]; then
|
||||
__fzf_awk=awk
|
||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||
__fzf_awk=/usr/xpg4/bin/awk
|
||||
elif command -v mawk > /dev/null 2>&1; then
|
||||
local n x y z d
|
||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] &&
|
||||
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
|
||||
((d >= 20230302)) 2> /dev/null &&
|
||||
__fzf_awk=mawk
|
||||
fi
|
||||
fi
|
||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||
}
|
||||
#----END INCLUDE
|
||||
|
||||
# CTRL-T - Paste the selected file path(s) into the command line
|
||||
__fsel() {
|
||||
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type f -print \
|
||||
-o -type d -print \
|
||||
-o -type l -print 2> /dev/null | cut -b3-"}"
|
||||
__fzf_select() {
|
||||
setopt localoptions pipefail no_aliases 2> /dev/null
|
||||
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read item; do
|
||||
echo -n "${(q)item} "
|
||||
local item
|
||||
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" < /dev/tty | while read -r item; do
|
||||
echo -n -E "${(q)item} "
|
||||
done
|
||||
local ret=$?
|
||||
echo
|
||||
return $ret
|
||||
}
|
||||
|
||||
__fzf_use_tmux__() {
|
||||
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ]
|
||||
}
|
||||
|
||||
__fzfcmd() {
|
||||
__fzf_use_tmux__ &&
|
||||
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
|
||||
[ -n "${TMUX_PANE-}" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "${FZF_TMUX_OPTS-}" ]; } &&
|
||||
echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf"
|
||||
}
|
||||
|
||||
fzf-file-widget() {
|
||||
LBUFFER="${LBUFFER}$(__fsel)"
|
||||
LBUFFER="${LBUFFER}$(__fzf_select)"
|
||||
local ret=$?
|
||||
zle reset-prompt
|
||||
return $ret
|
||||
}
|
||||
zle -N fzf-file-widget
|
||||
bindkey '^T' fzf-file-widget
|
||||
|
||||
# Ensure precmds are run after cd
|
||||
fzf-redraw-prompt() {
|
||||
local precmd
|
||||
for precmd in $precmd_functions; do
|
||||
$precmd
|
||||
done
|
||||
zle reset-prompt
|
||||
}
|
||||
zle -N fzf-redraw-prompt
|
||||
if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
|
||||
zle -N fzf-file-widget
|
||||
bindkey -M emacs '^T' fzf-file-widget
|
||||
bindkey -M vicmd '^T' fzf-file-widget
|
||||
bindkey -M viins '^T' fzf-file-widget
|
||||
fi
|
||||
|
||||
# ALT-C - cd into the selected directory
|
||||
fzf-cd-widget() {
|
||||
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type d -print 2> /dev/null | cut -b3-"}"
|
||||
setopt localoptions pipefail no_aliases 2> /dev/null
|
||||
local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m)"
|
||||
local dir="$(
|
||||
FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path" "${FZF_ALT_C_OPTS-} +m") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) < /dev/tty)"
|
||||
if [[ -z "$dir" ]]; then
|
||||
zle redisplay
|
||||
return 0
|
||||
fi
|
||||
cd "$dir"
|
||||
unset dir # ensure this doesn't end up appearing in prompt expansion
|
||||
zle push-line # Clear buffer. Auto-restored on next prompt.
|
||||
BUFFER="builtin cd -- ${(q)dir:a}"
|
||||
zle accept-line
|
||||
local ret=$?
|
||||
zle fzf-redraw-prompt
|
||||
unset dir # ensure this doesn't end up appearing in prompt expansion
|
||||
zle reset-prompt
|
||||
return $ret
|
||||
}
|
||||
zle -N fzf-cd-widget
|
||||
bindkey '\ec' fzf-cd-widget
|
||||
if [[ "${FZF_ALT_C_COMMAND-x}" != "" ]]; then
|
||||
zle -N fzf-cd-widget
|
||||
bindkey -M emacs '\ec' fzf-cd-widget
|
||||
bindkey -M vicmd '\ec' fzf-cd-widget
|
||||
bindkey -M viins '\ec' fzf-cd-widget
|
||||
fi
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
fzf-history-widget() {
|
||||
local selected num
|
||||
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
|
||||
selected=( $(fc -rl 1 |
|
||||
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) )
|
||||
local selected extracted_with_perl=0
|
||||
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases no_glob no_sh_glob no_ksharrays extendedglob 2> /dev/null
|
||||
# Ensure the module is loaded if not already, and the required features, such
|
||||
# as the associative 'history' array, which maps event numbers to full history
|
||||
# lines, are set. Also, make sure Perl is installed for multi-line output.
|
||||
if zmodload -F zsh/parameter p:{commands,history} 2>/dev/null && (( ${+commands[perl]} )); then
|
||||
selected="$(printf '%s\t%s\000' "${(kv)history[@]}" |
|
||||
perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\t(.*)/s, $1)}++) { s/\n/\n\t/g; print; }' |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '\t↳ ' --highlight-line --multi ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} --read0") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
||||
extracted_with_perl=1
|
||||
else
|
||||
selected="$(fc -rl 1 | __fzf_exec_awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '\t↳ ' --highlight-line --multi ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER}") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
||||
fi
|
||||
local ret=$?
|
||||
local -a cmds
|
||||
# Avoid leaking auto assigned values when using backreferences '(#b)'
|
||||
local -a mbegin mend match
|
||||
if [ -n "$selected" ]; then
|
||||
num=$selected[1]
|
||||
if [ -n "$num" ]; then
|
||||
zle vi-fetch-history -n $num
|
||||
# Heuristic to check if the selected value is from history or a custom query
|
||||
if ((( extracted_with_perl )) && [[ $selected == <->$'\t'* ]]) ||
|
||||
((( ! extracted_with_perl )) && [[ $selected == [[:blank:]]#<->( |\* )* ]]); then
|
||||
# Split at newlines
|
||||
for line in ${(ps:\n:)selected}; do
|
||||
if (( extracted_with_perl )); then
|
||||
if [[ $line == (#b)(<->)(#B)$'\t'* ]]; then
|
||||
(( ${+history[${match[1]}]} )) && cmds+=("${history[${match[1]}]}")
|
||||
fi
|
||||
elif [[ $line == [[:blank:]]#(#b)(<->)(#B)( |\* )* ]]; then
|
||||
# Avoid $history array: lags behind 'fc' on foreign commands (*)
|
||||
# https://zsh.org/mla/users/2024/msg00692.html
|
||||
# Push BUFFER onto stack; fetch and save history entry from BUFFER; restore
|
||||
zle .push-line
|
||||
zle vi-fetch-history -n ${match[1]}
|
||||
(( ${#BUFFER} )) && cmds+=("${BUFFER}")
|
||||
BUFFER=""
|
||||
zle .get-line
|
||||
fi
|
||||
done
|
||||
if (( ${#cmds[@]} )); then
|
||||
# Join by newline after stripping trailing newlines from each command
|
||||
BUFFER="${(pj:\n:)${(@)cmds%%$'\n'#}}"
|
||||
CURSOR=${#BUFFER}
|
||||
fi
|
||||
else # selected is a custom query, not from history
|
||||
LBUFFER="$selected"
|
||||
fi
|
||||
fi
|
||||
zle reset-prompt
|
||||
return $ret
|
||||
}
|
||||
zle -N fzf-history-widget
|
||||
bindkey '^R' fzf-history-widget
|
||||
|
||||
if [[ ${FZF_CTRL_R_COMMAND-x} != "" ]]; then
|
||||
if [[ -n ${FZF_CTRL_R_COMMAND-} ]]; then
|
||||
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
|
||||
fi
|
||||
zle -N fzf-history-widget
|
||||
bindkey -M emacs '^R' fzf-history-widget
|
||||
bindkey -M vicmd '^R' fzf-history-widget
|
||||
bindkey -M viins '^R' fzf-history-widget
|
||||
fi
|
||||
fi
|
||||
|
||||
} always {
|
||||
eval $__fzf_key_bindings_options
|
||||
'unset' '__fzf_key_bindings_options'
|
||||
}
|
||||
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script applies the contents of "common.sh" to the other files.
|
||||
|
||||
set -e
|
||||
|
||||
dir=${0%"${0##*/}"}
|
||||
|
||||
update() {
|
||||
{
|
||||
sed -n '1,/^#----BEGIN INCLUDE common\.sh/p' "$1"
|
||||
cat << EOF
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
EOF
|
||||
echo
|
||||
grep -v '^[[:blank:]]*#' "$dir/common.sh" # remove code comments in common.sh
|
||||
sed -n '/^#----END INCLUDE/,$p' "$1"
|
||||
} > "$1.part"
|
||||
|
||||
mv -f "$1.part" "$1"
|
||||
}
|
||||
|
||||
update "$dir/completion.bash"
|
||||
update "$dir/completion.zsh"
|
||||
update "$dir/key-bindings.bash"
|
||||
update "$dir/key-bindings.zsh"
|
||||
|
||||
# Check if --check is in ARGV
|
||||
check=0
|
||||
rest=()
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--check) check=1 ;;
|
||||
*) rest+=("$arg") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
fmt() {
|
||||
if ! grep -q "^#----BEGIN shfmt" "$1"; then
|
||||
if [[ $check == 1 ]]; then
|
||||
shfmt -d "$1"
|
||||
return $?
|
||||
else
|
||||
shfmt -w "$1"
|
||||
fi
|
||||
else
|
||||
{
|
||||
sed -n '1,/^#----BEGIN shfmt/p' "$1" | sed '$d'
|
||||
sed -n '/^#----BEGIN shfmt/,/^#----END shfmt/p' "$1" | shfmt --filename "$1"
|
||||
sed -n '/^#----END shfmt/,$p' "$1" | sed '1d'
|
||||
} > "$1.part"
|
||||
|
||||
if [[ $check == 1 ]]; then
|
||||
diff -q "$1" "$1.part"
|
||||
ret=$?
|
||||
rm -f "$1.part"
|
||||
return $ret
|
||||
fi
|
||||
|
||||
mv -f "$1.part" "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
for file in "${rest[@]}"; do
|
||||
fmt "$file" || exit $?
|
||||
done
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Junegunn Choi
|
||||
Copyright (c) 2013-2026 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
// Code generated by "stringer -type=actionType"; DO NOT EDIT.
|
||||
|
||||
package fzf
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[actIgnore-0]
|
||||
_ = x[actStart-1]
|
||||
_ = x[actClick-2]
|
||||
_ = x[actInvalid-3]
|
||||
_ = x[actBracketedPasteBegin-4]
|
||||
_ = x[actBracketedPasteEnd-5]
|
||||
_ = x[actChar-6]
|
||||
_ = x[actMouse-7]
|
||||
_ = x[actBeginningOfLine-8]
|
||||
_ = x[actAbort-9]
|
||||
_ = x[actAccept-10]
|
||||
_ = x[actAcceptNonEmpty-11]
|
||||
_ = x[actAcceptOrPrintQuery-12]
|
||||
_ = x[actBackwardChar-13]
|
||||
_ = x[actBackwardDeleteChar-14]
|
||||
_ = x[actBackwardDeleteCharEof-15]
|
||||
_ = x[actBackwardWord-16]
|
||||
_ = x[actBackwardSubWord-17]
|
||||
_ = x[actCancel-18]
|
||||
_ = x[actChangeBorderLabel-19]
|
||||
_ = x[actChangeGhost-20]
|
||||
_ = x[actChangeHeader-21]
|
||||
_ = x[actChangeHeaderLines-22]
|
||||
_ = x[actChangeFooter-23]
|
||||
_ = x[actChangeHeaderLabel-24]
|
||||
_ = x[actChangeFooterLabel-25]
|
||||
_ = x[actChangeInputLabel-26]
|
||||
_ = x[actChangeListLabel-27]
|
||||
_ = x[actChangeMulti-28]
|
||||
_ = x[actChangeNth-29]
|
||||
_ = x[actChangeWithNth-30]
|
||||
_ = x[actChangePointer-31]
|
||||
_ = x[actChangePreview-32]
|
||||
_ = x[actChangePreviewLabel-33]
|
||||
_ = x[actChangePreviewWindow-34]
|
||||
_ = x[actChangePrompt-35]
|
||||
_ = x[actChangeQuery-36]
|
||||
_ = x[actClearScreen-37]
|
||||
_ = x[actClearQuery-38]
|
||||
_ = x[actClearSelection-39]
|
||||
_ = x[actClose-40]
|
||||
_ = x[actDeleteChar-41]
|
||||
_ = x[actDeleteCharEof-42]
|
||||
_ = x[actEndOfLine-43]
|
||||
_ = x[actFatal-44]
|
||||
_ = x[actForwardChar-45]
|
||||
_ = x[actForwardWord-46]
|
||||
_ = x[actForwardSubWord-47]
|
||||
_ = x[actKillLine-48]
|
||||
_ = x[actKillWord-49]
|
||||
_ = x[actKillSubWord-50]
|
||||
_ = x[actUnixLineDiscard-51]
|
||||
_ = x[actUnixWordRubout-52]
|
||||
_ = x[actYank-53]
|
||||
_ = x[actBackwardKillWord-54]
|
||||
_ = x[actBackwardKillSubWord-55]
|
||||
_ = x[actSelectAll-56]
|
||||
_ = x[actDeselectAll-57]
|
||||
_ = x[actToggle-58]
|
||||
_ = x[actToggleSearch-59]
|
||||
_ = x[actToggleAll-60]
|
||||
_ = x[actToggleDown-61]
|
||||
_ = x[actToggleUp-62]
|
||||
_ = x[actToggleIn-63]
|
||||
_ = x[actToggleOut-64]
|
||||
_ = x[actToggleTrack-65]
|
||||
_ = x[actToggleTrackCurrent-66]
|
||||
_ = x[actToggleHeader-67]
|
||||
_ = x[actToggleWrap-68]
|
||||
_ = x[actToggleWrapWord-69]
|
||||
_ = x[actToggleMultiLine-70]
|
||||
_ = x[actToggleHscroll-71]
|
||||
_ = x[actToggleRaw-72]
|
||||
_ = x[actEnableRaw-73]
|
||||
_ = x[actDisableRaw-74]
|
||||
_ = x[actTrackCurrent-75]
|
||||
_ = x[actToggleInput-76]
|
||||
_ = x[actHideInput-77]
|
||||
_ = x[actShowInput-78]
|
||||
_ = x[actUntrackCurrent-79]
|
||||
_ = x[actDown-80]
|
||||
_ = x[actDownMatch-81]
|
||||
_ = x[actUp-82]
|
||||
_ = x[actUpMatch-83]
|
||||
_ = x[actPageUp-84]
|
||||
_ = x[actPageDown-85]
|
||||
_ = x[actPosition-86]
|
||||
_ = x[actHalfPageUp-87]
|
||||
_ = x[actHalfPageDown-88]
|
||||
_ = x[actOffsetUp-89]
|
||||
_ = x[actOffsetDown-90]
|
||||
_ = x[actOffsetMiddle-91]
|
||||
_ = x[actJump-92]
|
||||
_ = x[actJumpAccept-93]
|
||||
_ = x[actPrintQuery-94]
|
||||
_ = x[actRefreshPreview-95]
|
||||
_ = x[actReplaceQuery-96]
|
||||
_ = x[actToggleSort-97]
|
||||
_ = x[actShowPreview-98]
|
||||
_ = x[actHidePreview-99]
|
||||
_ = x[actTogglePreview-100]
|
||||
_ = x[actTogglePreviewWrap-101]
|
||||
_ = x[actTogglePreviewWrapWord-102]
|
||||
_ = x[actTransform-103]
|
||||
_ = x[actTransformBorderLabel-104]
|
||||
_ = x[actTransformGhost-105]
|
||||
_ = x[actTransformHeader-106]
|
||||
_ = x[actTransformHeaderLines-107]
|
||||
_ = x[actTransformFooter-108]
|
||||
_ = x[actTransformHeaderLabel-109]
|
||||
_ = x[actTransformFooterLabel-110]
|
||||
_ = x[actTransformInputLabel-111]
|
||||
_ = x[actTransformListLabel-112]
|
||||
_ = x[actTransformNth-113]
|
||||
_ = x[actTransformWithNth-114]
|
||||
_ = x[actTransformPointer-115]
|
||||
_ = x[actTransformPreviewLabel-116]
|
||||
_ = x[actTransformPrompt-117]
|
||||
_ = x[actTransformQuery-118]
|
||||
_ = x[actTransformSearch-119]
|
||||
_ = x[actTrigger-120]
|
||||
_ = x[actBgTransform-121]
|
||||
_ = x[actBgTransformBorderLabel-122]
|
||||
_ = x[actBgTransformGhost-123]
|
||||
_ = x[actBgTransformHeader-124]
|
||||
_ = x[actBgTransformHeaderLines-125]
|
||||
_ = x[actBgTransformFooter-126]
|
||||
_ = x[actBgTransformHeaderLabel-127]
|
||||
_ = x[actBgTransformFooterLabel-128]
|
||||
_ = x[actBgTransformInputLabel-129]
|
||||
_ = x[actBgTransformListLabel-130]
|
||||
_ = x[actBgTransformNth-131]
|
||||
_ = x[actBgTransformWithNth-132]
|
||||
_ = x[actBgTransformPointer-133]
|
||||
_ = x[actBgTransformPreviewLabel-134]
|
||||
_ = x[actBgTransformPrompt-135]
|
||||
_ = x[actBgTransformQuery-136]
|
||||
_ = x[actBgTransformSearch-137]
|
||||
_ = x[actBgCancel-138]
|
||||
_ = x[actSearch-139]
|
||||
_ = x[actPreview-140]
|
||||
_ = x[actPreviewTop-141]
|
||||
_ = x[actPreviewBottom-142]
|
||||
_ = x[actPreviewUp-143]
|
||||
_ = x[actPreviewDown-144]
|
||||
_ = x[actPreviewPageUp-145]
|
||||
_ = x[actPreviewPageDown-146]
|
||||
_ = x[actPreviewHalfPageUp-147]
|
||||
_ = x[actPreviewHalfPageDown-148]
|
||||
_ = x[actPrevHistory-149]
|
||||
_ = x[actPrevSelected-150]
|
||||
_ = x[actPrint-151]
|
||||
_ = x[actPut-152]
|
||||
_ = x[actNextHistory-153]
|
||||
_ = x[actNextSelected-154]
|
||||
_ = x[actExecute-155]
|
||||
_ = x[actExecuteSilent-156]
|
||||
_ = x[actExecuteMulti-157]
|
||||
_ = x[actSigStop-158]
|
||||
_ = x[actBest-159]
|
||||
_ = x[actFirst-160]
|
||||
_ = x[actLast-161]
|
||||
_ = x[actReload-162]
|
||||
_ = x[actReloadSync-163]
|
||||
_ = x[actDisableSearch-164]
|
||||
_ = x[actEnableSearch-165]
|
||||
_ = x[actSelect-166]
|
||||
_ = x[actDeselect-167]
|
||||
_ = x[actUnbind-168]
|
||||
_ = x[actRebind-169]
|
||||
_ = x[actToggleBind-170]
|
||||
_ = x[actBecome-171]
|
||||
_ = x[actShowHeader-172]
|
||||
_ = x[actHideHeader-173]
|
||||
_ = x[actBell-174]
|
||||
_ = x[actExclude-175]
|
||||
_ = x[actExcludeMulti-176]
|
||||
_ = x[actAsync-177]
|
||||
}
|
||||
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeHeaderLinesactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangeWithNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleWrapWordactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTogglePreviewWrapWordactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformHeaderLinesactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformWithNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformHeaderLinesactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformWithNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync"
|
||||
|
||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 258, 267, 287, 301, 316, 336, 351, 371, 391, 410, 428, 442, 454, 470, 486, 502, 523, 545, 560, 574, 588, 601, 618, 626, 639, 655, 667, 675, 689, 703, 720, 731, 742, 756, 774, 791, 798, 817, 839, 851, 865, 874, 889, 901, 914, 925, 936, 948, 962, 983, 998, 1011, 1028, 1046, 1062, 1074, 1086, 1099, 1114, 1128, 1140, 1152, 1169, 1176, 1188, 1193, 1203, 1212, 1223, 1234, 1247, 1262, 1273, 1286, 1301, 1308, 1321, 1334, 1351, 1366, 1379, 1393, 1407, 1423, 1443, 1467, 1479, 1502, 1519, 1537, 1560, 1578, 1601, 1624, 1646, 1667, 1682, 1701, 1720, 1744, 1762, 1779, 1797, 1807, 1821, 1846, 1865, 1885, 1910, 1930, 1955, 1980, 2004, 2027, 2044, 2065, 2086, 2112, 2132, 2151, 2171, 2182, 2191, 2201, 2214, 2230, 2242, 2256, 2272, 2290, 2310, 2332, 2346, 2361, 2369, 2375, 2389, 2404, 2414, 2430, 2445, 2455, 2462, 2470, 2477, 2486, 2499, 2515, 2530, 2539, 2550, 2559, 2568, 2581, 2590, 2603, 2616, 2623, 2633, 2648, 2656}
|
||||
|
||||
func (i actionType) String() string {
|
||||
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
||||
return "actionType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _actionType_name[_actionType_index[i]:_actionType_index[i+1]]
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
# SIMD byte search: `indexByteTwo` / `lastIndexByteTwo`
|
||||
|
||||
## What these functions do
|
||||
|
||||
`indexByteTwo(s []byte, b1, b2 byte) int` -- returns the index of the
|
||||
**first** occurrence of `b1` or `b2` in `s`, or `-1`.
|
||||
|
||||
`lastIndexByteTwo(s []byte, b1, b2 byte) int` -- returns the index of the
|
||||
**last** occurrence of `b1` or `b2` in `s`, or `-1`.
|
||||
|
||||
They are used by the fuzzy matching algorithm (`algo.go`) to skip ahead
|
||||
during case-insensitive search. Instead of calling `bytes.IndexByte` twice
|
||||
(once for lowercase, once for uppercase), a single SIMD pass finds both at
|
||||
once.
|
||||
|
||||
## File layout
|
||||
|
||||
| File | Purpose |
|
||||
| ------ | --------- |
|
||||
| `indexbyte2_arm64.go` | Go declarations (`//go:noescape`) for ARM64 |
|
||||
| `indexbyte2_arm64.s` | ARM64 NEON assembly (32-byte aligned blocks, syndrome extraction) |
|
||||
| `indexbyte2_amd64.go` | Go declarations + AVX2 runtime detection for AMD64 |
|
||||
| `indexbyte2_amd64.s` | AMD64 AVX2/SSE2 assembly with CPUID dispatch |
|
||||
| `indexbyte2_other.go` | Pure Go fallback for all other architectures |
|
||||
| `indexbyte2_test.go` | Unit tests, exhaustive tests, fuzz tests, and benchmarks |
|
||||
|
||||
## How the SIMD implementations work
|
||||
|
||||
**ARM64 (NEON):**
|
||||
- Broadcasts both needle bytes into NEON registers (`VMOV`).
|
||||
- Processes 32-byte aligned chunks. For each chunk, compares all bytes
|
||||
against both needles (`VCMEQ`), ORs the results (`VORR`), and builds a
|
||||
64-bit syndrome with 2 bits per byte.
|
||||
- `indexByteTwo` uses `RBIT` + `CLZ` to find the lowest set bit (first match).
|
||||
- `lastIndexByteTwo` scans backward and uses `CLZ` on the raw syndrome to
|
||||
find the highest set bit (last match).
|
||||
- Handles alignment and partial first/last blocks with bit masking.
|
||||
- Adapted from Go's `internal/bytealg/indexbyte_arm64.s`.
|
||||
|
||||
**AMD64 (AVX2 with SSE2 fallback):**
|
||||
- At init time, `cpuHasAVX2()` checks CPUID + XGETBV for AVX2 and OS YMM
|
||||
support. The result is cached in `_useAVX2`.
|
||||
- **AVX2 path** (inputs >= 32 bytes, when available):
|
||||
- Broadcasts both needles via `VPBROADCASTB`.
|
||||
- Processes 32-byte blocks: `VPCMPEQB` against both needles, `VPOR`, then
|
||||
`VPMOVMSKB` to get a 32-bit mask.
|
||||
- 5 instructions per loop iteration (vs 7 for SSE2) at 2x the throughput.
|
||||
- `VZEROUPPER` before every return to avoid SSE/AVX transition penalties.
|
||||
- **SSE2 fallback** (inputs < 32 bytes, or CPUs without AVX2):
|
||||
- Broadcasts via `PUNPCKLBW` + `PSHUFL`.
|
||||
- Processes 16-byte blocks: `PCMPEQB`, `POR`, `PMOVMSKB`.
|
||||
- Small inputs (<16 bytes) are handled with page-boundary-safe loads.
|
||||
- Both paths use `BSFL` (forward) / `BSRL` (reverse) for bit scanning.
|
||||
- Adapted from Go's `internal/bytealg/indexbyte_amd64.s`.
|
||||
|
||||
**Fallback (other platforms):**
|
||||
- `indexByteTwo` uses two `bytes.IndexByte` calls with scope-limiting
|
||||
(search `b1` first, then limit the `b2` search to `s[:i1]`).
|
||||
- `lastIndexByteTwo` uses a simple backward for loop.
|
||||
|
||||
## Running tests
|
||||
|
||||
```bash
|
||||
# Unit + exhaustive tests
|
||||
go test ./src/algo/ -run 'TestIndexByteTwo|TestLastIndexByteTwo' -v
|
||||
|
||||
# Fuzz tests (run for 10 seconds each)
|
||||
go test ./src/algo/ -run '^$' -fuzz FuzzIndexByteTwo -fuzztime 10s
|
||||
go test ./src/algo/ -run '^$' -fuzz FuzzLastIndexByteTwo -fuzztime 10s
|
||||
|
||||
# Cross-architecture: test amd64 on an arm64 Mac (via Rosetta)
|
||||
GOARCH=amd64 go test ./src/algo/ -run 'TestIndexByteTwo|TestLastIndexByteTwo' -v
|
||||
GOARCH=amd64 go test ./src/algo/ -run '^$' -fuzz FuzzIndexByteTwo -fuzztime 10s
|
||||
GOARCH=amd64 go test ./src/algo/ -run '^$' -fuzz FuzzLastIndexByteTwo -fuzztime 10s
|
||||
```
|
||||
|
||||
## Running micro-benchmarks
|
||||
|
||||
```bash
|
||||
# All indexByteTwo / lastIndexByteTwo benchmarks
|
||||
go test ./src/algo/ -bench 'IndexByteTwo' -benchmem
|
||||
|
||||
# Specific size
|
||||
go test ./src/algo/ -bench 'IndexByteTwo_1000'
|
||||
```
|
||||
|
||||
Each benchmark compares the SIMD `asm` implementation against reference
|
||||
implementations (`2xIndexByte` using `bytes.IndexByte`, and a simple `loop`).
|
||||
|
||||
## Correctness verification
|
||||
|
||||
The assembly is verified by three layers of testing:
|
||||
|
||||
1. **Table-driven tests** -- known inputs with expected outputs.
|
||||
2. **Exhaustive tests** -- all lengths 0–256, every match position, no-match
|
||||
cases, and both-bytes-present cases, compared against a simple loop
|
||||
reference.
|
||||
3. **Fuzz tests** -- randomized inputs via `testing.F`, compared against the
|
||||
same loop reference.
|
||||
+274
-103
@@ -80,6 +80,7 @@ Scoring criteria
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
@@ -89,6 +90,10 @@ import (
|
||||
|
||||
var DEBUG bool
|
||||
|
||||
var delimiterChars = "/,:;|"
|
||||
|
||||
const whiteChars = " \t\n\v\f\r\x85\xA0"
|
||||
|
||||
func indexAt(index int, max int, forward bool) int {
|
||||
if forward {
|
||||
return index
|
||||
@@ -107,7 +112,7 @@ type Result struct {
|
||||
const (
|
||||
scoreMatch = 16
|
||||
scoreGapStart = -3
|
||||
scoreGapExtention = -1
|
||||
scoreGapExtension = -1
|
||||
|
||||
// We prefer matches at the beginning of a word, but the bonus should not be
|
||||
// too great to prevent the longer acronym matches from always winning over
|
||||
@@ -125,31 +130,93 @@ const (
|
||||
// Edge-triggered bonus for matches in camelCase words.
|
||||
// Compared to word-boundary case, they don't accompany single-character gaps
|
||||
// (e.g. FooBar vs. foo-bar), so we deduct bonus point accordingly.
|
||||
bonusCamel123 = bonusBoundary + scoreGapExtention
|
||||
bonusCamel123 = bonusBoundary + scoreGapExtension
|
||||
|
||||
// Minimum bonus point given to characters in consecutive chunks.
|
||||
// Note that bonus points for consecutive matches shouldn't have needed if we
|
||||
// used fixed match score as in the original algorithm.
|
||||
bonusConsecutive = -(scoreGapStart + scoreGapExtention)
|
||||
bonusConsecutive = -(scoreGapStart + scoreGapExtension)
|
||||
|
||||
// The first character in the typed pattern usually has more significance
|
||||
// than the rest so it's important that it appears at special positions where
|
||||
// bonus points are given. e.g. "to-go" vs. "ongoing" on "og" or on "ogo".
|
||||
// bonus points are given, e.g. "to-go" vs. "ongoing" on "og" or on "ogo".
|
||||
// The amount of the extra bonus should be limited so that the gap penalty is
|
||||
// still respected.
|
||||
bonusFirstCharMultiplier = 2
|
||||
)
|
||||
|
||||
var (
|
||||
// Extra bonus for word boundary after whitespace character or beginning of the string
|
||||
bonusBoundaryWhite int16 = bonusBoundary + 2
|
||||
|
||||
// Extra bonus for word boundary after slash, colon, semi-colon, and comma
|
||||
bonusBoundaryDelimiter int16 = bonusBoundary + 1
|
||||
|
||||
initialCharClass = charWhite
|
||||
|
||||
// A minor optimization that can give 15%+ performance boost
|
||||
asciiCharClasses [unicode.MaxASCII + 1]charClass
|
||||
|
||||
// A minor optimization that can give yet another 5% performance boost
|
||||
bonusMatrix [charNumber + 1][charNumber + 1]int16
|
||||
)
|
||||
|
||||
type charClass int
|
||||
|
||||
const (
|
||||
charNonWord charClass = iota
|
||||
charWhite charClass = iota
|
||||
charNonWord
|
||||
charDelimiter
|
||||
charLower
|
||||
charUpper
|
||||
charLetter
|
||||
charNumber
|
||||
)
|
||||
|
||||
func Init(scheme string) bool {
|
||||
switch scheme {
|
||||
case "default":
|
||||
bonusBoundaryWhite = bonusBoundary + 2
|
||||
bonusBoundaryDelimiter = bonusBoundary + 1
|
||||
case "path":
|
||||
bonusBoundaryWhite = bonusBoundary
|
||||
bonusBoundaryDelimiter = bonusBoundary + 1
|
||||
if os.PathSeparator == '/' {
|
||||
delimiterChars = "/"
|
||||
} else {
|
||||
delimiterChars = string([]rune{os.PathSeparator, '/'})
|
||||
}
|
||||
initialCharClass = charDelimiter
|
||||
case "history":
|
||||
bonusBoundaryWhite = bonusBoundary
|
||||
bonusBoundaryDelimiter = bonusBoundary
|
||||
default:
|
||||
return false
|
||||
}
|
||||
for i := 0; i <= unicode.MaxASCII; i++ {
|
||||
char := rune(i)
|
||||
c := charNonWord
|
||||
if char >= 'a' && char <= 'z' {
|
||||
c = charLower
|
||||
} else if char >= 'A' && char <= 'Z' {
|
||||
c = charUpper
|
||||
} else if char >= '0' && char <= '9' {
|
||||
c = charNumber
|
||||
} else if strings.ContainsRune(whiteChars, char) {
|
||||
c = charWhite
|
||||
} else if strings.ContainsRune(delimiterChars, char) {
|
||||
c = charDelimiter
|
||||
}
|
||||
asciiCharClasses[i] = c
|
||||
}
|
||||
for i := 0; i <= int(charNumber); i++ {
|
||||
for j := 0; j <= int(charNumber); j++ {
|
||||
bonusMatrix[i][j] = bonusFor(charClass(i), charClass(j))
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func posArray(withPos bool, len int) *[]int {
|
||||
if withPos {
|
||||
pos := make([]int, 0, len)
|
||||
@@ -174,17 +241,6 @@ func alloc32(offset int, slab *util.Slab, size int) (int, []int32) {
|
||||
return offset, make([]int32, size)
|
||||
}
|
||||
|
||||
func charClassOfAscii(char rune) charClass {
|
||||
if char >= 'a' && char <= 'z' {
|
||||
return charLower
|
||||
} else if char >= 'A' && char <= 'Z' {
|
||||
return charUpper
|
||||
} else if char >= '0' && char <= '9' {
|
||||
return charNumber
|
||||
}
|
||||
return charNonWord
|
||||
}
|
||||
|
||||
func charClassOfNonAscii(char rune) charClass {
|
||||
if unicode.IsLower(char) {
|
||||
return charLower
|
||||
@@ -194,40 +250,60 @@ func charClassOfNonAscii(char rune) charClass {
|
||||
return charNumber
|
||||
} else if unicode.IsLetter(char) {
|
||||
return charLetter
|
||||
} else if unicode.IsSpace(char) {
|
||||
return charWhite
|
||||
} else if strings.ContainsRune(delimiterChars, char) {
|
||||
return charDelimiter
|
||||
}
|
||||
return charNonWord
|
||||
}
|
||||
|
||||
func charClassOf(char rune) charClass {
|
||||
if char <= unicode.MaxASCII {
|
||||
return charClassOfAscii(char)
|
||||
return asciiCharClasses[char]
|
||||
}
|
||||
return charClassOfNonAscii(char)
|
||||
}
|
||||
|
||||
func bonusFor(prevClass charClass, class charClass) int16 {
|
||||
if prevClass == charNonWord && class != charNonWord {
|
||||
// Word boundary
|
||||
return bonusBoundary
|
||||
} else if prevClass == charLower && class == charUpper ||
|
||||
if class >= charNonWord {
|
||||
switch prevClass {
|
||||
case charWhite:
|
||||
// Word boundary after whitespace
|
||||
return bonusBoundaryWhite
|
||||
case charDelimiter:
|
||||
// Word boundary after a delimiter character
|
||||
return bonusBoundaryDelimiter
|
||||
case charNonWord:
|
||||
// Word boundary
|
||||
return bonusBoundary
|
||||
}
|
||||
}
|
||||
|
||||
if prevClass == charLower && class == charUpper ||
|
||||
prevClass != charNumber && class == charNumber {
|
||||
// camelCase letter123
|
||||
return bonusCamel123
|
||||
} else if class == charNonWord {
|
||||
}
|
||||
|
||||
switch class {
|
||||
case charNonWord, charDelimiter:
|
||||
return bonusNonWord
|
||||
case charWhite:
|
||||
return bonusBoundaryWhite
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func bonusAt(input *util.Chars, idx int) int16 {
|
||||
if idx == 0 {
|
||||
return bonusBoundary
|
||||
return bonusBoundaryWhite
|
||||
}
|
||||
return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx)))
|
||||
return bonusMatrix[charClassOf(input.Get(idx-1))][charClassOf(input.Get(idx))]
|
||||
}
|
||||
|
||||
func normalizeRune(r rune) rune {
|
||||
if r < 0x00C0 || r > 0x2184 {
|
||||
if r < 0x00C0 || r > 0xFF61 {
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -245,22 +321,15 @@ type Algo func(caseSensitive bool, normalize bool, forward bool, input *util.Cha
|
||||
|
||||
func trySkip(input *util.Chars, caseSensitive bool, b byte, from int) int {
|
||||
byteArray := input.Bytes()[from:]
|
||||
idx := bytes.IndexByte(byteArray, b)
|
||||
if idx == 0 {
|
||||
// Can't skip any further
|
||||
return from
|
||||
}
|
||||
// We may need to search for the uppercase letter again. We don't have to
|
||||
// consider normalization as we can be sure that this is an ASCII string.
|
||||
// For case-insensitive search of a letter, search for both cases in one pass
|
||||
if !caseSensitive && b >= 'a' && b <= 'z' {
|
||||
if idx > 0 {
|
||||
byteArray = byteArray[:idx]
|
||||
}
|
||||
uidx := bytes.IndexByte(byteArray, b-32)
|
||||
if uidx >= 0 {
|
||||
idx = uidx
|
||||
idx := IndexByteTwo(byteArray, b, b-32)
|
||||
if idx < 0 {
|
||||
return -1
|
||||
}
|
||||
return from + idx
|
||||
}
|
||||
idx := bytes.IndexByte(byteArray, b)
|
||||
if idx < 0 {
|
||||
return -1
|
||||
}
|
||||
@@ -276,30 +345,48 @@ func isAscii(runes []rune) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) int {
|
||||
func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) (int, int) {
|
||||
// Can't determine
|
||||
if !input.IsBytes() {
|
||||
return 0
|
||||
return 0, input.Length()
|
||||
}
|
||||
|
||||
// Not possible
|
||||
if !isAscii(pattern) {
|
||||
return -1
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
firstIdx, idx := 0, 0
|
||||
for pidx := 0; pidx < len(pattern); pidx++ {
|
||||
idx = trySkip(input, caseSensitive, byte(pattern[pidx]), idx)
|
||||
firstIdx, idx, lastIdx := 0, 0, 0
|
||||
var b byte
|
||||
for pidx := range pattern {
|
||||
b = byte(pattern[pidx])
|
||||
idx = trySkip(input, caseSensitive, b, idx)
|
||||
if idx < 0 {
|
||||
return -1
|
||||
return -1, -1
|
||||
}
|
||||
if pidx == 0 && idx > 0 {
|
||||
// Step back to find the right bonus point
|
||||
firstIdx = idx - 1
|
||||
}
|
||||
lastIdx = idx
|
||||
idx++
|
||||
}
|
||||
return firstIdx
|
||||
|
||||
// Find the last appearance of the last character of the pattern to limit the search scope
|
||||
scope := input.Bytes()[lastIdx:]
|
||||
if len(scope) > 1 {
|
||||
tail := scope[1:]
|
||||
var end int
|
||||
if !caseSensitive && b >= 'a' && b <= 'z' {
|
||||
end = lastIndexByteTwo(tail, b, b-32)
|
||||
} else {
|
||||
end = bytes.LastIndexByte(tail, b)
|
||||
}
|
||||
if end >= 0 {
|
||||
return firstIdx, lastIdx + 1 + end + 1
|
||||
}
|
||||
}
|
||||
return firstIdx, lastIdx + 1
|
||||
}
|
||||
|
||||
func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []int16) {
|
||||
@@ -310,7 +397,7 @@ func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []in
|
||||
if i == 0 {
|
||||
fmt.Print(" ")
|
||||
for j := int(f); j <= lastIdx; j++ {
|
||||
fmt.Printf(" " + string(T[j]) + " ")
|
||||
fmt.Print(" " + string(T[j]) + " ")
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
@@ -348,18 +435,25 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
return Result{0, 0, 0}, posArray(withPos, M)
|
||||
}
|
||||
N := input.Length()
|
||||
if M > N {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
// Since O(nm) algorithm can be prohibitively expensive for large input,
|
||||
// we fall back to the greedy algorithm.
|
||||
if slab != nil && N*M > cap(slab.I16) {
|
||||
// Also, we should not allow a very long pattern to avoid 16-bit integer
|
||||
// overflow in the score matrix. 1000 is a safe limit.
|
||||
if slab != nil && int64(N)*int64(M) > int64(cap(slab.I16)) || M > 1000 {
|
||||
return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab)
|
||||
}
|
||||
|
||||
// Phase 1. Optimized search for ASCII string
|
||||
idx := asciiFuzzyIndex(input, pattern, caseSensitive)
|
||||
if idx < 0 {
|
||||
minIdx, maxIdx := asciiFuzzyIndex(input, pattern, caseSensitive)
|
||||
if minIdx < 0 {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
// fmt.Println(N, maxIdx, idx, maxIdx-idx, input.ToString())
|
||||
N = maxIdx - minIdx
|
||||
|
||||
// Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages
|
||||
offset16 := 0
|
||||
@@ -372,20 +466,19 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
offset32, F := alloc32(offset32, slab, M)
|
||||
// Rune array
|
||||
_, T := alloc32(offset32, slab, N)
|
||||
input.CopyRunes(T)
|
||||
input.CopyRunes(T, minIdx)
|
||||
|
||||
// Phase 2. Calculate bonus for each point
|
||||
maxScore, maxScorePos := int16(0), 0
|
||||
pidx, lastIdx := 0, 0
|
||||
pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), charNonWord, false
|
||||
Tsub := T[idx:]
|
||||
H0sub, C0sub, Bsub := H0[idx:][:len(Tsub)], C0[idx:][:len(Tsub)], B[idx:][:len(Tsub)]
|
||||
for off, char := range Tsub {
|
||||
pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), initialCharClass, false
|
||||
for off, char := range T {
|
||||
var class charClass
|
||||
if char <= unicode.MaxASCII {
|
||||
class = charClassOfAscii(char)
|
||||
class = asciiCharClasses[char]
|
||||
if !caseSensitive && class == charUpper {
|
||||
char += 32
|
||||
T[off] = char
|
||||
}
|
||||
} else {
|
||||
class = charClassOfNonAscii(char)
|
||||
@@ -395,53 +488,53 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
if normalize {
|
||||
char = normalizeRune(char)
|
||||
}
|
||||
T[off] = char
|
||||
}
|
||||
|
||||
Tsub[off] = char
|
||||
bonus := bonusFor(prevClass, class)
|
||||
Bsub[off] = bonus
|
||||
bonus := bonusMatrix[prevClass][class]
|
||||
B[off] = bonus
|
||||
prevClass = class
|
||||
|
||||
if char == pchar {
|
||||
if pidx < M {
|
||||
F[pidx] = int32(idx + off)
|
||||
F[pidx] = int32(off)
|
||||
pidx++
|
||||
pchar = pattern[util.Min(pidx, M-1)]
|
||||
pchar = pattern[min(pidx, M-1)]
|
||||
}
|
||||
lastIdx = idx + off
|
||||
lastIdx = off
|
||||
}
|
||||
|
||||
if char == pchar0 {
|
||||
score := scoreMatch + bonus*bonusFirstCharMultiplier
|
||||
H0sub[off] = score
|
||||
C0sub[off] = 1
|
||||
H0[off] = score
|
||||
C0[off] = 1
|
||||
if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) {
|
||||
maxScore, maxScorePos = score, idx+off
|
||||
if forward && bonus == bonusBoundary {
|
||||
maxScore, maxScorePos = score, off
|
||||
if forward && bonus >= bonusBoundary {
|
||||
break
|
||||
}
|
||||
}
|
||||
inGap = false
|
||||
} else {
|
||||
if inGap {
|
||||
H0sub[off] = util.Max16(prevH0+scoreGapExtention, 0)
|
||||
H0[off] = max(prevH0+scoreGapExtension, 0)
|
||||
} else {
|
||||
H0sub[off] = util.Max16(prevH0+scoreGapStart, 0)
|
||||
H0[off] = max(prevH0+scoreGapStart, 0)
|
||||
}
|
||||
C0sub[off] = 0
|
||||
C0[off] = 0
|
||||
inGap = true
|
||||
}
|
||||
prevH0 = H0sub[off]
|
||||
prevH0 = H0[off]
|
||||
}
|
||||
if pidx != M {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
if M == 1 {
|
||||
result := Result{maxScorePos, maxScorePos + 1, int(maxScore)}
|
||||
result := Result{minIdx + maxScorePos, minIdx + maxScorePos + 1, int(maxScore)}
|
||||
if !withPos {
|
||||
return result, nil
|
||||
}
|
||||
pos := []int{maxScorePos}
|
||||
pos := []int{minIdx + maxScorePos}
|
||||
return result, &pos
|
||||
}
|
||||
|
||||
@@ -477,7 +570,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
var s1, s2, consecutive int16
|
||||
|
||||
if inGap {
|
||||
s2 = Hleft[off] + scoreGapExtention
|
||||
s2 = Hleft[off] + scoreGapExtension
|
||||
} else {
|
||||
s2 = Hleft[off] + scoreGapStart
|
||||
}
|
||||
@@ -486,11 +579,14 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
s1 = Hdiag[off] + scoreMatch
|
||||
b := Bsub[off]
|
||||
consecutive = Cdiag[off] + 1
|
||||
// Break consecutive chunk
|
||||
if b == bonusBoundary {
|
||||
consecutive = 1
|
||||
} else if consecutive > 1 {
|
||||
b = util.Max16(b, util.Max16(bonusConsecutive, B[col-int(consecutive)+1]))
|
||||
if consecutive > 1 {
|
||||
fb := B[col-int(consecutive)+1]
|
||||
// Break consecutive chunk
|
||||
if b >= bonusBoundary && b > fb {
|
||||
consecutive = 1
|
||||
} else {
|
||||
b = max(b, bonusConsecutive, fb)
|
||||
}
|
||||
}
|
||||
if s1+b < s2 {
|
||||
s1 += Bsub[off]
|
||||
@@ -502,7 +598,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
Csub[off] = consecutive
|
||||
|
||||
inGap = s1 < s2
|
||||
score := util.Max16(util.Max16(s1, s2), 0)
|
||||
score := max(s1, s2, 0)
|
||||
if pidx == M-1 && (forward && score > maxScore || !forward && score >= maxScore) {
|
||||
maxScore, maxScorePos = score, col
|
||||
}
|
||||
@@ -535,7 +631,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
}
|
||||
|
||||
if s > s1 && (s > s2 || s == s2 && preferMatch) {
|
||||
*pos = append(*pos, j)
|
||||
*pos = append(*pos, j+minIdx)
|
||||
if i == 0 {
|
||||
break
|
||||
}
|
||||
@@ -548,14 +644,14 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
// Start offset we return here is only relevant when begin tiebreak is used.
|
||||
// However finding the accurate offset requires backtracking, and we don't
|
||||
// want to pay extra cost for the option that has lost its importance.
|
||||
return Result{j, maxScorePos + 1, int(maxScore)}, pos
|
||||
return Result{minIdx + j, minIdx + maxScorePos + 1, int(maxScore)}, pos
|
||||
}
|
||||
|
||||
// Implement the same sorting criteria as V2
|
||||
func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) {
|
||||
pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0)
|
||||
pos := posArray(withPos, len(pattern))
|
||||
prevClass := charNonWord
|
||||
prevClass := initialCharClass
|
||||
if sidx > 0 {
|
||||
prevClass = charClassOf(text.Get(sidx - 1))
|
||||
}
|
||||
@@ -578,15 +674,15 @@ func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, patter
|
||||
*pos = append(*pos, idx)
|
||||
}
|
||||
score += scoreMatch
|
||||
bonus := bonusFor(prevClass, class)
|
||||
bonus := bonusMatrix[prevClass][class]
|
||||
if consecutive == 0 {
|
||||
firstBonus = bonus
|
||||
} else {
|
||||
// Break consecutive chunk
|
||||
if bonus == bonusBoundary {
|
||||
if bonus >= bonusBoundary && bonus > firstBonus {
|
||||
firstBonus = bonus
|
||||
}
|
||||
bonus = util.Max16(util.Max16(bonus, firstBonus), bonusConsecutive)
|
||||
bonus = max(bonus, firstBonus, bonusConsecutive)
|
||||
}
|
||||
if pidx == 0 {
|
||||
score += int(bonus * bonusFirstCharMultiplier)
|
||||
@@ -598,7 +694,7 @@ func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, patter
|
||||
pidx++
|
||||
} else {
|
||||
if inGap {
|
||||
score += scoreGapExtention
|
||||
score += scoreGapExtension
|
||||
} else {
|
||||
score += scoreGapStart
|
||||
}
|
||||
@@ -616,7 +712,8 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
|
||||
if len(pattern) == 0 {
|
||||
return Result{0, 0, 0}, nil
|
||||
}
|
||||
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 {
|
||||
idx, _ := asciiFuzzyIndex(text, pattern, caseSensitive)
|
||||
if idx < 0 {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
@@ -627,7 +724,7 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
|
||||
lenRunes := text.Length()
|
||||
lenPattern := len(pattern)
|
||||
|
||||
for index := 0; index < lenRunes; index++ {
|
||||
for index := range lenRunes {
|
||||
char := text.Get(indexAt(index, lenRunes, forward))
|
||||
// This is considerably faster than blindly applying strings.ToLower to the
|
||||
// whole string
|
||||
@@ -668,6 +765,9 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
|
||||
char = unicode.To(unicode.LowerCase, char)
|
||||
}
|
||||
}
|
||||
if normalize {
|
||||
char = normalizeRune(char)
|
||||
}
|
||||
|
||||
pidx_ := indexAt(pidx, lenPattern, forward)
|
||||
pchar := pattern[pidx_]
|
||||
@@ -699,6 +799,14 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
|
||||
// The solution is much cheaper since there is only one possible alignment of
|
||||
// the pattern.
|
||||
func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
return exactMatchNaive(caseSensitive, normalize, forward, false, text, pattern, withPos, slab)
|
||||
}
|
||||
|
||||
func ExactMatchBoundary(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
return exactMatchNaive(caseSensitive, normalize, forward, true, text, pattern, withPos, slab)
|
||||
}
|
||||
|
||||
func exactMatchNaive(caseSensitive bool, normalize bool, forward bool, boundaryCheck bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
if len(pattern) == 0 {
|
||||
return Result{0, 0, 0}, nil
|
||||
}
|
||||
@@ -710,13 +818,14 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 {
|
||||
idx, _ := asciiFuzzyIndex(text, pattern, caseSensitive)
|
||||
if idx < 0 {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
// For simplicity, only look at the bonus at the first character position
|
||||
pidx := 0
|
||||
bestPos, bonus, bestBonus := -1, int16(0), int16(-1)
|
||||
bestPos, bonus, bbonus, bestBonus := -1, int16(0), int16(0), int16(-1)
|
||||
for index := 0; index < lenRunes; index++ {
|
||||
index_ := indexAt(index, lenRunes, forward)
|
||||
char := text.Get(index_)
|
||||
@@ -732,16 +841,37 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
|
||||
}
|
||||
pidx_ := indexAt(pidx, lenPattern, forward)
|
||||
pchar := pattern[pidx_]
|
||||
if pchar == char {
|
||||
ok := pchar == char
|
||||
if ok {
|
||||
if pidx_ == 0 {
|
||||
bonus = bonusAt(text, index_)
|
||||
}
|
||||
if boundaryCheck {
|
||||
if forward && pidx_ == 0 {
|
||||
bbonus = bonus
|
||||
} else if !forward && pidx_ == lenPattern-1 {
|
||||
if index_ < lenRunes-1 {
|
||||
bbonus = bonusAt(text, index_+1)
|
||||
} else {
|
||||
bbonus = bonusBoundaryWhite
|
||||
}
|
||||
}
|
||||
ok = bbonus >= bonusBoundary
|
||||
if ok && pidx_ == 0 {
|
||||
ok = index_ == 0 || charClassOf(text.Get(index_-1)) <= charDelimiter
|
||||
}
|
||||
if ok && pidx_ == len(pattern)-1 {
|
||||
ok = index_ == lenRunes-1 || charClassOf(text.Get(index_+1)) <= charDelimiter
|
||||
}
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
pidx++
|
||||
if pidx == lenPattern {
|
||||
if bonus > bestBonus {
|
||||
bestPos, bestBonus = index, bonus
|
||||
}
|
||||
if bonus == bonusBoundary {
|
||||
if bonus >= bonusBoundary {
|
||||
break
|
||||
}
|
||||
index -= pidx - 1
|
||||
@@ -761,7 +891,23 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
|
||||
sidx = lenRunes - (bestPos + 1)
|
||||
eidx = lenRunes - (bestPos - lenPattern + 1)
|
||||
}
|
||||
score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
|
||||
var score int
|
||||
if boundaryCheck {
|
||||
// Underscore boundaries should be ranked lower than the other types of boundaries
|
||||
score = int(bonus)
|
||||
deduct := int(bonus-bonusBoundary) + 1
|
||||
if sidx > 0 && text.Get(sidx-1) == '_' {
|
||||
score -= deduct + 1
|
||||
deduct = 1
|
||||
}
|
||||
if eidx < lenRunes && text.Get(eidx) == '_' {
|
||||
score -= deduct
|
||||
}
|
||||
// Add base score so that this can compete with other match types e.g. 'foo' | bar
|
||||
score += scoreMatch*lenPattern + int(bonusBoundaryWhite)*(lenPattern+1)
|
||||
} else {
|
||||
score, _ = calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
|
||||
}
|
||||
return Result{sidx, eidx, score}, nil
|
||||
}
|
||||
return Result{-1, -1, 0}, nil
|
||||
@@ -773,12 +919,17 @@ func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Ch
|
||||
return Result{0, 0, 0}, nil
|
||||
}
|
||||
|
||||
if text.Length() < len(pattern) {
|
||||
trimmedLen := 0
|
||||
if !unicode.IsSpace(pattern[0]) {
|
||||
trimmedLen = text.LeadingWhitespaces()
|
||||
}
|
||||
|
||||
if text.Length()-trimmedLen < len(pattern) {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
for index, r := range pattern {
|
||||
char := text.Get(index)
|
||||
char := text.Get(trimmedLen + index)
|
||||
if !caseSensitive {
|
||||
char = unicode.ToLower(char)
|
||||
}
|
||||
@@ -790,14 +941,17 @@ func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Ch
|
||||
}
|
||||
}
|
||||
lenPattern := len(pattern)
|
||||
score, _ := calculateScore(caseSensitive, normalize, text, pattern, 0, lenPattern, false)
|
||||
return Result{0, lenPattern, score}, nil
|
||||
score, _ := calculateScore(caseSensitive, normalize, text, pattern, trimmedLen, trimmedLen+lenPattern, false)
|
||||
return Result{trimmedLen, trimmedLen + lenPattern, score}, nil
|
||||
}
|
||||
|
||||
// SuffixMatch performs suffix-match
|
||||
func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
lenRunes := text.Length()
|
||||
trimmedLen := lenRunes - text.TrailingWhitespaces()
|
||||
trimmedLen := lenRunes
|
||||
if len(pattern) == 0 || !unicode.IsSpace(pattern[len(pattern)-1]) {
|
||||
trimmedLen -= text.TrailingWhitespaces()
|
||||
}
|
||||
if len(pattern) == 0 {
|
||||
return Result{trimmedLen, trimmedLen, 0}, nil
|
||||
}
|
||||
@@ -828,14 +982,30 @@ func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Ch
|
||||
// EqualMatch performs equal-match
|
||||
func EqualMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
lenPattern := len(pattern)
|
||||
if text.Length() != lenPattern {
|
||||
if lenPattern == 0 {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
// Strip leading whitespaces
|
||||
trimmedLen := 0
|
||||
if !unicode.IsSpace(pattern[0]) {
|
||||
trimmedLen = text.LeadingWhitespaces()
|
||||
}
|
||||
|
||||
// Strip trailing whitespaces
|
||||
trimmedEndLen := 0
|
||||
if !unicode.IsSpace(pattern[lenPattern-1]) {
|
||||
trimmedEndLen = text.TrailingWhitespaces()
|
||||
}
|
||||
|
||||
if text.Length()-trimmedLen-trimmedEndLen != lenPattern {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
match := true
|
||||
if normalize {
|
||||
runes := text.ToRunes()
|
||||
for idx, pchar := range pattern {
|
||||
char := runes[idx]
|
||||
char := runes[trimmedLen+idx]
|
||||
if !caseSensitive {
|
||||
char = unicode.To(unicode.LowerCase, char)
|
||||
}
|
||||
@@ -845,15 +1015,16 @@ func EqualMatch(caseSensitive bool, normalize bool, forward bool, text *util.Cha
|
||||
}
|
||||
}
|
||||
} else {
|
||||
runesStr := text.ToString()
|
||||
runes := text.ToRunes()
|
||||
runesStr := string(runes[trimmedLen : len(runes)-trimmedEndLen])
|
||||
if !caseSensitive {
|
||||
runesStr = strings.ToLower(runesStr)
|
||||
}
|
||||
match = runesStr == string(pattern)
|
||||
}
|
||||
if match {
|
||||
return Result{0, lenPattern, (scoreMatch+bonusBoundary)*lenPattern +
|
||||
(bonusFirstCharMultiplier-1)*bonusBoundary}, nil
|
||||
return Result{trimmedLen, trimmedLen + lenPattern, (scoreMatch+int(bonusBoundaryWhite))*lenPattern +
|
||||
(bonusFirstCharMultiplier-1)*int(bonusBoundaryWhite)}, nil
|
||||
}
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
+64
-30
@@ -9,6 +9,10 @@ import (
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Init("default")
|
||||
}
|
||||
|
||||
func assertMatch(t *testing.T, fun Algo, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, score int) {
|
||||
assertMatch2(t, fun, caseSensitive, false, forward, input, pattern, sidx, eidx, score)
|
||||
}
|
||||
@@ -43,46 +47,55 @@ func TestFuzzyMatch(t *testing.T) {
|
||||
for _, fn := range []Algo{FuzzyMatchV1, FuzzyMatchV2} {
|
||||
for _, forward := range []bool{true, false} {
|
||||
assertMatch(t, fn, false, forward, "fooBarbaz1", "oBZ", 2, 9,
|
||||
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtention*3)
|
||||
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3)
|
||||
assertMatch(t, fn, false, forward, "foo bar baz", "fbb", 0, 9,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
||||
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention)
|
||||
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+
|
||||
int(bonusBoundaryWhite)*2+2*scoreGapStart+4*scoreGapExtension)
|
||||
assertMatch(t, fn, false, forward, "/AutomatorDocument.icns", "rdoc", 9, 13,
|
||||
scoreMatch*4+bonusCamel123+bonusConsecutive*2)
|
||||
assertMatch(t, fn, false, forward, "/man1/zshcompctl.1", "zshc", 6, 10,
|
||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3)
|
||||
scoreMatch*4+int(bonusBoundaryDelimiter)*bonusFirstCharMultiplier+int(bonusBoundaryDelimiter)*3)
|
||||
assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13,
|
||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3+scoreGapStart)
|
||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+scoreGapStart+int(bonusBoundaryDelimiter))
|
||||
// Non-word character at start of input is treated as a strong boundary
|
||||
assertMatch(t, fn, false, forward, ".vimrc", ".vimrc", 0, 6,
|
||||
scoreMatch*6+int(bonusBoundaryWhite)*(bonusFirstCharMultiplier+5))
|
||||
// Non-word character right after a delimiter inherits the delimiter boundary
|
||||
assertMatch(t, fn, false, forward, "/.vimrc", ".vimrc", 1, 7,
|
||||
scoreMatch*6+int(bonusBoundaryDelimiter)*(bonusFirstCharMultiplier+5))
|
||||
// Non-word character in the middle of a word stays at bonusNonWord
|
||||
assertMatch(t, fn, false, forward, "a.vimrc", ".vimrc", 1, 7,
|
||||
scoreMatch*6+bonusBoundary*(bonusFirstCharMultiplier+5))
|
||||
assertMatch(t, fn, false, forward, "ab0123 456", "12356", 3, 10,
|
||||
scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtention)
|
||||
scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtension)
|
||||
assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10,
|
||||
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtention)
|
||||
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtension)
|
||||
assertMatch(t, fn, false, forward, "foo/bar/baz", "fbb", 0, 9,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
||||
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention)
|
||||
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+
|
||||
int(bonusBoundaryDelimiter)*2+2*scoreGapStart+4*scoreGapExtension)
|
||||
assertMatch(t, fn, false, forward, "fooBarBaz", "fbb", 0, 7,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
||||
bonusCamel123*2+2*scoreGapStart+2*scoreGapExtention)
|
||||
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+
|
||||
bonusCamel123*2+2*scoreGapStart+2*scoreGapExtension)
|
||||
assertMatch(t, fn, false, forward, "foo barbaz", "fbb", 0, 8,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary+
|
||||
scoreGapStart*2+scoreGapExtention*3)
|
||||
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)+
|
||||
scoreGapStart*2+scoreGapExtension*3)
|
||||
assertMatch(t, fn, false, forward, "fooBar Baz", "foob", 0, 4,
|
||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3)
|
||||
scoreMatch*4+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)*3)
|
||||
assertMatch(t, fn, false, forward, "xFoo-Bar Baz", "foo-b", 1, 6,
|
||||
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+
|
||||
bonusNonWord+bonusBoundary)
|
||||
|
||||
assertMatch(t, fn, true, forward, "fooBarbaz", "oBz", 2, 9,
|
||||
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtention*3)
|
||||
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3)
|
||||
assertMatch(t, fn, true, forward, "Foo/Bar/Baz", "FBB", 0, 9,
|
||||
scoreMatch*3+bonusBoundary*(bonusFirstCharMultiplier+2)+
|
||||
scoreGapStart*2+scoreGapExtention*4)
|
||||
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryDelimiter)*2+
|
||||
scoreGapStart*2+scoreGapExtension*4)
|
||||
assertMatch(t, fn, true, forward, "FooBarBaz", "FBB", 0, 7,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusCamel123*2+
|
||||
scoreGapStart*2+scoreGapExtention*2)
|
||||
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+bonusCamel123*2+
|
||||
scoreGapStart*2+scoreGapExtension*2)
|
||||
assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4,
|
||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+
|
||||
util.Max(bonusCamel123, bonusBoundary))
|
||||
scoreMatch*4+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)*2+
|
||||
max(bonusCamel123, int(bonusBoundaryWhite)))
|
||||
|
||||
// Consecutive bonus updated
|
||||
assertMatch(t, fn, true, forward, "foo-bar", "o-ba", 2, 6,
|
||||
@@ -98,10 +111,10 @@ func TestFuzzyMatch(t *testing.T) {
|
||||
|
||||
func TestFuzzyMatchBackward(t *testing.T) {
|
||||
assertMatch(t, FuzzyMatchV1, false, true, "foobar fb", "fb", 0, 4,
|
||||
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+
|
||||
scoreGapStart+scoreGapExtention)
|
||||
scoreMatch*2+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+
|
||||
scoreGapStart+scoreGapExtension)
|
||||
assertMatch(t, FuzzyMatchV1, false, false, "foobar fb", "fb", 7, 9,
|
||||
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary)
|
||||
scoreMatch*2+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite))
|
||||
}
|
||||
|
||||
func TestExactMatchNaive(t *testing.T) {
|
||||
@@ -114,9 +127,9 @@ func TestExactMatchNaive(t *testing.T) {
|
||||
assertMatch(t, ExactMatchNaive, false, dir, "/AutomatorDocument.icns", "rdoc", 9, 13,
|
||||
scoreMatch*4+bonusCamel123+bonusConsecutive*2)
|
||||
assertMatch(t, ExactMatchNaive, false, dir, "/man1/zshcompctl.1", "zshc", 6, 10,
|
||||
scoreMatch*4+bonusBoundary*(bonusFirstCharMultiplier+3))
|
||||
scoreMatch*4+int(bonusBoundaryDelimiter)*(bonusFirstCharMultiplier+3))
|
||||
assertMatch(t, ExactMatchNaive, false, dir, "/.oh-my-zsh/cache", "zsh/c", 8, 13,
|
||||
scoreMatch*5+bonusBoundary*(bonusFirstCharMultiplier+4))
|
||||
scoreMatch*5+bonusBoundary*(bonusFirstCharMultiplier+3)+int(bonusBoundaryDelimiter))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +141,7 @@ func TestExactMatchNaiveBackward(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrefixMatch(t *testing.T) {
|
||||
score := (scoreMatch+bonusBoundary)*3 + bonusBoundary*(bonusFirstCharMultiplier-1)
|
||||
score := scoreMatch*3 + int(bonusBoundaryWhite)*bonusFirstCharMultiplier + int(bonusBoundaryWhite)*2
|
||||
|
||||
for _, dir := range []bool{true, false} {
|
||||
assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0)
|
||||
@@ -136,6 +149,10 @@ func TestPrefixMatch(t *testing.T) {
|
||||
assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3, score)
|
||||
assertMatch(t, PrefixMatch, false, dir, "foOBarBaZ", "foo", 0, 3, score)
|
||||
assertMatch(t, PrefixMatch, false, dir, "f-oBarbaz", "f-o", 0, 3, score)
|
||||
|
||||
assertMatch(t, PrefixMatch, false, dir, " fooBar", "foo", 1, 4, score)
|
||||
assertMatch(t, PrefixMatch, false, dir, " fooBar", " fo", 0, 3, score)
|
||||
assertMatch(t, PrefixMatch, false, dir, " fo", "foo", -1, -1, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +165,14 @@ func TestSuffixMatch(t *testing.T) {
|
||||
scoreMatch*3+bonusConsecutive*2)
|
||||
assertMatch(t, SuffixMatch, false, dir, "fooBarBaZ", "baz", 6, 9,
|
||||
(scoreMatch+bonusCamel123)*3+bonusCamel123*(bonusFirstCharMultiplier-1))
|
||||
|
||||
// Strip trailing white space from the string
|
||||
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz ", "baz", 6, 9,
|
||||
scoreMatch*3+bonusConsecutive*2)
|
||||
|
||||
// Only when the pattern doesn't end with a space
|
||||
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz ", "baz ", 6, 10,
|
||||
scoreMatch*4+bonusConsecutive*2+int(bonusBoundaryWhite))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,9 +196,9 @@ func TestNormalize(t *testing.T) {
|
||||
input, pattern, sidx, eidx, score)
|
||||
}
|
||||
}
|
||||
test("Só Danço Samba", "So", 0, 2, 56, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive)
|
||||
test("Só Danço Samba", "sodc", 0, 7, 89, FuzzyMatchV1, FuzzyMatchV2)
|
||||
test("Danço", "danco", 0, 5, 128, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch)
|
||||
test("Só Danço Samba", "So", 0, 2, 62, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive)
|
||||
test("Só Danço Samba", "sodc", 0, 7, 97, FuzzyMatchV1, FuzzyMatchV2)
|
||||
test("Danço", "danco", 0, 5, 140, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch)
|
||||
}
|
||||
|
||||
func TestLongString(t *testing.T) {
|
||||
@@ -184,3 +209,12 @@ func TestLongString(t *testing.T) {
|
||||
bytes[math.MaxUint16] = 'z'
|
||||
assertMatch(t, FuzzyMatchV2, true, true, string(bytes), "zx", math.MaxUint16, math.MaxUint16+2, scoreMatch*2+bonusConsecutive)
|
||||
}
|
||||
|
||||
func TestLongStringWithNormalize(t *testing.T) {
|
||||
bytes := make([]byte, 30000)
|
||||
for i := range bytes {
|
||||
bytes[i] = 'x'
|
||||
}
|
||||
unicodeString := string(bytes) + " Minímal example"
|
||||
assertMatch2(t, FuzzyMatchV1, false, true, false, unicodeString, "minim", 30001, 30006, 140)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
//go:build amd64
|
||||
|
||||
package algo
|
||||
|
||||
var _useAVX2 bool
|
||||
|
||||
func init() {
|
||||
_useAVX2 = cpuHasAVX2()
|
||||
}
|
||||
|
||||
//go:noescape
|
||||
func cpuHasAVX2() bool
|
||||
|
||||
// indexByteTwo returns the index of the first occurrence of b1 or b2 in s,
|
||||
// or -1 if neither is present. Uses AVX2 when available, SSE2 otherwise.
|
||||
//
|
||||
//go:noescape
|
||||
func IndexByteTwo(s []byte, b1, b2 byte) int
|
||||
|
||||
// lastIndexByteTwo returns the index of the last occurrence of b1 or b2 in s,
|
||||
// or -1 if neither is present. Uses AVX2 when available, SSE2 otherwise.
|
||||
//
|
||||
//go:noescape
|
||||
func lastIndexByteTwo(s []byte, b1, b2 byte) int
|
||||
@@ -0,0 +1,377 @@
|
||||
#include "textflag.h"
|
||||
|
||||
// func cpuHasAVX2() bool
|
||||
//
|
||||
// Checks CPUID and XGETBV for AVX2 + OS YMM support.
|
||||
TEXT ·cpuHasAVX2(SB),NOSPLIT,$0-1
|
||||
MOVQ BX, R8 // save BX (callee-saved, clobbered by CPUID)
|
||||
|
||||
// Check max CPUID leaf >= 7
|
||||
MOVL $0, AX
|
||||
CPUID
|
||||
CMPL AX, $7
|
||||
JL cpuid_no
|
||||
|
||||
// Check OSXSAVE (CPUID.1:ECX bit 27)
|
||||
MOVL $1, AX
|
||||
CPUID
|
||||
TESTL $(1<<27), CX
|
||||
JZ cpuid_no
|
||||
|
||||
// Check AVX2 (CPUID.7.0:EBX bit 5)
|
||||
MOVL $7, AX
|
||||
MOVL $0, CX
|
||||
CPUID
|
||||
TESTL $(1<<5), BX
|
||||
JZ cpuid_no
|
||||
|
||||
// Check OS YMM state support via XGETBV
|
||||
MOVL $0, CX
|
||||
BYTE $0x0F; BYTE $0x01; BYTE $0xD0 // XGETBV → EDX:EAX
|
||||
ANDL $6, AX // bits 1 (XMM) and 2 (YMM)
|
||||
CMPL AX, $6
|
||||
JNE cpuid_no
|
||||
|
||||
MOVQ R8, BX // restore BX
|
||||
MOVB $1, ret+0(FP)
|
||||
RET
|
||||
|
||||
cpuid_no:
|
||||
MOVQ R8, BX
|
||||
MOVB $0, ret+0(FP)
|
||||
RET
|
||||
|
||||
// func IndexByteTwo(s []byte, b1, b2 byte) int
|
||||
//
|
||||
// Returns the index of the first occurrence of b1 or b2 in s, or -1.
|
||||
// Uses AVX2 (32 bytes/iter) when available, SSE2 (16 bytes/iter) otherwise.
|
||||
TEXT ·IndexByteTwo(SB),NOSPLIT,$0-40
|
||||
MOVQ s_base+0(FP), SI
|
||||
MOVQ s_len+8(FP), BX
|
||||
MOVBLZX b1+24(FP), AX
|
||||
MOVBLZX b2+25(FP), CX
|
||||
LEAQ ret+32(FP), R8
|
||||
|
||||
TESTQ BX, BX
|
||||
JEQ fwd_failure
|
||||
|
||||
// Try AVX2 for inputs >= 32 bytes
|
||||
CMPQ BX, $32
|
||||
JLT fwd_sse2
|
||||
CMPB ·_useAVX2(SB), $1
|
||||
JNE fwd_sse2
|
||||
|
||||
// ====== AVX2 forward search ======
|
||||
MOVD AX, X0
|
||||
VPBROADCASTB X0, Y0 // Y0 = splat(b1)
|
||||
MOVD CX, X1
|
||||
VPBROADCASTB X1, Y1 // Y1 = splat(b2)
|
||||
|
||||
MOVQ SI, DI
|
||||
LEAQ -32(SI)(BX*1), AX // AX = last valid 32-byte chunk
|
||||
JMP fwd_avx2_entry
|
||||
|
||||
fwd_avx2_loop:
|
||||
VMOVDQU (DI), Y2
|
||||
VPCMPEQB Y0, Y2, Y3
|
||||
VPCMPEQB Y1, Y2, Y4
|
||||
VPOR Y3, Y4, Y3
|
||||
VPMOVMSKB Y3, DX
|
||||
BSFL DX, DX
|
||||
JNZ fwd_avx2_success
|
||||
ADDQ $32, DI
|
||||
|
||||
fwd_avx2_entry:
|
||||
CMPQ DI, AX
|
||||
JB fwd_avx2_loop
|
||||
|
||||
// Last 32-byte chunk (may overlap with previous)
|
||||
MOVQ AX, DI
|
||||
VMOVDQU (AX), Y2
|
||||
VPCMPEQB Y0, Y2, Y3
|
||||
VPCMPEQB Y1, Y2, Y4
|
||||
VPOR Y3, Y4, Y3
|
||||
VPMOVMSKB Y3, DX
|
||||
BSFL DX, DX
|
||||
JNZ fwd_avx2_success
|
||||
|
||||
MOVQ $-1, (R8)
|
||||
VZEROUPPER
|
||||
RET
|
||||
|
||||
fwd_avx2_success:
|
||||
SUBQ SI, DI
|
||||
ADDQ DX, DI
|
||||
MOVQ DI, (R8)
|
||||
VZEROUPPER
|
||||
RET
|
||||
|
||||
// ====== SSE2 forward search (< 32 bytes or no AVX2) ======
|
||||
|
||||
fwd_sse2:
|
||||
// Broadcast b1 into X0
|
||||
MOVD AX, X0
|
||||
PUNPCKLBW X0, X0
|
||||
PUNPCKLBW X0, X0
|
||||
PSHUFL $0, X0, X0
|
||||
|
||||
// Broadcast b2 into X4
|
||||
MOVD CX, X4
|
||||
PUNPCKLBW X4, X4
|
||||
PUNPCKLBW X4, X4
|
||||
PSHUFL $0, X4, X4
|
||||
|
||||
CMPQ BX, $16
|
||||
JLT fwd_small
|
||||
|
||||
MOVQ SI, DI
|
||||
LEAQ -16(SI)(BX*1), AX
|
||||
JMP fwd_sseloopentry
|
||||
|
||||
fwd_sseloop:
|
||||
MOVOU (DI), X1
|
||||
MOVOU X1, X2
|
||||
PCMPEQB X0, X1
|
||||
PCMPEQB X4, X2
|
||||
POR X2, X1
|
||||
PMOVMSKB X1, DX
|
||||
BSFL DX, DX
|
||||
JNZ fwd_ssesuccess
|
||||
ADDQ $16, DI
|
||||
|
||||
fwd_sseloopentry:
|
||||
CMPQ DI, AX
|
||||
JB fwd_sseloop
|
||||
|
||||
// Search the last 16-byte chunk (may overlap)
|
||||
MOVQ AX, DI
|
||||
MOVOU (AX), X1
|
||||
MOVOU X1, X2
|
||||
PCMPEQB X0, X1
|
||||
PCMPEQB X4, X2
|
||||
POR X2, X1
|
||||
PMOVMSKB X1, DX
|
||||
BSFL DX, DX
|
||||
JNZ fwd_ssesuccess
|
||||
|
||||
fwd_failure:
|
||||
MOVQ $-1, (R8)
|
||||
RET
|
||||
|
||||
fwd_ssesuccess:
|
||||
SUBQ SI, DI
|
||||
ADDQ DX, DI
|
||||
MOVQ DI, (R8)
|
||||
RET
|
||||
|
||||
fwd_small:
|
||||
// Check if loading 16 bytes from SI would cross a page boundary
|
||||
LEAQ 16(SI), AX
|
||||
TESTW $0xff0, AX
|
||||
JEQ fwd_endofpage
|
||||
|
||||
MOVOU (SI), X1
|
||||
MOVOU X1, X2
|
||||
PCMPEQB X0, X1
|
||||
PCMPEQB X4, X2
|
||||
POR X2, X1
|
||||
PMOVMSKB X1, DX
|
||||
BSFL DX, DX
|
||||
JZ fwd_failure
|
||||
CMPL DX, BX
|
||||
JAE fwd_failure
|
||||
MOVQ DX, (R8)
|
||||
RET
|
||||
|
||||
fwd_endofpage:
|
||||
MOVOU -16(SI)(BX*1), X1
|
||||
MOVOU X1, X2
|
||||
PCMPEQB X0, X1
|
||||
PCMPEQB X4, X2
|
||||
POR X2, X1
|
||||
PMOVMSKB X1, DX
|
||||
MOVL BX, CX
|
||||
SHLL CX, DX
|
||||
SHRL $16, DX
|
||||
BSFL DX, DX
|
||||
JZ fwd_failure
|
||||
MOVQ DX, (R8)
|
||||
RET
|
||||
|
||||
// func lastIndexByteTwo(s []byte, b1, b2 byte) int
|
||||
//
|
||||
// Returns the index of the last occurrence of b1 or b2 in s, or -1.
|
||||
// Uses AVX2 (32 bytes/iter) when available, SSE2 (16 bytes/iter) otherwise.
|
||||
TEXT ·lastIndexByteTwo(SB),NOSPLIT,$0-40
|
||||
MOVQ s_base+0(FP), SI
|
||||
MOVQ s_len+8(FP), BX
|
||||
MOVBLZX b1+24(FP), AX
|
||||
MOVBLZX b2+25(FP), CX
|
||||
LEAQ ret+32(FP), R8
|
||||
|
||||
TESTQ BX, BX
|
||||
JEQ back_failure
|
||||
|
||||
// Try AVX2 for inputs >= 32 bytes
|
||||
CMPQ BX, $32
|
||||
JLT back_sse2
|
||||
CMPB ·_useAVX2(SB), $1
|
||||
JNE back_sse2
|
||||
|
||||
// ====== AVX2 backward search ======
|
||||
MOVD AX, X0
|
||||
VPBROADCASTB X0, Y0
|
||||
MOVD CX, X1
|
||||
VPBROADCASTB X1, Y1
|
||||
|
||||
// DI = start of last 32-byte chunk
|
||||
LEAQ -32(SI)(BX*1), DI
|
||||
|
||||
back_avx2_loop:
|
||||
CMPQ DI, SI
|
||||
JBE back_avx2_first
|
||||
|
||||
VMOVDQU (DI), Y2
|
||||
VPCMPEQB Y0, Y2, Y3
|
||||
VPCMPEQB Y1, Y2, Y4
|
||||
VPOR Y3, Y4, Y3
|
||||
VPMOVMSKB Y3, DX
|
||||
BSRL DX, DX
|
||||
JNZ back_avx2_success
|
||||
SUBQ $32, DI
|
||||
JMP back_avx2_loop
|
||||
|
||||
back_avx2_first:
|
||||
// First 32 bytes (DI <= SI, load from SI)
|
||||
VMOVDQU (SI), Y2
|
||||
VPCMPEQB Y0, Y2, Y3
|
||||
VPCMPEQB Y1, Y2, Y4
|
||||
VPOR Y3, Y4, Y3
|
||||
VPMOVMSKB Y3, DX
|
||||
BSRL DX, DX
|
||||
JNZ back_avx2_firstsuccess
|
||||
|
||||
MOVQ $-1, (R8)
|
||||
VZEROUPPER
|
||||
RET
|
||||
|
||||
back_avx2_success:
|
||||
SUBQ SI, DI
|
||||
ADDQ DX, DI
|
||||
MOVQ DI, (R8)
|
||||
VZEROUPPER
|
||||
RET
|
||||
|
||||
back_avx2_firstsuccess:
|
||||
MOVQ DX, (R8)
|
||||
VZEROUPPER
|
||||
RET
|
||||
|
||||
// ====== SSE2 backward search (< 32 bytes or no AVX2) ======
|
||||
|
||||
back_sse2:
|
||||
// Broadcast b1 into X0
|
||||
MOVD AX, X0
|
||||
PUNPCKLBW X0, X0
|
||||
PUNPCKLBW X0, X0
|
||||
PSHUFL $0, X0, X0
|
||||
|
||||
// Broadcast b2 into X4
|
||||
MOVD CX, X4
|
||||
PUNPCKLBW X4, X4
|
||||
PUNPCKLBW X4, X4
|
||||
PSHUFL $0, X4, X4
|
||||
|
||||
CMPQ BX, $16
|
||||
JLT back_small
|
||||
|
||||
// DI = start of last 16-byte chunk
|
||||
LEAQ -16(SI)(BX*1), DI
|
||||
|
||||
back_sseloop:
|
||||
CMPQ DI, SI
|
||||
JBE back_ssefirst
|
||||
|
||||
MOVOU (DI), X1
|
||||
MOVOU X1, X2
|
||||
PCMPEQB X0, X1
|
||||
PCMPEQB X4, X2
|
||||
POR X2, X1
|
||||
PMOVMSKB X1, DX
|
||||
BSRL DX, DX
|
||||
JNZ back_ssesuccess
|
||||
SUBQ $16, DI
|
||||
JMP back_sseloop
|
||||
|
||||
back_ssefirst:
|
||||
// First 16 bytes (DI <= SI, load from SI)
|
||||
MOVOU (SI), X1
|
||||
MOVOU X1, X2
|
||||
PCMPEQB X0, X1
|
||||
PCMPEQB X4, X2
|
||||
POR X2, X1
|
||||
PMOVMSKB X1, DX
|
||||
BSRL DX, DX
|
||||
JNZ back_ssefirstsuccess
|
||||
|
||||
back_failure:
|
||||
MOVQ $-1, (R8)
|
||||
RET
|
||||
|
||||
back_ssesuccess:
|
||||
SUBQ SI, DI
|
||||
ADDQ DX, DI
|
||||
MOVQ DI, (R8)
|
||||
RET
|
||||
|
||||
back_ssefirstsuccess:
|
||||
// DX = byte offset from base
|
||||
MOVQ DX, (R8)
|
||||
RET
|
||||
|
||||
back_small:
|
||||
// Check page boundary
|
||||
LEAQ 16(SI), AX
|
||||
TESTW $0xff0, AX
|
||||
JEQ back_endofpage
|
||||
|
||||
MOVOU (SI), X1
|
||||
MOVOU X1, X2
|
||||
PCMPEQB X0, X1
|
||||
PCMPEQB X4, X2
|
||||
POR X2, X1
|
||||
PMOVMSKB X1, DX
|
||||
// Mask to first BX bytes: keep bits 0..BX-1
|
||||
MOVL $1, AX
|
||||
MOVL BX, CX
|
||||
SHLL CX, AX
|
||||
DECL AX
|
||||
ANDL AX, DX
|
||||
BSRL DX, DX
|
||||
JZ back_failure
|
||||
MOVQ DX, (R8)
|
||||
RET
|
||||
|
||||
back_endofpage:
|
||||
// Load 16 bytes ending at base+n
|
||||
MOVOU -16(SI)(BX*1), X1
|
||||
MOVOU X1, X2
|
||||
PCMPEQB X0, X1
|
||||
PCMPEQB X4, X2
|
||||
POR X2, X1
|
||||
PMOVMSKB X1, DX
|
||||
// Bits correspond to bytes [base+n-16, base+n).
|
||||
// We want original bytes [0, n), which are bits [16-n, 16).
|
||||
// Mask: keep bits (16-n) through 15.
|
||||
MOVL $16, CX
|
||||
SUBL BX, CX
|
||||
SHRL CX, DX
|
||||
SHLL CX, DX
|
||||
BSRL DX, DX
|
||||
JZ back_failure
|
||||
// DX is the bit position in the loaded chunk.
|
||||
// Original byte index = DX - (16 - n) = DX + n - 16
|
||||
ADDL BX, DX
|
||||
SUBL $16, DX
|
||||
MOVQ DX, (R8)
|
||||
RET
|
||||
@@ -0,0 +1,17 @@
|
||||
//go:build arm64
|
||||
|
||||
package algo
|
||||
|
||||
// indexByteTwo returns the index of the first occurrence of b1 or b2 in s,
|
||||
// or -1 if neither is present. Implemented in assembly using ARM64 NEON
|
||||
// to search for both bytes in a single pass.
|
||||
//
|
||||
//go:noescape
|
||||
func IndexByteTwo(s []byte, b1, b2 byte) int
|
||||
|
||||
// lastIndexByteTwo returns the index of the last occurrence of b1 or b2 in s,
|
||||
// or -1 if neither is present. Implemented in assembly using ARM64 NEON,
|
||||
// scanning backward.
|
||||
//
|
||||
//go:noescape
|
||||
func lastIndexByteTwo(s []byte, b1, b2 byte) int
|
||||
@@ -0,0 +1,249 @@
|
||||
#include "textflag.h"
|
||||
|
||||
// func IndexByteTwo(s []byte, b1, b2 byte) int
|
||||
//
|
||||
// Returns the index of the first occurrence of b1 or b2 in s, or -1.
|
||||
// Uses ARM64 NEON to search for both bytes in a single pass over the data.
|
||||
// Adapted from Go's internal/bytealg/indexbyte_arm64.s (single-byte version).
|
||||
TEXT ·IndexByteTwo(SB),NOSPLIT,$0-40
|
||||
MOVD s_base+0(FP), R0
|
||||
MOVD s_len+8(FP), R2
|
||||
MOVBU b1+24(FP), R1
|
||||
MOVBU b2+25(FP), R7
|
||||
MOVD $ret+32(FP), R8
|
||||
|
||||
// Core algorithm:
|
||||
// For each 32-byte chunk we calculate a 64-bit syndrome value,
|
||||
// with two bits per byte. We compare against both b1 and b2,
|
||||
// OR the results, then use the same syndrome extraction as
|
||||
// Go's IndexByte.
|
||||
|
||||
CBZ R2, fail
|
||||
MOVD R0, R11
|
||||
// Magic constant 0x40100401 allows us to identify which lane matches.
|
||||
// Each byte in the group of 4 gets a distinct bit: 1, 4, 16, 64.
|
||||
MOVD $0x40100401, R5
|
||||
VMOV R1, V0.B16 // V0 = splat(b1)
|
||||
VMOV R7, V7.B16 // V7 = splat(b2)
|
||||
// Work with aligned 32-byte chunks
|
||||
BIC $0x1f, R0, R3
|
||||
VMOV R5, V5.S4
|
||||
ANDS $0x1f, R0, R9
|
||||
AND $0x1f, R2, R10
|
||||
BEQ loop
|
||||
|
||||
// Input string is not 32-byte aligned. Process the first
|
||||
// aligned 32-byte block and mask off bytes before our start.
|
||||
VLD1.P (R3), [V1.B16, V2.B16]
|
||||
SUB $0x20, R9, R4
|
||||
ADDS R4, R2, R2
|
||||
// Compare against both needles
|
||||
VCMEQ V0.B16, V1.B16, V3.B16 // b1 vs first 16 bytes
|
||||
VCMEQ V7.B16, V1.B16, V8.B16 // b2 vs first 16 bytes
|
||||
VORR V8.B16, V3.B16, V3.B16 // combine
|
||||
VCMEQ V0.B16, V2.B16, V4.B16 // b1 vs second 16 bytes
|
||||
VCMEQ V7.B16, V2.B16, V9.B16 // b2 vs second 16 bytes
|
||||
VORR V9.B16, V4.B16, V4.B16 // combine
|
||||
// Build syndrome
|
||||
VAND V5.B16, V3.B16, V3.B16
|
||||
VAND V5.B16, V4.B16, V4.B16
|
||||
VADDP V4.B16, V3.B16, V6.B16
|
||||
VADDP V6.B16, V6.B16, V6.B16
|
||||
VMOV V6.D[0], R6
|
||||
// Clear the irrelevant lower bits
|
||||
LSL $1, R9, R4
|
||||
LSR R4, R6, R6
|
||||
LSL R4, R6, R6
|
||||
// The first block can also be the last
|
||||
BLS masklast
|
||||
// Have we found something already?
|
||||
CBNZ R6, tail
|
||||
|
||||
loop:
|
||||
VLD1.P (R3), [V1.B16, V2.B16]
|
||||
SUBS $0x20, R2, R2
|
||||
// Compare against both needles, OR results
|
||||
VCMEQ V0.B16, V1.B16, V3.B16
|
||||
VCMEQ V7.B16, V1.B16, V8.B16
|
||||
VORR V8.B16, V3.B16, V3.B16
|
||||
VCMEQ V0.B16, V2.B16, V4.B16
|
||||
VCMEQ V7.B16, V2.B16, V9.B16
|
||||
VORR V9.B16, V4.B16, V4.B16
|
||||
// If we're out of data we finish regardless of the result
|
||||
BLS end
|
||||
// Fast check: OR both halves and check for any match
|
||||
VORR V4.B16, V3.B16, V6.B16
|
||||
VADDP V6.D2, V6.D2, V6.D2
|
||||
VMOV V6.D[0], R6
|
||||
CBZ R6, loop
|
||||
|
||||
end:
|
||||
// Found something or out of data, build full syndrome
|
||||
VAND V5.B16, V3.B16, V3.B16
|
||||
VAND V5.B16, V4.B16, V4.B16
|
||||
VADDP V4.B16, V3.B16, V6.B16
|
||||
VADDP V6.B16, V6.B16, V6.B16
|
||||
VMOV V6.D[0], R6
|
||||
// Only mask for the last block
|
||||
BHS tail
|
||||
|
||||
masklast:
|
||||
// Clear irrelevant upper bits
|
||||
ADD R9, R10, R4
|
||||
AND $0x1f, R4, R4
|
||||
SUB $0x20, R4, R4
|
||||
NEG R4<<1, R4
|
||||
LSL R4, R6, R6
|
||||
LSR R4, R6, R6
|
||||
|
||||
tail:
|
||||
CBZ R6, fail
|
||||
RBIT R6, R6
|
||||
SUB $0x20, R3, R3
|
||||
CLZ R6, R6
|
||||
ADD R6>>1, R3, R0
|
||||
SUB R11, R0, R0
|
||||
MOVD R0, (R8)
|
||||
RET
|
||||
|
||||
fail:
|
||||
MOVD $-1, R0
|
||||
MOVD R0, (R8)
|
||||
RET
|
||||
|
||||
// func lastIndexByteTwo(s []byte, b1, b2 byte) int
|
||||
//
|
||||
// Returns the index of the last occurrence of b1 or b2 in s, or -1.
|
||||
// Scans backward using ARM64 NEON.
|
||||
TEXT ·lastIndexByteTwo(SB),NOSPLIT,$0-40
|
||||
MOVD s_base+0(FP), R0
|
||||
MOVD s_len+8(FP), R2
|
||||
MOVBU b1+24(FP), R1
|
||||
MOVBU b2+25(FP), R7
|
||||
MOVD $ret+32(FP), R8
|
||||
|
||||
CBZ R2, lfail
|
||||
MOVD R0, R11 // save base
|
||||
ADD R0, R2, R12 // R12 = end = base + len
|
||||
MOVD $0x40100401, R5
|
||||
VMOV R1, V0.B16 // V0 = splat(b1)
|
||||
VMOV R7, V7.B16 // V7 = splat(b2)
|
||||
VMOV R5, V5.S4
|
||||
|
||||
// Align: find the aligned block containing the last byte
|
||||
SUB $1, R12, R3
|
||||
BIC $0x1f, R3, R3 // R3 = start of aligned block containing last byte
|
||||
|
||||
// --- Process tail block ---
|
||||
VLD1 (R3), [V1.B16, V2.B16]
|
||||
VCMEQ V0.B16, V1.B16, V3.B16
|
||||
VCMEQ V7.B16, V1.B16, V8.B16
|
||||
VORR V8.B16, V3.B16, V3.B16
|
||||
VCMEQ V0.B16, V2.B16, V4.B16
|
||||
VCMEQ V7.B16, V2.B16, V9.B16
|
||||
VORR V9.B16, V4.B16, V4.B16
|
||||
VAND V5.B16, V3.B16, V3.B16
|
||||
VAND V5.B16, V4.B16, V4.B16
|
||||
VADDP V4.B16, V3.B16, V6.B16
|
||||
VADDP V6.B16, V6.B16, V6.B16
|
||||
VMOV V6.D[0], R6
|
||||
|
||||
// Mask upper bits (bytes past end of slice)
|
||||
// tail_bytes = end - R3 (1..32)
|
||||
SUB R3, R12, R10 // R10 = tail_bytes
|
||||
MOVD $64, R4
|
||||
SUB R10<<1, R4, R4 // R4 = 64 - 2*tail_bytes
|
||||
LSL R4, R6, R6
|
||||
LSR R4, R6, R6
|
||||
|
||||
// Is this also the head block?
|
||||
CMP R11, R3 // R3 - R11
|
||||
BLO lmaskfirst // R3 < base: head+tail in same block
|
||||
BEQ ltailonly // R3 == base: single aligned block
|
||||
|
||||
// R3 > base: more blocks before this one
|
||||
CBNZ R6, llast
|
||||
B lbacksetup
|
||||
|
||||
ltailonly:
|
||||
// Single block, already masked upper bits
|
||||
CBNZ R6, llast
|
||||
B lfail
|
||||
|
||||
lmaskfirst:
|
||||
// Mask lower bits (bytes before start of slice)
|
||||
SUB R3, R11, R4 // R4 = base - R3
|
||||
LSL $1, R4, R4
|
||||
LSR R4, R6, R6
|
||||
LSL R4, R6, R6
|
||||
CBNZ R6, llast
|
||||
B lfail
|
||||
|
||||
lbacksetup:
|
||||
SUB $0x20, R3
|
||||
|
||||
lbackloop:
|
||||
VLD1 (R3), [V1.B16, V2.B16]
|
||||
VCMEQ V0.B16, V1.B16, V3.B16
|
||||
VCMEQ V7.B16, V1.B16, V8.B16
|
||||
VORR V8.B16, V3.B16, V3.B16
|
||||
VCMEQ V0.B16, V2.B16, V4.B16
|
||||
VCMEQ V7.B16, V2.B16, V9.B16
|
||||
VORR V9.B16, V4.B16, V4.B16
|
||||
// Quick check: any match in this block?
|
||||
VORR V4.B16, V3.B16, V6.B16
|
||||
VADDP V6.D2, V6.D2, V6.D2
|
||||
VMOV V6.D[0], R6
|
||||
|
||||
// Is this a head block? (R3 < base)
|
||||
CMP R11, R3
|
||||
BLO lheadblock
|
||||
|
||||
// Full block (R3 >= base)
|
||||
CBNZ R6, lbackfound
|
||||
// More blocks?
|
||||
BEQ lfail // R3 == base, no more
|
||||
SUB $0x20, R3
|
||||
B lbackloop
|
||||
|
||||
lbackfound:
|
||||
// Build full syndrome
|
||||
VAND V5.B16, V3.B16, V3.B16
|
||||
VAND V5.B16, V4.B16, V4.B16
|
||||
VADDP V4.B16, V3.B16, V6.B16
|
||||
VADDP V6.B16, V6.B16, V6.B16
|
||||
VMOV V6.D[0], R6
|
||||
B llast
|
||||
|
||||
lheadblock:
|
||||
// R3 < base. Build full syndrome if quick check had a match.
|
||||
CBZ R6, lfail
|
||||
VAND V5.B16, V3.B16, V3.B16
|
||||
VAND V5.B16, V4.B16, V4.B16
|
||||
VADDP V4.B16, V3.B16, V6.B16
|
||||
VADDP V6.B16, V6.B16, V6.B16
|
||||
VMOV V6.D[0], R6
|
||||
// Mask lower bits
|
||||
SUB R3, R11, R4 // R4 = base - R3
|
||||
LSL $1, R4, R4
|
||||
LSR R4, R6, R6
|
||||
LSL R4, R6, R6
|
||||
CBZ R6, lfail
|
||||
|
||||
llast:
|
||||
// Find last match: highest set bit in syndrome
|
||||
// Syndrome has bit 2i set for matching byte i.
|
||||
// CLZ gives leading zeros; byte_offset = (63 - CLZ) / 2.
|
||||
CLZ R6, R6
|
||||
MOVD $63, R4
|
||||
SUB R6, R4, R6 // R6 = 63 - CLZ = bit position
|
||||
LSR $1, R6 // R6 = byte offset within block
|
||||
ADD R3, R6, R0 // R0 = absolute address
|
||||
SUB R11, R0, R0 // R0 = slice index
|
||||
MOVD R0, (R8)
|
||||
RET
|
||||
|
||||
lfail:
|
||||
MOVD $-1, R0
|
||||
MOVD R0, (R8)
|
||||
RET
|
||||
@@ -0,0 +1,33 @@
|
||||
//go:build !arm64 && !amd64
|
||||
|
||||
package algo
|
||||
|
||||
import "bytes"
|
||||
|
||||
// indexByteTwo returns the index of the first occurrence of b1 or b2 in s,
|
||||
// or -1 if neither is present.
|
||||
func IndexByteTwo(s []byte, b1, b2 byte) int {
|
||||
i1 := bytes.IndexByte(s, b1)
|
||||
if i1 == 0 {
|
||||
return 0
|
||||
}
|
||||
scope := s
|
||||
if i1 > 0 {
|
||||
scope = s[:i1]
|
||||
}
|
||||
if i2 := bytes.IndexByte(scope, b2); i2 >= 0 {
|
||||
return i2
|
||||
}
|
||||
return i1
|
||||
}
|
||||
|
||||
// lastIndexByteTwo returns the index of the last occurrence of b1 or b2 in s,
|
||||
// or -1 if neither is present.
|
||||
func lastIndexByteTwo(s []byte, b1, b2 byte) int {
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
if s[i] == b1 || s[i] == b2 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package algo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIndexByteTwo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
s string
|
||||
b1 byte
|
||||
b2 byte
|
||||
want int
|
||||
}{
|
||||
{"empty", "", 'a', 'b', -1},
|
||||
{"single_b1", "a", 'a', 'b', 0},
|
||||
{"single_b2", "b", 'a', 'b', 0},
|
||||
{"single_none", "c", 'a', 'b', -1},
|
||||
{"b1_first", "xaxb", 'a', 'b', 1},
|
||||
{"b2_first", "xbxa", 'a', 'b', 1},
|
||||
{"same_byte", "xxa", 'a', 'a', 2},
|
||||
{"at_end", "xxxxa", 'a', 'b', 4},
|
||||
{"not_found", "xxxxxxxx", 'a', 'b', -1},
|
||||
{"long_b1_at_3000", string(make([]byte, 3000)) + "a" + string(make([]byte, 1000)), 'a', 'b', 3000},
|
||||
{"long_b2_at_3000", string(make([]byte, 3000)) + "b" + string(make([]byte, 1000)), 'a', 'b', 3000},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := IndexByteTwo([]byte(tt.s), tt.b1, tt.b2)
|
||||
if got != tt.want {
|
||||
t.Errorf("IndexByteTwo(%q, %c, %c) = %d, want %d", tt.s[:min(len(tt.s), 40)], tt.b1, tt.b2, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Exhaustive test: compare against loop reference for various lengths,
|
||||
// including sizes around SIMD block boundaries (16, 32, 64).
|
||||
for n := 0; n <= 256; n++ {
|
||||
data := make([]byte, n)
|
||||
for i := range data {
|
||||
data[i] = byte('c' + (i % 20))
|
||||
}
|
||||
// Test with match at every position
|
||||
for pos := 0; pos < n; pos++ {
|
||||
for _, b := range []byte{'A', 'B'} {
|
||||
data[pos] = b
|
||||
got := IndexByteTwo(data, 'A', 'B')
|
||||
want := loopIndexByteTwo(data, 'A', 'B')
|
||||
if got != want {
|
||||
t.Fatalf("IndexByteTwo(len=%d, match=%c@%d) = %d, want %d", n, b, pos, got, want)
|
||||
}
|
||||
data[pos] = byte('c' + (pos % 20))
|
||||
}
|
||||
}
|
||||
// Test with no match
|
||||
got := IndexByteTwo(data, 'A', 'B')
|
||||
if got != -1 {
|
||||
t.Fatalf("IndexByteTwo(len=%d, no match) = %d, want -1", n, got)
|
||||
}
|
||||
// Test with both bytes present
|
||||
if n >= 2 {
|
||||
data[n/3] = 'A'
|
||||
data[n*2/3] = 'B'
|
||||
got := IndexByteTwo(data, 'A', 'B')
|
||||
want := loopIndexByteTwo(data, 'A', 'B')
|
||||
if got != want {
|
||||
t.Fatalf("IndexByteTwo(len=%d, both@%d,%d) = %d, want %d", n, n/3, n*2/3, got, want)
|
||||
}
|
||||
data[n/3] = byte('c' + ((n / 3) % 20))
|
||||
data[n*2/3] = byte('c' + ((n * 2 / 3) % 20))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastIndexByteTwo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
s string
|
||||
b1 byte
|
||||
b2 byte
|
||||
want int
|
||||
}{
|
||||
{"empty", "", 'a', 'b', -1},
|
||||
{"single_b1", "a", 'a', 'b', 0},
|
||||
{"single_b2", "b", 'a', 'b', 0},
|
||||
{"single_none", "c", 'a', 'b', -1},
|
||||
{"b1_last", "xbxa", 'a', 'b', 3},
|
||||
{"b2_last", "xaxb", 'a', 'b', 3},
|
||||
{"same_byte", "axx", 'a', 'a', 0},
|
||||
{"at_start", "axxxx", 'a', 'b', 0},
|
||||
{"both_present", "axbx", 'a', 'b', 2},
|
||||
{"not_found", "xxxxxxxx", 'a', 'b', -1},
|
||||
{"long_b1_at_3000", string(make([]byte, 3000)) + "a" + string(make([]byte, 1000)), 'a', 'b', 3000},
|
||||
{"long_b2_at_end", string(make([]byte, 4000)) + "b", 'a', 'b', 4000},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := lastIndexByteTwo([]byte(tt.s), tt.b1, tt.b2)
|
||||
if got != tt.want {
|
||||
t.Errorf("lastIndexByteTwo(%q, %c, %c) = %d, want %d", tt.s[:min(len(tt.s), 40)], tt.b1, tt.b2, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Exhaustive test against loop reference
|
||||
for n := 0; n <= 256; n++ {
|
||||
data := make([]byte, n)
|
||||
for i := range data {
|
||||
data[i] = byte('c' + (i % 20))
|
||||
}
|
||||
for pos := 0; pos < n; pos++ {
|
||||
for _, b := range []byte{'A', 'B'} {
|
||||
data[pos] = b
|
||||
got := lastIndexByteTwo(data, 'A', 'B')
|
||||
want := refLastIndexByteTwo(data, 'A', 'B')
|
||||
if got != want {
|
||||
t.Fatalf("lastIndexByteTwo(len=%d, match=%c@%d) = %d, want %d", n, b, pos, got, want)
|
||||
}
|
||||
data[pos] = byte('c' + (pos % 20))
|
||||
}
|
||||
}
|
||||
// No match
|
||||
got := lastIndexByteTwo(data, 'A', 'B')
|
||||
if got != -1 {
|
||||
t.Fatalf("lastIndexByteTwo(len=%d, no match) = %d, want -1", n, got)
|
||||
}
|
||||
// Both bytes present
|
||||
if n >= 2 {
|
||||
data[n/3] = 'A'
|
||||
data[n*2/3] = 'B'
|
||||
got := lastIndexByteTwo(data, 'A', 'B')
|
||||
want := refLastIndexByteTwo(data, 'A', 'B')
|
||||
if got != want {
|
||||
t.Fatalf("lastIndexByteTwo(len=%d, both@%d,%d) = %d, want %d", n, n/3, n*2/3, got, want)
|
||||
}
|
||||
data[n/3] = byte('c' + ((n / 3) % 20))
|
||||
data[n*2/3] = byte('c' + ((n * 2 / 3) % 20))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzIndexByteTwo(f *testing.F) {
|
||||
f.Add([]byte("hello world"), byte('o'), byte('l'))
|
||||
f.Add([]byte(""), byte('a'), byte('b'))
|
||||
f.Add([]byte("aaa"), byte('a'), byte('a'))
|
||||
f.Fuzz(func(t *testing.T, data []byte, b1, b2 byte) {
|
||||
got := IndexByteTwo(data, b1, b2)
|
||||
want := loopIndexByteTwo(data, b1, b2)
|
||||
if got != want {
|
||||
t.Errorf("IndexByteTwo(len=%d, b1=%d, b2=%d) = %d, want %d", len(data), b1, b2, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzLastIndexByteTwo(f *testing.F) {
|
||||
f.Add([]byte("hello world"), byte('o'), byte('l'))
|
||||
f.Add([]byte(""), byte('a'), byte('b'))
|
||||
f.Add([]byte("aaa"), byte('a'), byte('a'))
|
||||
f.Fuzz(func(t *testing.T, data []byte, b1, b2 byte) {
|
||||
got := lastIndexByteTwo(data, b1, b2)
|
||||
want := refLastIndexByteTwo(data, b1, b2)
|
||||
if got != want {
|
||||
t.Errorf("lastIndexByteTwo(len=%d, b1=%d, b2=%d) = %d, want %d", len(data), b1, b2, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Reference implementations for correctness checking
|
||||
func refIndexByteTwo(s []byte, b1, b2 byte) int {
|
||||
i1 := bytes.IndexByte(s, b1)
|
||||
if i1 == 0 {
|
||||
return 0
|
||||
}
|
||||
scope := s
|
||||
if i1 > 0 {
|
||||
scope = s[:i1]
|
||||
}
|
||||
if i2 := bytes.IndexByte(scope, b2); i2 >= 0 {
|
||||
return i2
|
||||
}
|
||||
return i1
|
||||
}
|
||||
|
||||
func loopIndexByteTwo(s []byte, b1, b2 byte) int {
|
||||
for i, b := range s {
|
||||
if b == b1 || b == b2 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func refLastIndexByteTwo(s []byte, b1, b2 byte) int {
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
if s[i] == b1 || s[i] == b2 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func benchIndexByteTwo(b *testing.B, size int, pos int) {
|
||||
data := make([]byte, size)
|
||||
for i := range data {
|
||||
data[i] = byte('a' + (i % 20))
|
||||
}
|
||||
data[pos] = 'Z'
|
||||
|
||||
type impl struct {
|
||||
name string
|
||||
fn func([]byte, byte, byte) int
|
||||
}
|
||||
impls := []impl{
|
||||
{"asm", IndexByteTwo},
|
||||
{"2xIndexByte", refIndexByteTwo},
|
||||
{"loop", loopIndexByteTwo},
|
||||
}
|
||||
for _, im := range impls {
|
||||
b.Run(im.name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
im.fn(data, 'Z', 'z')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func benchLastIndexByteTwo(b *testing.B, size int, pos int) {
|
||||
data := make([]byte, size)
|
||||
for i := range data {
|
||||
data[i] = byte('a' + (i % 20))
|
||||
}
|
||||
data[pos] = 'Z'
|
||||
|
||||
type impl struct {
|
||||
name string
|
||||
fn func([]byte, byte, byte) int
|
||||
}
|
||||
impls := []impl{
|
||||
{"asm", lastIndexByteTwo},
|
||||
{"loop", refLastIndexByteTwo},
|
||||
}
|
||||
for _, im := range impls {
|
||||
b.Run(im.name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
im.fn(data, 'Z', 'z')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIndexByteTwo_10(b *testing.B) { benchIndexByteTwo(b, 10, 8) }
|
||||
func BenchmarkIndexByteTwo_100(b *testing.B) { benchIndexByteTwo(b, 100, 80) }
|
||||
func BenchmarkIndexByteTwo_1000(b *testing.B) { benchIndexByteTwo(b, 1000, 800) }
|
||||
func BenchmarkLastIndexByteTwo_10(b *testing.B) { benchLastIndexByteTwo(b, 10, 2) }
|
||||
func BenchmarkLastIndexByteTwo_100(b *testing.B) { benchLastIndexByteTwo(b, 100, 20) }
|
||||
func BenchmarkLastIndexByteTwo_1000(b *testing.B) { benchLastIndexByteTwo(b, 1000, 200) }
|
||||
+167
-2
@@ -3,7 +3,7 @@
|
||||
|
||||
package algo
|
||||
|
||||
var normalized map[rune]rune = map[rune]rune{
|
||||
var normalized = map[rune]rune{
|
||||
0x00E1: 'a', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x0103: 'a', // WITH BREVE, LATIN SMALL LETTER
|
||||
0x01CE: 'a', // WITH CARON, LATIN SMALL LETTER
|
||||
@@ -405,6 +405,171 @@ var normalized map[rune]rune = map[rune]rune{
|
||||
0x024E: 'Y', // WITH STROKE, LATIN CAPITAL LETTER
|
||||
0x028F: 'Y', // , LATIN LETTER SMALL CAPITAL
|
||||
0x1D22: 'Z', // , LATIN LETTER SMALL CAPITAL
|
||||
|
||||
'Ắ': 'A',
|
||||
'Ấ': 'A',
|
||||
'Ằ': 'A',
|
||||
'Ầ': 'A',
|
||||
'Ẳ': 'A',
|
||||
'Ẩ': 'A',
|
||||
'Ẵ': 'A',
|
||||
'Ẫ': 'A',
|
||||
'Ặ': 'A',
|
||||
'Ậ': 'A',
|
||||
|
||||
'ắ': 'a',
|
||||
'ấ': 'a',
|
||||
'ằ': 'a',
|
||||
'ầ': 'a',
|
||||
'ẳ': 'a',
|
||||
'ẩ': 'a',
|
||||
'ẵ': 'a',
|
||||
'ẫ': 'a',
|
||||
'ặ': 'a',
|
||||
'ậ': 'a',
|
||||
|
||||
'Ế': 'E',
|
||||
'Ề': 'E',
|
||||
'Ể': 'E',
|
||||
'Ễ': 'E',
|
||||
'Ệ': 'E',
|
||||
|
||||
'ế': 'e',
|
||||
'ề': 'e',
|
||||
'ể': 'e',
|
||||
'ễ': 'e',
|
||||
'ệ': 'e',
|
||||
|
||||
'Ố': 'O',
|
||||
'Ớ': 'O',
|
||||
'Ồ': 'O',
|
||||
'Ờ': 'O',
|
||||
'Ổ': 'O',
|
||||
'Ở': 'O',
|
||||
'Ỗ': 'O',
|
||||
'Ỡ': 'O',
|
||||
'Ộ': 'O',
|
||||
'Ợ': 'O',
|
||||
|
||||
'ố': 'o',
|
||||
'ớ': 'o',
|
||||
'ồ': 'o',
|
||||
'ờ': 'o',
|
||||
'ổ': 'o',
|
||||
'ở': 'o',
|
||||
'ỗ': 'o',
|
||||
'ỡ': 'o',
|
||||
'ộ': 'o',
|
||||
'ợ': 'o',
|
||||
|
||||
'Ứ': 'U',
|
||||
'Ừ': 'U',
|
||||
'Ử': 'U',
|
||||
'Ữ': 'U',
|
||||
'Ự': 'U',
|
||||
|
||||
'ứ': 'u',
|
||||
'ừ': 'u',
|
||||
'ử': 'u',
|
||||
'ữ': 'u',
|
||||
'ự': 'u',
|
||||
|
||||
// https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
|
||||
0xFF01: '!', // Fullwidth exclamation
|
||||
0xFF02: '"', // Fullwidth quotation mark
|
||||
0xFF03: '#', // Fullwidth number sign
|
||||
0xFF04: '$', // Fullwidth dollar sign
|
||||
0xFF05: '%', // Fullwidth percent
|
||||
0xFF06: '&', // Fullwidth ampersand
|
||||
0xFF07: '\'', // Fullwidth apostrophe
|
||||
0xFF08: '(', // Fullwidth left parenthesis
|
||||
0xFF09: ')', // Fullwidth right parenthesis
|
||||
0xFF0A: '*', // Fullwidth asterisk
|
||||
0xFF0B: '+', // Fullwidth plus
|
||||
0xFF0C: ',', // Fullwidth comma
|
||||
0xFF0D: '-', // Fullwidth hyphen-minus
|
||||
0xFF0E: '.', // Fullwidth period
|
||||
0xFF0F: '/', // Fullwidth slash
|
||||
0xFF10: '0',
|
||||
0xFF11: '1',
|
||||
0xFF12: '2',
|
||||
0xFF13: '3',
|
||||
0xFF14: '4',
|
||||
0xFF15: '5',
|
||||
0xFF16: '6',
|
||||
0xFF17: '7',
|
||||
0xFF18: '8',
|
||||
0xFF19: '9',
|
||||
0xFF1A: ':', // Fullwidth colon
|
||||
0xFF1B: ';', // Fullwidth semicolon
|
||||
0xFF1C: '<', // Fullwidth less-than
|
||||
0xFF1D: '=', // Fullwidth equal
|
||||
0xFF1E: '>', // Fullwidth greater-than
|
||||
0xFF1F: '?', // Fullwidth question mark
|
||||
0xFF20: '@', // Fullwidth at sign
|
||||
0xFF21: 'A',
|
||||
0xFF22: 'B',
|
||||
0xFF23: 'C',
|
||||
0xFF24: 'D',
|
||||
0xFF25: 'E',
|
||||
0xFF26: 'F',
|
||||
0xFF27: 'G',
|
||||
0xFF28: 'H',
|
||||
0xFF29: 'I',
|
||||
0xFF2A: 'J',
|
||||
0xFF2B: 'K',
|
||||
0xFF2C: 'L',
|
||||
0xFF2D: 'M',
|
||||
0xFF2E: 'N',
|
||||
0xFF2F: 'O',
|
||||
0xFF30: 'P',
|
||||
0xFF31: 'Q',
|
||||
0xFF32: 'R',
|
||||
0xFF33: 'S',
|
||||
0xFF34: 'T',
|
||||
0xFF35: 'U',
|
||||
0xFF36: 'V',
|
||||
0xFF37: 'W',
|
||||
0xFF38: 'X',
|
||||
0xFF39: 'Y',
|
||||
0xFF3A: 'Z',
|
||||
0xFF3B: '[', // Fullwidth left bracket
|
||||
0xFF3C: '\\', // Fullwidth backslash
|
||||
0xFF3D: ']', // Fullwidth right bracket
|
||||
0xFF3E: '^', // Fullwidth circumflex
|
||||
0xFF3F: '_', // Fullwidth underscore
|
||||
0xFF40: '`', // Fullwidth grave accent
|
||||
0xFF41: 'a',
|
||||
0xFF42: 'b',
|
||||
0xFF43: 'c',
|
||||
0xFF44: 'd',
|
||||
0xFF45: 'e',
|
||||
0xFF46: 'f',
|
||||
0xFF47: 'g',
|
||||
0xFF48: 'h',
|
||||
0xFF49: 'i',
|
||||
0xFF4A: 'j',
|
||||
0xFF4B: 'k',
|
||||
0xFF4C: 'l',
|
||||
0xFF4D: 'm',
|
||||
0xFF4E: 'n',
|
||||
0xFF4F: 'o',
|
||||
0xFF50: 'p',
|
||||
0xFF51: 'q',
|
||||
0xFF52: 'r',
|
||||
0xFF53: 's',
|
||||
0xFF54: 't',
|
||||
0xFF55: 'u',
|
||||
0xFF56: 'v',
|
||||
0xFF57: 'w',
|
||||
0xFF58: 'x',
|
||||
0xFF59: 'y',
|
||||
0xFF5A: 'z',
|
||||
0xFF5B: '{', // Fullwidth left brace
|
||||
0xFF5C: '|', // Fullwidth vertical bar
|
||||
0xFF5D: '}', // Fullwidth right brace
|
||||
0xFF5E: '~', // Fullwidth tilde
|
||||
0xFF61: '.', // Halfwidth ideographic full stop
|
||||
}
|
||||
|
||||
// NormalizeRunes normalizes latin script letters
|
||||
@@ -412,7 +577,7 @@ func NormalizeRunes(runes []rune) []rune {
|
||||
ret := make([]rune, len(runes))
|
||||
copy(ret, runes)
|
||||
for idx, r := range runes {
|
||||
if r < 0x00C0 || r > 0x2184 {
|
||||
if r < 0x00C0 || r > 0xFF61 {
|
||||
continue
|
||||
}
|
||||
n := normalized[r]
|
||||
|
||||
+370
-79
@@ -1,12 +1,12 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/junegunn/fzf/src/algo"
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
)
|
||||
|
||||
@@ -15,21 +15,29 @@ type ansiOffset struct {
|
||||
color ansiState
|
||||
}
|
||||
|
||||
type url struct {
|
||||
uri string
|
||||
params string
|
||||
}
|
||||
|
||||
type ansiState struct {
|
||||
fg tui.Color
|
||||
bg tui.Color
|
||||
ul tui.Color
|
||||
attr tui.Attr
|
||||
lbg tui.Color
|
||||
url *url
|
||||
}
|
||||
|
||||
func (s *ansiState) colored() bool {
|
||||
return s.fg != -1 || s.bg != -1 || s.attr > 0
|
||||
return s.fg != -1 || s.bg != -1 || s.ul != -1 || s.attr > 0 || s.lbg >= 0 || s.url != nil
|
||||
}
|
||||
|
||||
func (s *ansiState) equals(t *ansiState) bool {
|
||||
if t == nil {
|
||||
return !s.colored()
|
||||
}
|
||||
return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr
|
||||
return s.fg == t.fg && s.bg == t.bg && s.ul == t.ul && s.attr == t.attr && s.lbg == t.lbg && s.url == t.url
|
||||
}
|
||||
|
||||
func (s *ansiState) ToString() string {
|
||||
@@ -38,7 +46,7 @@ func (s *ansiState) ToString() string {
|
||||
}
|
||||
|
||||
ret := ""
|
||||
if s.attr&tui.Bold > 0 {
|
||||
if s.attr&tui.Bold > 0 || s.attr&tui.BoldForce > 0 {
|
||||
ret += "1;"
|
||||
}
|
||||
if s.attr&tui.Dim > 0 {
|
||||
@@ -48,7 +56,18 @@ func (s *ansiState) ToString() string {
|
||||
ret += "3;"
|
||||
}
|
||||
if s.attr&tui.Underline > 0 {
|
||||
ret += "4;"
|
||||
switch s.attr.UnderlineStyle() {
|
||||
case tui.UlStyleDouble:
|
||||
ret += "4:2;"
|
||||
case tui.UlStyleCurly:
|
||||
ret += "4:3;"
|
||||
case tui.UlStyleDotted:
|
||||
ret += "4:4;"
|
||||
case tui.UlStyleDashed:
|
||||
ret += "4:5;"
|
||||
default:
|
||||
ret += "4;"
|
||||
}
|
||||
}
|
||||
if s.attr&tui.Blink > 0 {
|
||||
ret += "5;"
|
||||
@@ -56,9 +75,33 @@ func (s *ansiState) ToString() string {
|
||||
if s.attr&tui.Reverse > 0 {
|
||||
ret += "7;"
|
||||
}
|
||||
if s.attr&tui.StrikeThrough > 0 {
|
||||
ret += "9;"
|
||||
}
|
||||
ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40)
|
||||
if s.ul != -1 {
|
||||
ret += toAnsiStringUl(s.ul)
|
||||
}
|
||||
|
||||
return "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
|
||||
ret = "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
|
||||
if s.url != nil {
|
||||
ret = fmt.Sprintf("\x1b]8;%s;%s\x1b\\%s\x1b]8;;\x1b", s.url.params, s.url.uri, ret)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func toAnsiStringUl(color tui.Color) string {
|
||||
col := int(color)
|
||||
if col < 0 {
|
||||
return ""
|
||||
}
|
||||
if col >= (1 << 24) {
|
||||
r := strconv.Itoa((col >> 16) & 0xff)
|
||||
g := strconv.Itoa((col >> 8) & 0xff)
|
||||
b := strconv.Itoa(col & 0xff)
|
||||
return "58;2;" + r + ";" + g + ";" + b + ";"
|
||||
}
|
||||
return "58;5;" + strconv.Itoa(col) + ";"
|
||||
}
|
||||
|
||||
func toAnsiString(color tui.Color, offset int) string {
|
||||
@@ -81,82 +124,215 @@ func toAnsiString(color tui.Color, offset int) string {
|
||||
return ret + ";"
|
||||
}
|
||||
|
||||
var ansiRegex *regexp.Regexp
|
||||
func matchOperatingSystemCommand(s string, start int) int {
|
||||
// `\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)`
|
||||
// ^ match starting here after the first printable character
|
||||
//
|
||||
i := start // prefix matched in nextAnsiEscapeSequence()
|
||||
|
||||
func init() {
|
||||
/*
|
||||
References:
|
||||
- https://github.com/gnachman/iTerm2
|
||||
- http://ascii-table.com/ansi-escape-sequences.php
|
||||
- http://ascii-table.com/ansi-escape-sequences-vt-100.php
|
||||
- http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
|
||||
*/
|
||||
// The following regular expression will include not all but most of the
|
||||
// frequently used ANSI sequences
|
||||
ansiRegex = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b.|[\x0e\x0f]|.\x08)")
|
||||
// Find the terminator: BEL (\x07) or ESC (\x1b) for ST (\x1b\\)
|
||||
idx := algo.IndexByteTwo(stringBytes(s[i:]), '\x07', '\x1b')
|
||||
if idx < 0 {
|
||||
return -1
|
||||
}
|
||||
i += idx
|
||||
|
||||
if s[i] == '\x07' {
|
||||
return i + 1
|
||||
}
|
||||
// `\x1b]8;PARAMS;URI\x1b\\TITLE\x1b]8;;\x1b`
|
||||
// ------
|
||||
if i < len(s)-1 && s[i+1] == '\\' {
|
||||
return i + 2
|
||||
}
|
||||
|
||||
// `\x1b]8;PARAMS;URI\x1b\\TITLE\x1b]8;;\x1b`
|
||||
// ------------
|
||||
if s[:i+1] == "\x1b]8;;\x1b" {
|
||||
return i + 1
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func findAnsiStart(str string) int {
|
||||
idx := 0
|
||||
for ; idx < len(str); idx++ {
|
||||
b := str[idx]
|
||||
if b == 0x1b || b == 0x0e || b == 0x0f {
|
||||
return idx
|
||||
}
|
||||
if b == 0x08 && idx > 0 {
|
||||
return idx - 1
|
||||
func matchControlSequence(s string) int {
|
||||
// `\x1b[\\[()][0-9;:?]*[a-zA-Z@]`
|
||||
// ^ match starting here
|
||||
//
|
||||
i := 2 // prefix matched in nextAnsiEscapeSequence()
|
||||
for ; i < len(s); i++ {
|
||||
c := s[i]
|
||||
switch c {
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ';', ':', '?':
|
||||
// ok
|
||||
default:
|
||||
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '@' {
|
||||
return i + 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
}
|
||||
return idx
|
||||
return -1
|
||||
}
|
||||
|
||||
func isCtrlSeqStart(c uint8) bool {
|
||||
switch c {
|
||||
case '\\', '[', '(', ')':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// nextAnsiEscapeSequence returns the ANSI escape sequence and is equivalent to
|
||||
// calling FindStringIndex() on the below regex (which was originally used):
|
||||
//
|
||||
// "(?:\x1b[\\[()][0-9;:?]*[a-zA-Z@]|\x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08|\n)"
|
||||
func nextAnsiEscapeSequence(s string) (int, int) {
|
||||
// fast check for ANSI escape sequences
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '\x0e', '\x0f', '\x1b', '\x08', '\n':
|
||||
// We ignore the fact that '\x08' cannot be the first char
|
||||
// in the string and be an escape sequence for the sake of
|
||||
// speed and simplicity.
|
||||
goto Loop
|
||||
}
|
||||
}
|
||||
return -1, -1
|
||||
|
||||
Loop:
|
||||
for ; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '\n':
|
||||
// match: `\n`
|
||||
return i, i + 1
|
||||
case '\x08':
|
||||
// backtrack to match: `.\x08`
|
||||
if i > 0 && s[i-1] != '\n' {
|
||||
if s[i-1] < utf8.RuneSelf {
|
||||
return i - 1, i + 1
|
||||
}
|
||||
_, n := utf8.DecodeLastRuneInString(s[:i])
|
||||
return i - n, i + 1
|
||||
}
|
||||
case '\x1b':
|
||||
// match: `\x1b[\\[()][0-9;:?]*[a-zA-Z@]`
|
||||
if i+2 < len(s) && isCtrlSeqStart(s[i+1]) {
|
||||
if j := matchControlSequence(s[i:]); j != -1 {
|
||||
return i, i + j
|
||||
}
|
||||
}
|
||||
|
||||
// match: `\x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)`
|
||||
if i+5 < len(s) && s[i+1] == ']' {
|
||||
j := 2
|
||||
// \x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)
|
||||
// ------
|
||||
for ; i+j < len(s) && isNumeric(s[i+j]); j++ {
|
||||
}
|
||||
|
||||
// \x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)
|
||||
// ---------------
|
||||
if j > 2 && i+j+1 < len(s) && (s[i+j] == ';' || s[i+j] == ':') && s[i+j+1] >= '\x20' {
|
||||
if k := matchOperatingSystemCommand(s[i:], j+2); k != -1 {
|
||||
return i, i + k
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// match: `\x1b.`
|
||||
if i+1 < len(s) && s[i+1] != '\n' {
|
||||
if s[i+1] < utf8.RuneSelf {
|
||||
return i, i + 2
|
||||
}
|
||||
_, n := utf8.DecodeRuneInString(s[i+1:])
|
||||
return i, i + n + 1
|
||||
}
|
||||
case '\x0e', '\x0f':
|
||||
// match: `[\x0e\x0f]`
|
||||
return i, i + 1
|
||||
}
|
||||
}
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
|
||||
var offsets []ansiOffset
|
||||
var output bytes.Buffer
|
||||
// We append to a stack allocated variable that we'll
|
||||
// later copy and return, to save on allocations.
|
||||
offsets := make([]ansiOffset, 0, 32)
|
||||
|
||||
if state != nil {
|
||||
offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state})
|
||||
}
|
||||
|
||||
prevIdx := 0
|
||||
runeCount := 0
|
||||
var (
|
||||
pstate *ansiState // lazily allocated
|
||||
output strings.Builder
|
||||
prevIdx int
|
||||
runeCount int
|
||||
)
|
||||
for idx := 0; idx < len(str); {
|
||||
idx += findAnsiStart(str[idx:])
|
||||
if idx == len(str) {
|
||||
// Make sure that we found an ANSI code
|
||||
start, end := nextAnsiEscapeSequence(str[idx:])
|
||||
if start == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
// Make sure that we found an ANSI code
|
||||
offset := ansiRegex.FindStringIndex(str[idx:])
|
||||
if len(offset) < 2 {
|
||||
idx++
|
||||
continue
|
||||
}
|
||||
offset[0] += idx
|
||||
offset[1] += idx
|
||||
idx = offset[1]
|
||||
start += idx
|
||||
idx += end
|
||||
|
||||
// Check if we should continue
|
||||
prev := str[prevIdx:offset[0]]
|
||||
prev := str[prevIdx:start]
|
||||
if proc != nil && !proc(prev, state) {
|
||||
return "", nil, nil
|
||||
}
|
||||
prevIdx = idx
|
||||
|
||||
prevIdx = offset[1]
|
||||
runeCount += utf8.RuneCountInString(prev)
|
||||
output.WriteString(prev)
|
||||
if len(prev) != 0 {
|
||||
runeCount += utf8.RuneCountInString(prev)
|
||||
// Grow the buffer size to the maximum possible length (string length
|
||||
// containing ansi codes) to avoid repetitive allocation
|
||||
if output.Cap() == 0 {
|
||||
output.Grow(len(str))
|
||||
}
|
||||
output.WriteString(prev)
|
||||
}
|
||||
|
||||
newState := interpretCode(str[offset[0]:offset[1]], state)
|
||||
if !newState.equals(state) {
|
||||
code := str[start:idx]
|
||||
newState := interpretCode(code, state)
|
||||
if code == "\n" || !newState.equals(state) {
|
||||
if state != nil {
|
||||
// Update last offset
|
||||
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
|
||||
}
|
||||
|
||||
if code == "\n" {
|
||||
output.WriteRune('\n')
|
||||
runeCount++
|
||||
// Full-background marker
|
||||
if newState.lbg >= 0 {
|
||||
marker := newState
|
||||
marker.attr |= tui.FullBg
|
||||
offsets = append(offsets, ansiOffset{
|
||||
[2]int32{int32(runeCount), int32(runeCount)},
|
||||
marker,
|
||||
})
|
||||
// Reset the full-line background color
|
||||
newState.lbg = -1
|
||||
}
|
||||
}
|
||||
|
||||
if newState.colored() {
|
||||
// Append new offset
|
||||
state = newState
|
||||
offsets = append(offsets, ansiOffset{[2]int32{int32(runeCount), int32(runeCount)}, *state})
|
||||
if pstate == nil {
|
||||
pstate = &ansiState{}
|
||||
}
|
||||
*pstate = newState
|
||||
state = pstate
|
||||
offsets = append(offsets, ansiOffset{
|
||||
[2]int32{int32(runeCount), int32(runeCount)},
|
||||
newState,
|
||||
})
|
||||
} else {
|
||||
// Discard state
|
||||
state = nil
|
||||
@@ -166,7 +342,6 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
||||
|
||||
var rest string
|
||||
var trimmed string
|
||||
|
||||
if prevIdx == 0 {
|
||||
// No ANSI code found
|
||||
rest = str
|
||||
@@ -176,48 +351,113 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
||||
output.WriteString(rest)
|
||||
trimmed = output.String()
|
||||
}
|
||||
if len(rest) > 0 && state != nil {
|
||||
// Update last offset
|
||||
runeCount += utf8.RuneCountInString(rest)
|
||||
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
|
||||
}
|
||||
if proc != nil {
|
||||
proc(rest, state)
|
||||
}
|
||||
if len(offsets) == 0 {
|
||||
return trimmed, nil, state
|
||||
if len(offsets) > 0 {
|
||||
if len(rest) > 0 && state != nil {
|
||||
// Update last offset
|
||||
runeCount += utf8.RuneCountInString(rest)
|
||||
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
|
||||
}
|
||||
// Return a copy of the offsets slice
|
||||
a := make([]ansiOffset, len(offsets))
|
||||
copy(a, offsets)
|
||||
return trimmed, &a, state
|
||||
}
|
||||
return trimmed, &offsets, state
|
||||
return trimmed, nil, state
|
||||
}
|
||||
|
||||
func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
|
||||
// State
|
||||
var state *ansiState
|
||||
func parseAnsiCode(s string) (int, byte, string) {
|
||||
var remaining string
|
||||
var sep byte
|
||||
// Find the first separator (either ; or :)
|
||||
i := -1
|
||||
for j := 0; j < len(s); j++ {
|
||||
if s[j] == ';' || s[j] == ':' {
|
||||
i = j
|
||||
break
|
||||
}
|
||||
}
|
||||
if i >= 0 {
|
||||
sep = s[i]
|
||||
remaining = s[i+1:]
|
||||
s = s[:i]
|
||||
}
|
||||
|
||||
if len(s) > 0 {
|
||||
// Inlined version of strconv.Atoi() that only handles positive
|
||||
// integers and does not allocate on error.
|
||||
code := 0
|
||||
for _, ch := range stringBytes(s) {
|
||||
ch -= '0'
|
||||
if ch > 9 {
|
||||
return -1, sep, remaining
|
||||
}
|
||||
code = code*10 + int(ch)
|
||||
}
|
||||
return code, sep, remaining
|
||||
}
|
||||
|
||||
return -1, sep, remaining
|
||||
}
|
||||
|
||||
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
if ansiCode == "\n" {
|
||||
if prevState != nil {
|
||||
return *prevState
|
||||
}
|
||||
return ansiState{-1, -1, -1, 0, -1, nil}
|
||||
}
|
||||
|
||||
var state ansiState
|
||||
if prevState == nil {
|
||||
state = &ansiState{-1, -1, 0}
|
||||
state = ansiState{-1, -1, -1, 0, -1, nil}
|
||||
} else {
|
||||
state = &ansiState{prevState.fg, prevState.bg, prevState.attr}
|
||||
state = ansiState{prevState.fg, prevState.bg, prevState.ul, prevState.attr, prevState.lbg, prevState.url}
|
||||
}
|
||||
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
||||
if prevState != nil && (strings.HasSuffix(ansiCode, "0K") || strings.HasSuffix(ansiCode, "[K")) {
|
||||
state.lbg = prevState.bg
|
||||
} else if strings.HasPrefix(ansiCode, "\x1b]8;") && (strings.HasSuffix(ansiCode, "\x1b\\") || strings.HasSuffix(ansiCode, "\a")) {
|
||||
stLen := 2
|
||||
if strings.HasSuffix(ansiCode, "\a") {
|
||||
stLen = 1
|
||||
}
|
||||
// "\x1b]8;;\x1b\\" or "\x1b]8;;\a"
|
||||
if len(ansiCode) == 5+stLen && ansiCode[4] == ';' {
|
||||
state.url = nil
|
||||
} else if paramsEnd := strings.IndexRune(ansiCode[4:], ';'); paramsEnd >= 0 {
|
||||
params := ansiCode[4 : 4+paramsEnd]
|
||||
uri := ansiCode[5+paramsEnd : len(ansiCode)-stLen]
|
||||
state.url = &url{uri: uri, params: params}
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
ptr := &state.fg
|
||||
state256 := 0
|
||||
|
||||
init := func() {
|
||||
reset := func() {
|
||||
state.fg = -1
|
||||
state.bg = -1
|
||||
state.ul = -1
|
||||
state.attr = 0
|
||||
state256 = 0
|
||||
}
|
||||
|
||||
ansiCode = ansiCode[2 : len(ansiCode)-1]
|
||||
if len(ansiCode) == 0 {
|
||||
init()
|
||||
if len(ansiCode) <= 3 {
|
||||
reset()
|
||||
return state
|
||||
}
|
||||
for _, code := range strings.Split(ansiCode, ";") {
|
||||
if num, err := strconv.Atoi(code); err == nil {
|
||||
ansiCode = ansiCode[2 : len(ansiCode)-1]
|
||||
|
||||
state256 := 0
|
||||
ptr := &state.fg
|
||||
|
||||
count := 0
|
||||
for len(ansiCode) != 0 {
|
||||
var num int
|
||||
var sep byte
|
||||
if num, sep, ansiCode = parseAnsiCode(ansiCode); num != -1 {
|
||||
count++
|
||||
switch state256 {
|
||||
case 0:
|
||||
switch num {
|
||||
@@ -227,10 +467,15 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
|
||||
case 48:
|
||||
ptr = &state.bg
|
||||
state256++
|
||||
case 58:
|
||||
ptr = &state.ul
|
||||
state256++
|
||||
case 39:
|
||||
state.fg = -1
|
||||
case 49:
|
||||
state.bg = -1
|
||||
case 59:
|
||||
state.ul = -1
|
||||
case 1:
|
||||
state.attr = state.attr | tui.Bold
|
||||
case 2:
|
||||
@@ -238,13 +483,53 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
|
||||
case 3:
|
||||
state.attr = state.attr | tui.Italic
|
||||
case 4:
|
||||
state.attr = state.attr | tui.Underline
|
||||
if sep == ':' {
|
||||
// SGR 4:N - underline style sub-parameter
|
||||
var subNum int
|
||||
subNum, _, ansiCode = parseAnsiCode(ansiCode)
|
||||
state.attr = state.attr &^ tui.UnderlineStyleMask
|
||||
switch subNum {
|
||||
case 0:
|
||||
state.attr = state.attr &^ tui.Underline
|
||||
case 1:
|
||||
state.attr = state.attr | tui.Underline
|
||||
case 2:
|
||||
state.attr = state.attr | tui.Underline | tui.UlStyleDouble
|
||||
case 3:
|
||||
state.attr = state.attr | tui.Underline | tui.UlStyleCurly
|
||||
case 4:
|
||||
state.attr = state.attr | tui.Underline | tui.UlStyleDotted
|
||||
case 5:
|
||||
state.attr = state.attr | tui.Underline | tui.UlStyleDashed
|
||||
default:
|
||||
state.attr = state.attr | tui.Underline
|
||||
}
|
||||
} else {
|
||||
state.attr = state.attr | tui.Underline
|
||||
}
|
||||
case 5:
|
||||
state.attr = state.attr | tui.Blink
|
||||
case 7:
|
||||
state.attr = state.attr | tui.Reverse
|
||||
case 9:
|
||||
state.attr = state.attr | tui.StrikeThrough
|
||||
case 22:
|
||||
state.attr = state.attr &^ tui.Bold
|
||||
state.attr = state.attr &^ tui.Dim
|
||||
case 23: // tput rmso
|
||||
state.attr = state.attr &^ tui.Italic
|
||||
case 24: // tput rmul
|
||||
state.attr = state.attr &^ tui.Underline
|
||||
state.attr = state.attr &^ tui.UnderlineStyleMask
|
||||
case 25:
|
||||
state.attr = state.attr &^ tui.Blink
|
||||
case 27:
|
||||
state.attr = state.attr &^ tui.Reverse
|
||||
case 29:
|
||||
state.attr = state.attr &^ tui.StrikeThrough
|
||||
case 0:
|
||||
init()
|
||||
reset()
|
||||
state256 = 0
|
||||
default:
|
||||
if num >= 30 && num <= 37 {
|
||||
state.fg = tui.Color(num - 30)
|
||||
@@ -280,6 +565,12 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty sequence: reset
|
||||
if count == 0 {
|
||||
reset()
|
||||
}
|
||||
|
||||
if state256 > 0 {
|
||||
*ptr = -1
|
||||
}
|
||||
|
||||
+380
-8
@@ -1,13 +1,192 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
)
|
||||
|
||||
// The following regular expression will include not all but most of the
|
||||
// frequently used ANSI sequences. This regex is used as a reference for
|
||||
// testing nextAnsiEscapeSequence().
|
||||
//
|
||||
// References:
|
||||
// - https://github.com/gnachman/iTerm2
|
||||
// - https://web.archive.org/web/20090204053813/http://ascii-table.com/ansi-escape-sequences.php
|
||||
// (archived from http://ascii-table.com/ansi-escape-sequences.php)
|
||||
// - https://web.archive.org/web/20090227051140/http://ascii-table.com/ansi-escape-sequences-vt-100.php
|
||||
// (archived from http://ascii-table.com/ansi-escape-sequences-vt-100.php)
|
||||
// - http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
|
||||
// - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||
var ansiRegexReference = regexp.MustCompile("(?:\x1b[\\[()][0-9;:]*[a-zA-Z@]|\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08|\n)")
|
||||
|
||||
func testParserReference(t testing.TB, str string) {
|
||||
t.Helper()
|
||||
|
||||
toSlice := func(start, end int) []int {
|
||||
if start == -1 {
|
||||
return nil
|
||||
}
|
||||
return []int{start, end}
|
||||
}
|
||||
|
||||
s := str
|
||||
for i := 0; ; i++ {
|
||||
got := toSlice(nextAnsiEscapeSequence(s))
|
||||
exp := ansiRegexReference.FindStringIndex(s)
|
||||
|
||||
equal := len(got) == len(exp)
|
||||
if equal {
|
||||
for i := range got {
|
||||
if got[i] != exp[i] {
|
||||
equal = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !equal {
|
||||
var exps, gots []rune
|
||||
if len(got) == 2 {
|
||||
gots = []rune(s[got[0]:got[1]])
|
||||
}
|
||||
if len(exp) == 2 {
|
||||
exps = []rune(s[exp[0]:exp[1]])
|
||||
}
|
||||
t.Errorf("%d: %q: got: %v (%q) want: %v (%q)", i, s, got, gots, exp, exps)
|
||||
return
|
||||
}
|
||||
if len(exp) == 0 {
|
||||
return
|
||||
}
|
||||
s = s[exp[1]:]
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextAnsiEscapeSequence(t *testing.T) {
|
||||
testStrs := []string{
|
||||
"\x1b[0mhello world",
|
||||
"\x1b[1mhello world",
|
||||
"椙\x1b[1m椙",
|
||||
"椙\x1b[1椙m椙",
|
||||
"\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d",
|
||||
"\x1b[1mhello \x1b[Kworld",
|
||||
"hello \x1b[34;45;1mworld",
|
||||
"hello \x1b[34;45;1mwor\x1b[34;45;1mld",
|
||||
"hello \x1b[34;45;1mwor\x1b[0mld",
|
||||
"hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md",
|
||||
"hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md",
|
||||
"hello \x1b[32;1mworld",
|
||||
"hello world",
|
||||
"hello \x1b[0;38;5;200;48;5;100mworld",
|
||||
"\x1b椙",
|
||||
"椙\x08",
|
||||
"\n\x08",
|
||||
"X\x08",
|
||||
"",
|
||||
"\x1b]4;3;rgb:aa/bb/cc\x07 ",
|
||||
"\x1b]4;3;rgb:aa/bb/cc\x1b\\ ",
|
||||
ansiBenchmarkString,
|
||||
}
|
||||
|
||||
for _, s := range testStrs {
|
||||
testParserReference(t, s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextAnsiEscapeSequence_Fuzz_Modified(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
t.Skip("short test")
|
||||
}
|
||||
|
||||
testStrs := []string{
|
||||
"\x1b[0mhello world",
|
||||
"\x1b[1mhello world",
|
||||
"椙\x1b[1m椙",
|
||||
"椙\x1b[1椙m椙",
|
||||
"\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d",
|
||||
"\x1b[1mhello \x1b[Kworld",
|
||||
"hello \x1b[34;45;1mworld",
|
||||
"hello \x1b[34;45;1mwor\x1b[34;45;1mld",
|
||||
"hello \x1b[34;45;1mwor\x1b[0mld",
|
||||
"hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md",
|
||||
"hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md",
|
||||
"hello \x1b[32;1mworld",
|
||||
"hello world",
|
||||
"hello \x1b[0;38;5;200;48;5;100mworld",
|
||||
ansiBenchmarkString,
|
||||
}
|
||||
|
||||
replacementBytes := [...]rune{'\x0e', '\x0f', '\x1b', '\x08'}
|
||||
|
||||
modifyString := func(s string, rr *rand.Rand) string {
|
||||
n := rr.Intn(len(s))
|
||||
b := []rune(s)
|
||||
for ; n >= 0 && len(b) != 0; n-- {
|
||||
i := rr.Intn(len(b))
|
||||
switch x := rr.Intn(4); x {
|
||||
case 0:
|
||||
b = append(b[:i], b[i+1:]...)
|
||||
case 1:
|
||||
j := rr.Intn(len(replacementBytes) - 1)
|
||||
b[i] = replacementBytes[j]
|
||||
case 2:
|
||||
x := rune(rr.Intn(utf8.MaxRune))
|
||||
for !utf8.ValidRune(x) {
|
||||
x = rune(rr.Intn(utf8.MaxRune))
|
||||
}
|
||||
b[i] = x
|
||||
case 3:
|
||||
b[i] = rune(rr.Intn(utf8.MaxRune)) // potentially invalid
|
||||
default:
|
||||
t.Fatalf("unsupported value: %d", x)
|
||||
}
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
rr := rand.New(rand.NewSource(1))
|
||||
for _, s := range testStrs {
|
||||
for i := 1_000; i >= 0; i-- {
|
||||
testParserReference(t, modifyString(s, rr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextAnsiEscapeSequence_Fuzz_Random(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if testing.Short() {
|
||||
t.Skip("short test")
|
||||
}
|
||||
|
||||
randomString := func(rr *rand.Rand) string {
|
||||
numChars := rand.Intn(50)
|
||||
codePoints := make([]rune, numChars)
|
||||
for i := range codePoints {
|
||||
var r rune
|
||||
for range 1000 {
|
||||
r = rune(rr.Intn(utf8.MaxRune))
|
||||
// Allow 10% of runes to be invalid
|
||||
if utf8.ValidRune(r) || rr.Float64() < 0.10 {
|
||||
break
|
||||
}
|
||||
}
|
||||
codePoints[i] = r
|
||||
}
|
||||
return string(codePoints)
|
||||
}
|
||||
|
||||
rr := rand.New(rand.NewSource(1))
|
||||
for range 100_000 {
|
||||
testParserReference(t, randomString(rr))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractColor(t *testing.T) {
|
||||
assert := func(offset ansiOffset, b int32, e int32, fg tui.Color, bg tui.Color, bold bool) {
|
||||
var attr tui.Attr
|
||||
@@ -29,7 +208,7 @@ func TestExtractColor(t *testing.T) {
|
||||
if output != "hello world" {
|
||||
t.Errorf("Invalid output: %s %v", output, []rune(output))
|
||||
}
|
||||
fmt.Println(src, ansiOffsets, clean)
|
||||
t.Log(src, ansiOffsets, clean)
|
||||
assertion(ansiOffsets, state)
|
||||
}
|
||||
|
||||
@@ -156,6 +335,28 @@ func TestExtractColor(t *testing.T) {
|
||||
assert((*offsets)[0], 0, 6, 2, -1, true)
|
||||
assert((*offsets)[1], 6, 11, 200, 100, false)
|
||||
})
|
||||
|
||||
state = nil
|
||||
var color24 tui.Color = (1 << 24) + (180 << 16) + (190 << 8) + 254
|
||||
src = "\x1b[1mhello \x1b[22;1;38:2:180:190:254mworld"
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 2 {
|
||||
t.Fail()
|
||||
}
|
||||
if state.fg != color24 || state.attr != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
assert((*offsets)[0], 0, 6, -1, -1, true)
|
||||
assert((*offsets)[1], 6, 11, color24, -1, true)
|
||||
})
|
||||
|
||||
src = "\x1b]133;A\x1b\\hello \x1b]133;C\x1b\\world"
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
assert((*offsets)[0], 0, 11, color24, -1, true)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAnsiCodeStringConversion(t *testing.T) {
|
||||
@@ -163,12 +364,15 @@ func TestAnsiCodeStringConversion(t *testing.T) {
|
||||
state := interpretCode(code, prevState)
|
||||
if expected != state.ToString() {
|
||||
t.Errorf("expected: %s, actual: %s",
|
||||
strings.Replace(expected, "\x1b[", "\\x1b[", -1),
|
||||
strings.Replace(state.ToString(), "\x1b[", "\\x1b[", -1))
|
||||
strings.ReplaceAll(expected, "\x1b[", "\\x1b["),
|
||||
strings.ReplaceAll(state.ToString(), "\x1b[", "\\x1b["))
|
||||
}
|
||||
}
|
||||
assert("\x1b[m", nil, "")
|
||||
assert("\x1b[m", &ansiState{attr: tui.Blink}, "")
|
||||
assert("\x1b[m", &ansiState{attr: tui.Blink, ul: -1, lbg: -1}, "")
|
||||
assert("\x1b[0m", &ansiState{fg: 4, bg: 4, ul: -1, lbg: -1}, "")
|
||||
assert("\x1b[;m", &ansiState{fg: 4, bg: 4, ul: -1, lbg: -1}, "")
|
||||
assert("\x1b[;;m", &ansiState{fg: 4, bg: 4, ul: -1, lbg: -1}, "")
|
||||
|
||||
assert("\x1b[31m", nil, "\x1b[31;49m")
|
||||
assert("\x1b[41m", nil, "\x1b[39;41m")
|
||||
@@ -176,12 +380,180 @@ func TestAnsiCodeStringConversion(t *testing.T) {
|
||||
assert("\x1b[92m", nil, "\x1b[92;49m")
|
||||
assert("\x1b[102m", nil, "\x1b[39;102m")
|
||||
|
||||
assert("\x1b[31m", &ansiState{fg: 4, bg: 4}, "\x1b[31;44m")
|
||||
assert("\x1b[1;2;31m", &ansiState{fg: 2, bg: -1, attr: tui.Reverse}, "\x1b[1;2;7;31;49m")
|
||||
assert("\x1b[31m", &ansiState{fg: 4, bg: 4, ul: -1, lbg: -1}, "\x1b[31;44m")
|
||||
assert("\x1b[1;2;31m", &ansiState{fg: 2, bg: -1, ul: -1, attr: tui.Reverse, lbg: -1}, "\x1b[1;2;7;31;49m")
|
||||
assert("\x1b[38;5;100;48;5;200m", nil, "\x1b[38;5;100;48;5;200m")
|
||||
assert("\x1b[38:5:100:48:5:200m", nil, "\x1b[38;5;100;48;5;200m")
|
||||
assert("\x1b[48;5;100;38;5;200m", nil, "\x1b[38;5;200;48;5;100m")
|
||||
assert("\x1b[48;5;100;38;2;10;20;30;1m", nil, "\x1b[1;38;2;10;20;30;48;5;100m")
|
||||
assert("\x1b[48;5;100;38;2;10;20;30;7m",
|
||||
&ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1},
|
||||
&ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1, ul: -1},
|
||||
"\x1b[2;3;7;38;2;10;20;30;48;5;100m")
|
||||
|
||||
// Underline styles
|
||||
assert("\x1b[4:3m", nil, "\x1b[4:3;39;49m")
|
||||
assert("\x1b[4:2m", nil, "\x1b[4:2;39;49m")
|
||||
assert("\x1b[4:4m", nil, "\x1b[4:4;39;49m")
|
||||
assert("\x1b[4:5m", nil, "\x1b[4:5;39;49m")
|
||||
assert("\x1b[4:1m", nil, "\x1b[4;39;49m")
|
||||
|
||||
// Underline color (256-color)
|
||||
assert("\x1b[4;58;5;100m", nil, "\x1b[4;39;49;58;5;100m")
|
||||
// Underline color (24-bit)
|
||||
assert("\x1b[4;58;2;255;0;128m", nil, "\x1b[4;39;49;58;2;255;0;128m")
|
||||
// Curly underline + underline color
|
||||
assert("\x1b[4:3;58;2;255;0;0m", nil, "\x1b[4:3;39;49;58;2;255;0;0m")
|
||||
// SGR 59 resets underline color
|
||||
assert("\x1b[59m", &ansiState{fg: 1, bg: -1, ul: 100, lbg: -1}, "\x1b[31;49m")
|
||||
}
|
||||
|
||||
func TestParseAnsiCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
In string
|
||||
Exp string
|
||||
N int
|
||||
Sep byte
|
||||
}{
|
||||
{"123", "", 123, 0},
|
||||
{"1a", "", -1, 0},
|
||||
{"1a;12", "12", -1, ';'},
|
||||
{"12;a", "a", 12, ';'},
|
||||
{"-2", "", -1, 0},
|
||||
// Colon sub-parameters: earliest separator wins (@shtse8)
|
||||
{"4:3", "3", 4, ':'},
|
||||
{"4:3;31", "3;31", 4, ':'},
|
||||
{"38:2:255:0:0", "2:255:0:0", 38, ':'},
|
||||
{"58:5:200", "5:200", 58, ':'},
|
||||
// Semicolon before colon
|
||||
{"4;38:2:0:0:0", "38:2:0:0:0", 4, ';'},
|
||||
}
|
||||
for _, x := range tests {
|
||||
n, sep, s := parseAnsiCode(x.In)
|
||||
if n != x.N || s != x.Exp || sep != x.Sep {
|
||||
t.Fatalf("%q: got: (%d %q %q) want: (%d %q %q)", x.In, n, s, string(sep), x.N, x.Exp, string(x.Sep))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test cases adapted from @shtse8 (PR #4678)
|
||||
func TestInterpretCodeUnderlineStyles(t *testing.T) {
|
||||
// 4:0 = no underline
|
||||
state := interpretCode("\x1b[4:0m", nil)
|
||||
if state.attr&tui.Underline != 0 {
|
||||
t.Error("4:0 should not set underline")
|
||||
}
|
||||
|
||||
// 4:1 = single underline
|
||||
state = interpretCode("\x1b[4:1m", nil)
|
||||
if state.attr&tui.Underline == 0 {
|
||||
t.Error("4:1 should set underline")
|
||||
}
|
||||
|
||||
// 4:3 = curly underline
|
||||
state = interpretCode("\x1b[4:3m", nil)
|
||||
if state.attr&tui.Underline == 0 {
|
||||
t.Error("4:3 should set underline")
|
||||
}
|
||||
if state.attr.UnderlineStyle() != tui.UlStyleCurly {
|
||||
t.Error("4:3 should set curly underline style")
|
||||
}
|
||||
|
||||
// 4:3 should NOT set italic (3 is a sub-param, not SGR 3)
|
||||
if state.attr&tui.Italic != 0 {
|
||||
t.Error("4:3 should not set italic")
|
||||
}
|
||||
|
||||
// 4:2;31 = double underline + red fg
|
||||
state = interpretCode("\x1b[4:2;31m", nil)
|
||||
if state.attr&tui.Underline == 0 {
|
||||
t.Error("4:2;31 should set underline")
|
||||
}
|
||||
if state.fg != 1 {
|
||||
t.Errorf("4:2;31 should set fg to red (1), got %d", state.fg)
|
||||
}
|
||||
if state.attr&tui.Dim != 0 {
|
||||
t.Error("4:2;31 should not set dim")
|
||||
}
|
||||
|
||||
// Plain 4 still works
|
||||
state = interpretCode("\x1b[4m", nil)
|
||||
if state.attr&tui.Underline == 0 {
|
||||
t.Error("4 should set underline")
|
||||
}
|
||||
|
||||
// 4;2 (semicolon) = underline + dim
|
||||
state = interpretCode("\x1b[4;2m", nil)
|
||||
if state.attr&tui.Underline == 0 {
|
||||
t.Error("4;2 should set underline")
|
||||
}
|
||||
if state.attr&tui.Dim == 0 {
|
||||
t.Error("4;2 should set dim")
|
||||
}
|
||||
}
|
||||
|
||||
// Test cases adapted from @shtse8 (PR #4678)
|
||||
func TestInterpretCodeUnderlineColor(t *testing.T) {
|
||||
// 58:2:R:G:B should not affect fg or bg
|
||||
state := interpretCode("\x1b[58:2:255:0:0m", nil)
|
||||
if state.fg != -1 || state.bg != -1 {
|
||||
t.Errorf("58:2:R:G:B should not affect fg/bg, got fg=%d bg=%d", state.fg, state.bg)
|
||||
}
|
||||
|
||||
// 58:5:200 should not affect fg or bg
|
||||
state = interpretCode("\x1b[58:5:200m", nil)
|
||||
if state.fg != -1 || state.bg != -1 {
|
||||
t.Errorf("58:5:N should not affect fg/bg, got fg=%d bg=%d", state.fg, state.bg)
|
||||
}
|
||||
|
||||
// 58:2:R:G:B combined with 38:2:R:G:B should only set fg
|
||||
state = interpretCode("\x1b[58:2:255:0:0;38:2:0:255:0m", nil)
|
||||
expectedFg := tui.Color(1<<24 | 0<<16 | 255<<8 | 0)
|
||||
if state.fg != expectedFg {
|
||||
t.Errorf("expected fg=%d, got %d", expectedFg, state.fg)
|
||||
}
|
||||
if state.bg != -1 {
|
||||
t.Errorf("bg should be -1, got %d", state.bg)
|
||||
}
|
||||
}
|
||||
|
||||
// kernel/bpf/preload/iterators/README
|
||||
const ansiBenchmarkString = "\x1b[38;5;81m\x1b[01;31m\x1b[Kkernel/\x1b[0m\x1b[38:5:81mbpf/" +
|
||||
"\x1b[0m\x1b[38:5:81mpreload/\x1b[0m\x1b[38;5;81miterators/" +
|
||||
"\x1b[0m\x1b[38:5:149mMakefile\x1b[m\x1b[K\x1b[0m"
|
||||
|
||||
func BenchmarkNextAnsiEscapeSequence(b *testing.B) {
|
||||
b.SetBytes(int64(len(ansiBenchmarkString)))
|
||||
for i := 0; i < b.N; i++ {
|
||||
s := ansiBenchmarkString
|
||||
for {
|
||||
_, o := nextAnsiEscapeSequence(s)
|
||||
if o == -1 {
|
||||
break
|
||||
}
|
||||
s = s[o:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Baseline test to compare the speed of nextAnsiEscapeSequence() to the
|
||||
// previously used regex based implementation.
|
||||
func BenchmarkNextAnsiEscapeSequence_Regex(b *testing.B) {
|
||||
b.SetBytes(int64(len(ansiBenchmarkString)))
|
||||
for i := 0; i < b.N; i++ {
|
||||
s := ansiBenchmarkString
|
||||
for {
|
||||
a := ansiRegexReference.FindStringIndex(s)
|
||||
if len(a) == 0 {
|
||||
break
|
||||
}
|
||||
s = s[a[1]:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExtractColor(b *testing.B) {
|
||||
b.SetBytes(int64(len(ansiBenchmarkString)))
|
||||
for i := 0; i < b.N; i++ {
|
||||
extractColor(ansiBenchmarkString, nil, nil)
|
||||
}
|
||||
}
|
||||
|
||||
+34
-17
@@ -2,23 +2,40 @@ package fzf
|
||||
|
||||
import "sync"
|
||||
|
||||
// queryCache associates strings to lists of items
|
||||
type queryCache map[string][]Result
|
||||
// ChunkBitmap is a bitmap with one bit per item in a chunk.
|
||||
type ChunkBitmap [chunkBitWords]uint64
|
||||
|
||||
// ChunkCache associates Chunk and query string to lists of items
|
||||
// queryCache associates query strings to bitmaps of matching items
|
||||
type queryCache map[string]ChunkBitmap
|
||||
|
||||
// ChunkCache associates Chunk and query string to bitmaps
|
||||
type ChunkCache struct {
|
||||
mutex sync.Mutex
|
||||
cache map[*Chunk]*queryCache
|
||||
}
|
||||
|
||||
// NewChunkCache returns a new ChunkCache
|
||||
func NewChunkCache() ChunkCache {
|
||||
return ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)}
|
||||
func NewChunkCache() *ChunkCache {
|
||||
return &ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)}
|
||||
}
|
||||
|
||||
// Add adds the list to the cache
|
||||
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []Result) {
|
||||
if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax {
|
||||
func (cc *ChunkCache) Clear() {
|
||||
cc.mutex.Lock()
|
||||
cc.cache = make(map[*Chunk]*queryCache)
|
||||
cc.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (cc *ChunkCache) retire(chunk ...*Chunk) {
|
||||
cc.mutex.Lock()
|
||||
for _, c := range chunk {
|
||||
delete(cc.cache, c)
|
||||
}
|
||||
cc.mutex.Unlock()
|
||||
}
|
||||
|
||||
// Add stores the bitmap for the given chunk and key
|
||||
func (cc *ChunkCache) Add(chunk *Chunk, key string, bitmap ChunkBitmap, matchCount int) {
|
||||
if len(key) == 0 || !chunk.IsFull() || matchCount > queryCacheMax {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -30,11 +47,11 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []Result) {
|
||||
cc.cache[chunk] = &queryCache{}
|
||||
qc = cc.cache[chunk]
|
||||
}
|
||||
(*qc)[key] = list
|
||||
(*qc)[key] = bitmap
|
||||
}
|
||||
|
||||
// Lookup is called to lookup ChunkCache
|
||||
func (cc *ChunkCache) Lookup(chunk *Chunk, key string) []Result {
|
||||
// Lookup returns the bitmap for the exact key
|
||||
func (cc *ChunkCache) Lookup(chunk *Chunk, key string) *ChunkBitmap {
|
||||
if len(key) == 0 || !chunk.IsFull() {
|
||||
return nil
|
||||
}
|
||||
@@ -44,15 +61,15 @@ func (cc *ChunkCache) Lookup(chunk *Chunk, key string) []Result {
|
||||
|
||||
qc, ok := cc.cache[chunk]
|
||||
if ok {
|
||||
list, ok := (*qc)[key]
|
||||
if ok {
|
||||
return list
|
||||
if bm, ok := (*qc)[key]; ok {
|
||||
return &bm
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cc *ChunkCache) Search(chunk *Chunk, key string) []Result {
|
||||
// Search finds the bitmap for the longest prefix or suffix of the key
|
||||
func (cc *ChunkCache) Search(chunk *Chunk, key string) *ChunkBitmap {
|
||||
if len(key) == 0 || !chunk.IsFull() {
|
||||
return nil
|
||||
}
|
||||
@@ -72,8 +89,8 @@ func (cc *ChunkCache) Search(chunk *Chunk, key string) []Result {
|
||||
prefix := key[:len(key)-idx]
|
||||
suffix := key[idx:]
|
||||
for _, substr := range [2]string{prefix, suffix} {
|
||||
if cached, found := (*qc)[substr]; found {
|
||||
return cached
|
||||
if bm, found := (*qc)[substr]; found {
|
||||
return &bm
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-11
@@ -6,34 +6,34 @@ func TestChunkCache(t *testing.T) {
|
||||
cache := NewChunkCache()
|
||||
chunk1p := &Chunk{}
|
||||
chunk2p := &Chunk{count: chunkSize}
|
||||
items1 := []Result{Result{}}
|
||||
items2 := []Result{Result{}, Result{}}
|
||||
cache.Add(chunk1p, "foo", items1)
|
||||
cache.Add(chunk2p, "foo", items1)
|
||||
cache.Add(chunk2p, "bar", items2)
|
||||
bm1 := ChunkBitmap{1}
|
||||
bm2 := ChunkBitmap{1, 2}
|
||||
cache.Add(chunk1p, "foo", bm1, 1)
|
||||
cache.Add(chunk2p, "foo", bm1, 1)
|
||||
cache.Add(chunk2p, "bar", bm2, 2)
|
||||
|
||||
{ // chunk1 is not full
|
||||
cached := cache.Lookup(chunk1p, "foo")
|
||||
if cached != nil {
|
||||
t.Error("Cached disabled for non-empty chunks", cached)
|
||||
t.Error("Cached disabled for non-full chunks", cached)
|
||||
}
|
||||
}
|
||||
{
|
||||
cached := cache.Lookup(chunk2p, "foo")
|
||||
if cached == nil || len(cached) != 1 {
|
||||
t.Error("Expected 1 item cached", cached)
|
||||
if cached == nil || cached[0] != 1 {
|
||||
t.Error("Expected bitmap cached", cached)
|
||||
}
|
||||
}
|
||||
{
|
||||
cached := cache.Lookup(chunk2p, "bar")
|
||||
if cached == nil || len(cached) != 2 {
|
||||
t.Error("Expected 2 items cached", cached)
|
||||
if cached == nil || cached[1] != 2 {
|
||||
t.Error("Expected bitmap cached", cached)
|
||||
}
|
||||
}
|
||||
{
|
||||
cached := cache.Lookup(chunk1p, "foobar")
|
||||
if cached != nil {
|
||||
t.Error("Expected 0 item cached", cached)
|
||||
t.Error("Expected nil cached", cached)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+88
-7
@@ -16,14 +16,16 @@ type ChunkList struct {
|
||||
chunks []*Chunk
|
||||
mutex sync.Mutex
|
||||
trans ItemBuilder
|
||||
cache *ChunkCache
|
||||
}
|
||||
|
||||
// NewChunkList returns a new ChunkList
|
||||
func NewChunkList(trans ItemBuilder) *ChunkList {
|
||||
func NewChunkList(cache *ChunkCache, trans ItemBuilder) *ChunkList {
|
||||
return &ChunkList{
|
||||
chunks: []*Chunk{},
|
||||
mutex: sync.Mutex{},
|
||||
trans: trans}
|
||||
trans: trans,
|
||||
cache: cache}
|
||||
}
|
||||
|
||||
func (c *Chunk) push(trans ItemBuilder, data []byte) bool {
|
||||
@@ -39,16 +41,42 @@ func (c *Chunk) IsFull() bool {
|
||||
return c.count == chunkSize
|
||||
}
|
||||
|
||||
func (c *Chunk) lastIndex(minValue int32) int32 {
|
||||
if c.count == 0 {
|
||||
return minValue
|
||||
}
|
||||
return c.items[c.count-1].Index() + 1 // Exclusive
|
||||
}
|
||||
|
||||
func (cl *ChunkList) lastChunk() *Chunk {
|
||||
return cl.chunks[len(cl.chunks)-1]
|
||||
}
|
||||
|
||||
// GetItems returns the first n items from the given chunks
|
||||
func GetItems(chunks []*Chunk, n int) []Item {
|
||||
items := make([]Item, 0, n)
|
||||
for _, chunk := range chunks {
|
||||
for i := 0; i < chunk.count && len(items) < n; i++ {
|
||||
items = append(items, chunk.items[i])
|
||||
}
|
||||
if len(items) >= n {
|
||||
break
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// CountItems returns the total number of Items
|
||||
func CountItems(cs []*Chunk) int {
|
||||
if len(cs) == 0 {
|
||||
return 0
|
||||
}
|
||||
return chunkSize*(len(cs)-1) + cs[len(cs)-1].count
|
||||
if len(cs) == 1 {
|
||||
return cs[0].count
|
||||
}
|
||||
|
||||
// First chunk might not be full due to --tail=N
|
||||
return cs[0].count + chunkSize*(len(cs)-2) + cs[len(cs)-1].count
|
||||
}
|
||||
|
||||
// Push adds the item to the list
|
||||
@@ -71,19 +99,72 @@ func (cl *ChunkList) Clear() {
|
||||
cl.mutex.Unlock()
|
||||
}
|
||||
|
||||
// Snapshot returns immutable snapshot of the ChunkList
|
||||
func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
|
||||
// ForEachItem iterates all items and applies fn to each one.
|
||||
// The done callback runs under the lock to safely update shared state.
|
||||
func (cl *ChunkList) ForEachItem(fn func(*Item), done func()) {
|
||||
cl.mutex.Lock()
|
||||
for _, chunk := range cl.chunks {
|
||||
for i := 0; i < chunk.count; i++ {
|
||||
fn(&chunk.items[i])
|
||||
}
|
||||
}
|
||||
if done != nil {
|
||||
done()
|
||||
}
|
||||
cl.mutex.Unlock()
|
||||
}
|
||||
|
||||
// Snapshot returns immutable snapshot of the ChunkList
|
||||
func (cl *ChunkList) Snapshot(tail int) ([]*Chunk, int, bool) {
|
||||
cl.mutex.Lock()
|
||||
|
||||
changed := false
|
||||
if tail > 0 && CountItems(cl.chunks) > tail {
|
||||
changed = true
|
||||
// Find the number of chunks to keep
|
||||
numChunks := 0
|
||||
for left, i := tail, len(cl.chunks)-1; left > 0 && i >= 0; i-- {
|
||||
numChunks++
|
||||
left -= cl.chunks[i].count
|
||||
}
|
||||
|
||||
// Copy the chunks to keep
|
||||
ret := make([]*Chunk, numChunks)
|
||||
minIndex := len(cl.chunks) - numChunks
|
||||
cl.cache.retire(cl.chunks[:minIndex]...)
|
||||
copy(ret, cl.chunks[minIndex:])
|
||||
|
||||
for left, i := tail, len(ret)-1; i >= 0; i-- {
|
||||
chunk := ret[i]
|
||||
if chunk.count > left {
|
||||
newChunk := *chunk
|
||||
newChunk.count = left
|
||||
oldCount := chunk.count
|
||||
for i := 0; i < left; i++ {
|
||||
newChunk.items[i] = chunk.items[oldCount-left+i]
|
||||
}
|
||||
ret[i] = &newChunk
|
||||
cl.cache.retire(chunk)
|
||||
break
|
||||
}
|
||||
left -= chunk.count
|
||||
}
|
||||
cl.chunks = ret
|
||||
}
|
||||
|
||||
ret := make([]*Chunk, len(cl.chunks))
|
||||
copy(ret, cl.chunks)
|
||||
|
||||
// Duplicate the last chunk
|
||||
// Duplicate the first and the last chunk
|
||||
if cnt := len(ret); cnt > 0 {
|
||||
if tail > 0 && cnt > 1 {
|
||||
newChunk := *ret[0]
|
||||
ret[0] = &newChunk
|
||||
}
|
||||
newChunk := *ret[cnt-1]
|
||||
ret[cnt-1] = &newChunk
|
||||
}
|
||||
|
||||
cl.mutex.Unlock()
|
||||
return ret, CountItems(ret)
|
||||
return ret, CountItems(ret), changed
|
||||
}
|
||||
|
||||
+42
-6
@@ -11,13 +11,13 @@ func TestChunkList(t *testing.T) {
|
||||
// FIXME global
|
||||
sortCriteria = []criterion{byScore, byLength}
|
||||
|
||||
cl := NewChunkList(func(item *Item, s []byte) bool {
|
||||
cl := NewChunkList(NewChunkCache(), func(item *Item, s []byte) bool {
|
||||
item.text = util.ToChars(s)
|
||||
return true
|
||||
})
|
||||
|
||||
// Snapshot
|
||||
snapshot, count := cl.Snapshot()
|
||||
snapshot, count, _ := cl.Snapshot(0)
|
||||
if len(snapshot) > 0 || count > 0 {
|
||||
t.Error("Snapshot should be empty now")
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func TestChunkList(t *testing.T) {
|
||||
}
|
||||
|
||||
// But the new snapshot should contain the added items
|
||||
snapshot, count = cl.Snapshot()
|
||||
snapshot, count, _ = cl.Snapshot(0)
|
||||
if len(snapshot) != 1 && count != 2 {
|
||||
t.Error("Snapshot should not be empty now")
|
||||
}
|
||||
@@ -51,8 +51,8 @@ func TestChunkList(t *testing.T) {
|
||||
}
|
||||
|
||||
// Add more data
|
||||
for i := 0; i < chunkSize*2; i++ {
|
||||
cl.Push([]byte(fmt.Sprintf("item %d", i)))
|
||||
for i := range chunkSize * 2 {
|
||||
cl.Push(fmt.Appendf(nil, "item %d", i))
|
||||
}
|
||||
|
||||
// Previous snapshot should remain the same
|
||||
@@ -61,7 +61,7 @@ func TestChunkList(t *testing.T) {
|
||||
}
|
||||
|
||||
// New snapshot
|
||||
snapshot, count = cl.Snapshot()
|
||||
snapshot, count, _ = cl.Snapshot(0)
|
||||
if len(snapshot) != 3 || !snapshot[0].IsFull() ||
|
||||
!snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 {
|
||||
t.Error("Expected two full chunks and one more chunk")
|
||||
@@ -78,3 +78,39 @@ func TestChunkList(t *testing.T) {
|
||||
t.Error("Unexpected number of items:", lastChunkCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkListTail(t *testing.T) {
|
||||
cl := NewChunkList(NewChunkCache(), func(item *Item, s []byte) bool {
|
||||
item.text = util.ToChars(s)
|
||||
return true
|
||||
})
|
||||
total := chunkSize*2 + chunkSize/2
|
||||
for i := range total {
|
||||
cl.Push(fmt.Appendf(nil, "item %d", i))
|
||||
}
|
||||
|
||||
snapshot, count, changed := cl.Snapshot(0)
|
||||
assertCount := func(expected int, shouldChange bool) {
|
||||
if count != expected || CountItems(snapshot) != expected {
|
||||
t.Errorf("Unexpected count: %d (expected: %d)", count, expected)
|
||||
}
|
||||
if changed != shouldChange {
|
||||
t.Error("Unexpected change status")
|
||||
}
|
||||
}
|
||||
assertCount(total, false)
|
||||
|
||||
tail := chunkSize + chunkSize/2
|
||||
snapshot, count, changed = cl.Snapshot(tail)
|
||||
assertCount(tail, true)
|
||||
|
||||
snapshot, count, changed = cl.Snapshot(tail)
|
||||
assertCount(tail, false)
|
||||
|
||||
snapshot, count, changed = cl.Snapshot(0)
|
||||
assertCount(tail, false)
|
||||
|
||||
tail = chunkSize / 2
|
||||
snapshot, count, changed = cl.Snapshot(tail)
|
||||
assertCount(tail, true)
|
||||
}
|
||||
|
||||
+19
-29
@@ -2,22 +2,19 @@ package fzf
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
const (
|
||||
// Current version
|
||||
version = "0.20.0"
|
||||
|
||||
// Core
|
||||
coordinatorDelayMax time.Duration = 100 * time.Millisecond
|
||||
coordinatorDelayStep time.Duration = 10 * time.Millisecond
|
||||
|
||||
// Reader
|
||||
readerBufferSize = 64 * 1024
|
||||
readerSlabSize = 128 * 1024
|
||||
readerPollIntervalMin = 10 * time.Millisecond
|
||||
readerPollIntervalStep = 5 * time.Millisecond
|
||||
readerPollIntervalMax = 50 * time.Millisecond
|
||||
@@ -25,25 +22,30 @@ const (
|
||||
// Terminal
|
||||
initialDelay = 20 * time.Millisecond
|
||||
initialDelayTac = 100 * time.Millisecond
|
||||
spinnerDuration = 200 * time.Millisecond
|
||||
spinnerDuration = 100 * time.Millisecond
|
||||
previewCancelWait = 500 * time.Millisecond
|
||||
maxPatternLength = 300
|
||||
previewChunkDelay = 100 * time.Millisecond
|
||||
previewDelayed = 500 * time.Millisecond
|
||||
maxPatternLength = 1000
|
||||
maxMulti = math.MaxInt32
|
||||
|
||||
// Background processes
|
||||
maxBgProcesses = 30
|
||||
maxBgProcessesPerAction = 3
|
||||
|
||||
// Matcher
|
||||
numPartitionsMultiplier = 8
|
||||
maxPartitions = 32
|
||||
progressMinDuration = 200 * time.Millisecond
|
||||
progressMinDuration = 200 * time.Millisecond
|
||||
|
||||
// Capacity of each chunk
|
||||
chunkSize int = 100
|
||||
chunkSize int = 1024
|
||||
chunkBitWords = (chunkSize + 63) / 64
|
||||
|
||||
// Pre-allocated memory slices to minimize GC
|
||||
slab16Size int = 100 * 1024 // 200KB * 32 = 12.8MB
|
||||
slab32Size int = 2048 // 8KB * 32 = 256KB
|
||||
|
||||
// Do not cache results of low selectivity queries
|
||||
queryCacheMax int = chunkSize / 5
|
||||
queryCacheMax int = chunkSize / 2
|
||||
|
||||
// Not to cache mergers with large lists
|
||||
mergerCacheMax int = 100000
|
||||
@@ -55,18 +57,6 @@ const (
|
||||
defaultJumpLabels string = "asdfghjklqwertyuiopzxcvbnm1234567890ASDFGHJKLQWERTYUIOPZXCVBNM`~;:,<.>/?'\"!@#$%^&*()[{]}-_=+"
|
||||
)
|
||||
|
||||
var defaultCommand string
|
||||
|
||||
func init() {
|
||||
if !util.IsWindows() {
|
||||
defaultCommand = `set -o pipefail; command find -L . -mindepth 1 \( -path '*/\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \) -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-`
|
||||
} else if os.Getenv("TERM") == "cygwin" {
|
||||
defaultCommand = `sh -c "command find -L . -mindepth 1 -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-"`
|
||||
} else {
|
||||
defaultCommand = `for /r %P in (*) do @(set "_curfile=%P" & set "_curfile=!_curfile:%__CD__%=!" & echo !_curfile!)`
|
||||
}
|
||||
}
|
||||
|
||||
// fzf events
|
||||
const (
|
||||
EvtReadNew util.EventType = iota
|
||||
@@ -74,14 +64,14 @@ const (
|
||||
EvtSearchNew
|
||||
EvtSearchProgress
|
||||
EvtSearchFin
|
||||
EvtHeader
|
||||
EvtReady
|
||||
EvtQuit
|
||||
)
|
||||
|
||||
const (
|
||||
exitCancel = -1
|
||||
exitOk = 0
|
||||
exitNoMatch = 1
|
||||
exitError = 2
|
||||
exitInterrupt = 130
|
||||
ExitOk = 0
|
||||
ExitNoMatch = 1
|
||||
ExitError = 2
|
||||
ExitBecome = 126
|
||||
ExitInterrupt = 130
|
||||
)
|
||||
|
||||
+452
-156
@@ -1,35 +1,14 @@
|
||||
/*
|
||||
Package fzf implements fzf, a command-line fuzzy finder.
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
// Package fzf implements fzf, a command-line fuzzy finder.
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
@@ -39,23 +18,69 @@ Reader -> EvtReadNew -> Matcher (restart)
|
||||
Terminal -> EvtSearchNew:bool -> Matcher (restart)
|
||||
Matcher -> EvtSearchProgress -> Terminal (update info)
|
||||
Matcher -> EvtSearchFin -> Terminal (update list)
|
||||
Matcher -> EvtHeader -> Terminal (update header)
|
||||
*/
|
||||
|
||||
type revision struct {
|
||||
major int
|
||||
minor int
|
||||
}
|
||||
|
||||
func (r *revision) bumpMajor() {
|
||||
r.major++
|
||||
r.minor = 0
|
||||
}
|
||||
|
||||
func (r *revision) bumpMinor() {
|
||||
r.minor++
|
||||
}
|
||||
|
||||
func (r revision) compatible(other revision) bool {
|
||||
return r.major == other.major
|
||||
}
|
||||
|
||||
func buildItemTransformer(opts *Options) func(*Item) string {
|
||||
if opts.AcceptNth != nil {
|
||||
fn := opts.AcceptNth(opts.Delimiter)
|
||||
return func(item *Item) string {
|
||||
return item.acceptNth(opts.Ansi, opts.Delimiter, fn)
|
||||
}
|
||||
}
|
||||
return func(item *Item) string {
|
||||
return item.AsString(opts.Ansi)
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts fzf
|
||||
func Run(opts *Options, revision string) {
|
||||
func Run(opts *Options) (int, error) {
|
||||
if opts.Filter == nil {
|
||||
if opts.useTmux() {
|
||||
return runTmux(os.Args, opts)
|
||||
}
|
||||
if opts.useZellij() {
|
||||
return runZellij(os.Args, opts)
|
||||
}
|
||||
|
||||
if needWinpty(opts) {
|
||||
return runWinpty(os.Args, opts)
|
||||
}
|
||||
}
|
||||
|
||||
if err := postProcessOptions(opts); err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
|
||||
defer util.RunAtExitFuncs()
|
||||
|
||||
// Output channel given
|
||||
if opts.Output != nil {
|
||||
opts.Printer = func(str string) {
|
||||
opts.Output <- str
|
||||
}
|
||||
}
|
||||
|
||||
sort := opts.Sort > 0
|
||||
sortCriteria = opts.Criteria
|
||||
|
||||
if opts.Version {
|
||||
if len(revision) > 0 {
|
||||
fmt.Printf("%s (%s)\n", version, revision)
|
||||
} else {
|
||||
fmt.Println(version)
|
||||
}
|
||||
os.Exit(exitOk)
|
||||
}
|
||||
|
||||
// Event channel
|
||||
eventBox := util.NewEventBox()
|
||||
|
||||
@@ -66,67 +91,82 @@ func Run(opts *Options, revision string) {
|
||||
|
||||
var lineAnsiState, prevLineAnsiState *ansiState
|
||||
if opts.Ansi {
|
||||
if opts.Theme != nil {
|
||||
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||
prevLineAnsiState = lineAnsiState
|
||||
trimmed, offsets, newState := extractColor(string(data), lineAnsiState, nil)
|
||||
lineAnsiState = newState
|
||||
return util.ToChars([]byte(trimmed)), offsets
|
||||
}
|
||||
} else {
|
||||
// When color is disabled but ansi option is given,
|
||||
// we simply strip out ANSI codes from the input
|
||||
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||
trimmed, _, _ := extractColor(string(data), nil, nil)
|
||||
return util.ToChars([]byte(trimmed)), nil
|
||||
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||
prevLineAnsiState = lineAnsiState
|
||||
trimmed, offsets, newState := extractColor(byteString(data), lineAnsiState, nil)
|
||||
lineAnsiState = newState
|
||||
|
||||
// Full line background is found. Add a special marker.
|
||||
if offsets != nil && newState != nil && len(*offsets) > 0 && newState.lbg >= 0 {
|
||||
marker := (*offsets)[len(*offsets)-1]
|
||||
marker.offset[0] = marker.offset[1]
|
||||
marker.color.bg = newState.lbg
|
||||
marker.color.attr = marker.color.attr | tui.FullBg
|
||||
newOffsets := append(*offsets, marker)
|
||||
offsets = &newOffsets
|
||||
|
||||
// Reset the full-line background color
|
||||
lineAnsiState.lbg = -1
|
||||
}
|
||||
return util.ToChars(stringBytes(trimmed)), offsets
|
||||
}
|
||||
}
|
||||
|
||||
// Chunk list
|
||||
cache := NewChunkCache()
|
||||
var chunkList *ChunkList
|
||||
var itemIndex int32
|
||||
header := make([]string, 0, opts.HeaderLines)
|
||||
if len(opts.WithNth) == 0 {
|
||||
chunkList = NewChunkList(func(item *Item, data []byte) bool {
|
||||
if len(header) < opts.HeaderLines {
|
||||
header = append(header, string(data))
|
||||
eventBox.Set(EvtHeader, header)
|
||||
return false
|
||||
// transformItem applies with-nth transformation to an item's raw data.
|
||||
// It handles ANSI token propagation using prevLineAnsiState for cross-line continuity.
|
||||
transformItem := func(item *Item, data []byte, transformer func([]Token, int32) string, index int32) {
|
||||
tokens := Tokenize(byteString(data), opts.Delimiter)
|
||||
if opts.Ansi && len(tokens) > 1 {
|
||||
var ansiState *ansiState
|
||||
if prevLineAnsiState != nil {
|
||||
ansiStateDup := *prevLineAnsiState
|
||||
ansiState = &ansiStateDup
|
||||
}
|
||||
for _, token := range tokens {
|
||||
prevAnsiState := ansiState
|
||||
_, _, ansiState = extractColor(token.text.ToString(), ansiState, nil)
|
||||
if prevAnsiState != nil {
|
||||
token.text.Prepend("\x1b[m" + prevAnsiState.ToString())
|
||||
} else {
|
||||
token.text.Prepend("\x1b[m")
|
||||
}
|
||||
}
|
||||
}
|
||||
transformed := transformer(tokens, index)
|
||||
item.text, item.colors = ansiProcessor(stringBytes(transformed))
|
||||
|
||||
// We should not trim trailing whitespaces with background colors
|
||||
var maxColorOffset int32
|
||||
if item.colors != nil {
|
||||
for _, ansi := range *item.colors {
|
||||
if ansi.color.bg >= 0 {
|
||||
maxColorOffset = max(maxColorOffset, ansi.offset[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
item.text.TrimTrailingWhitespaces(int(maxColorOffset))
|
||||
}
|
||||
|
||||
var nthTransformer func([]Token, int32) string
|
||||
if opts.WithNth == nil {
|
||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||
item.text, item.colors = ansiProcessor(data)
|
||||
item.text.Index = itemIndex
|
||||
itemIndex++
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
chunkList = NewChunkList(func(item *Item, data []byte) bool {
|
||||
tokens := Tokenize(string(data), opts.Delimiter)
|
||||
if opts.Ansi && opts.Theme != nil && len(tokens) > 1 {
|
||||
var ansiState *ansiState
|
||||
if prevLineAnsiState != nil {
|
||||
ansiStateDup := *prevLineAnsiState
|
||||
ansiState = &ansiStateDup
|
||||
}
|
||||
for _, token := range tokens {
|
||||
prevAnsiState := ansiState
|
||||
_, _, ansiState = extractColor(token.text.ToString(), ansiState, nil)
|
||||
if prevAnsiState != nil {
|
||||
token.text.Prepend("\x1b[m" + prevAnsiState.ToString())
|
||||
} else {
|
||||
token.text.Prepend("\x1b[m")
|
||||
}
|
||||
}
|
||||
nthTransformer = opts.WithNth(opts.Delimiter)
|
||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||
if nthTransformer == nil {
|
||||
item.text, item.colors = ansiProcessor(data)
|
||||
} else {
|
||||
transformItem(item, data, nthTransformer, itemIndex)
|
||||
}
|
||||
trans := Transform(tokens, opts.WithNth)
|
||||
transformed := joinTokens(trans)
|
||||
if len(header) < opts.HeaderLines {
|
||||
header = append(header, transformed)
|
||||
eventBox.Set(EvtHeader, header)
|
||||
return false
|
||||
}
|
||||
item.text, item.colors = ansiProcessor([]byte(transformed))
|
||||
item.text.TrimTrailingWhitespaces()
|
||||
item.text.Index = itemIndex
|
||||
item.origText = &data
|
||||
itemIndex++
|
||||
@@ -134,33 +174,84 @@ func Run(opts *Options, revision string) {
|
||||
})
|
||||
}
|
||||
|
||||
// Process executor
|
||||
executor := util.NewExecutor(opts.WithShell)
|
||||
|
||||
// Terminal I/O
|
||||
var terminal *Terminal
|
||||
var err error
|
||||
var initialEnv []string
|
||||
initialReload := opts.extractReloadOnStart()
|
||||
if opts.Filter == nil {
|
||||
terminal, err = NewTerminal(opts, eventBox, executor)
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
if len(initialReload) > 0 {
|
||||
var temps []string
|
||||
initialReload, temps = terminal.replacePlaceholderInInitialCommand(initialReload)
|
||||
initialEnv = terminal.environ()
|
||||
defer removeFiles(temps)
|
||||
}
|
||||
}
|
||||
|
||||
// Reader
|
||||
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
|
||||
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync && opts.Bench == 0
|
||||
var reader *Reader
|
||||
var ingestionStart time.Time
|
||||
if !streamingFilter {
|
||||
reader = NewReader(func(data []byte) bool {
|
||||
return chunkList.Push(data)
|
||||
}, eventBox, opts.ReadZero, opts.Filter == nil)
|
||||
go reader.ReadSource()
|
||||
}, eventBox, executor, opts.ReadZero, opts.Filter == nil)
|
||||
|
||||
ingestionStart = time.Now()
|
||||
readyChan := make(chan bool)
|
||||
go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv, readyChan)
|
||||
<-readyChan
|
||||
}
|
||||
|
||||
// Matcher
|
||||
forward := true
|
||||
for _, cri := range opts.Criteria[1:] {
|
||||
if cri == byEnd {
|
||||
withPos := false
|
||||
for idx := len(opts.Criteria) - 1; idx > 0; idx-- {
|
||||
switch opts.Criteria[idx] {
|
||||
case byChunk:
|
||||
withPos = true
|
||||
case byEnd:
|
||||
forward = false
|
||||
case byBegin:
|
||||
forward = true
|
||||
case byPathname:
|
||||
withPos = true
|
||||
forward = false
|
||||
break
|
||||
}
|
||||
if cri == byBegin {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
nth := opts.Nth
|
||||
inputRevision := revision{}
|
||||
snapshotRevision := revision{}
|
||||
patternCache := make(map[string]*Pattern)
|
||||
denyMutex := sync.Mutex{}
|
||||
denylist := make(map[int32]struct{})
|
||||
clearDenylist := func() {
|
||||
denyMutex.Lock()
|
||||
if len(denylist) > 0 {
|
||||
patternCache = make(map[string]*Pattern)
|
||||
}
|
||||
denylist = make(map[int32]struct{})
|
||||
denyMutex.Unlock()
|
||||
}
|
||||
headerLines := int32(opts.HeaderLines)
|
||||
headerUpdated := false
|
||||
patternBuilder := func(runes []rune) *Pattern {
|
||||
return BuildPattern(
|
||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward,
|
||||
opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
|
||||
denyMutex.Lock()
|
||||
denylistCopy := maps.Clone(denylist)
|
||||
denyMutex.Unlock()
|
||||
return BuildPattern(cache, patternCache,
|
||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
||||
opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy, headerLines)
|
||||
}
|
||||
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)
|
||||
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision, opts.Threads)
|
||||
|
||||
// Filtering mode
|
||||
if opts.Filter != nil {
|
||||
@@ -171,38 +262,89 @@ func Run(opts *Options, revision string) {
|
||||
pattern := patternBuilder([]rune(*opts.Filter))
|
||||
matcher.sort = pattern.sortable
|
||||
|
||||
transformer := buildItemTransformer(opts)
|
||||
|
||||
found := false
|
||||
if streamingFilter {
|
||||
slab := util.MakeSlab(slab16Size, slab32Size)
|
||||
mutex := sync.Mutex{}
|
||||
reader := NewReader(
|
||||
func(runes []byte) bool {
|
||||
item := Item{}
|
||||
if chunkList.trans(&item, runes) {
|
||||
if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
|
||||
opts.Printer(item.text.ToString())
|
||||
if item.Index() < headerLines {
|
||||
return false
|
||||
}
|
||||
mutex.Lock()
|
||||
if result, _, _ := pattern.MatchItem(&item, false, slab); result.item != nil {
|
||||
opts.Printer(transformer(&item))
|
||||
found = true
|
||||
}
|
||||
mutex.Unlock()
|
||||
}
|
||||
return false
|
||||
}, eventBox, opts.ReadZero, false)
|
||||
reader.ReadSource()
|
||||
}, eventBox, executor, opts.ReadZero, false)
|
||||
reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv, nil)
|
||||
} else {
|
||||
eventBox.Unwatch(EvtReadNew)
|
||||
eventBox.WaitFor(EvtReadFin)
|
||||
ingestionTime := time.Since(ingestionStart)
|
||||
|
||||
snapshot, _ := chunkList.Snapshot()
|
||||
merger, _ := matcher.scan(MatchRequest{
|
||||
// NOTE: Streaming filter is inherently not compatible with --tail
|
||||
snapshot, _, _ := chunkList.Snapshot(opts.Tail)
|
||||
|
||||
if opts.Bench > 0 {
|
||||
// Benchmark mode: repeat scan for the given duration
|
||||
totalItems := CountItems(snapshot)
|
||||
var matchCount int
|
||||
var times []time.Duration
|
||||
deadline := time.Now().Add(opts.Bench)
|
||||
for time.Now().Before(deadline) {
|
||||
cache.Clear()
|
||||
start := time.Now()
|
||||
result := matcher.scan(MatchRequest{
|
||||
chunks: snapshot,
|
||||
pattern: pattern})
|
||||
times = append(times, time.Since(start))
|
||||
matchCount = result.merger.Length()
|
||||
}
|
||||
// Print stats
|
||||
var total time.Duration
|
||||
minD, maxD := times[0], times[0]
|
||||
for _, d := range times {
|
||||
total += d
|
||||
if d < minD {
|
||||
minD = d
|
||||
}
|
||||
if d > maxD {
|
||||
maxD = d
|
||||
}
|
||||
}
|
||||
avg := total / time.Duration(len(times))
|
||||
selectivity := float64(matchCount) / float64(totalItems) * 100
|
||||
fmt.Printf(" %d iterations avg: %.2fms min: %.2fms max: %.2fms total: %.2fs items: %d matches: %d (%.2f%%) ingestion: %.2fms\n",
|
||||
len(times),
|
||||
float64(avg.Microseconds())/1000,
|
||||
float64(minD.Microseconds())/1000,
|
||||
float64(maxD.Microseconds())/1000,
|
||||
total.Seconds(),
|
||||
totalItems, matchCount, selectivity,
|
||||
float64(ingestionTime.Microseconds())/1000)
|
||||
return ExitOk, nil
|
||||
}
|
||||
|
||||
result := matcher.scan(MatchRequest{
|
||||
chunks: snapshot,
|
||||
pattern: pattern})
|
||||
for i := 0; i < merger.Length(); i++ {
|
||||
opts.Printer(merger.Get(i).item.AsString(opts.Ansi))
|
||||
for i := 0; i < result.merger.Length(); i++ {
|
||||
opts.Printer(transformer(result.merger.Get(i).item))
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if found {
|
||||
os.Exit(exitOk)
|
||||
return ExitOk, nil
|
||||
}
|
||||
os.Exit(exitNoMatch)
|
||||
return ExitNoMatch, nil
|
||||
}
|
||||
|
||||
// Synchronous search
|
||||
@@ -213,38 +355,74 @@ func Run(opts *Options, revision string) {
|
||||
|
||||
// Go interactive
|
||||
go matcher.Loop()
|
||||
defer matcher.Stop()
|
||||
|
||||
// Terminal I/O
|
||||
terminal := NewTerminal(opts, eventBox)
|
||||
deferred := opts.Select1 || opts.Exit0
|
||||
// Handling adaptive height
|
||||
maxFit := 0 // Maximum number of items that can fit on screen
|
||||
padHeight := 0
|
||||
heightUnknown := opts.Height.auto
|
||||
if heightUnknown {
|
||||
maxFit, padHeight = terminal.MaxFitAndPad()
|
||||
}
|
||||
deferred := opts.Select1 || opts.Exit0 || opts.Sync
|
||||
go terminal.Loop()
|
||||
if !deferred {
|
||||
terminal.startChan <- true
|
||||
if !deferred && !heightUnknown {
|
||||
// Start right away
|
||||
terminal.startChan <- fitpad{-1, -1}
|
||||
}
|
||||
|
||||
// Event coordination
|
||||
reading := true
|
||||
clearCache := util.Once(false)
|
||||
clearSelection := util.Once(false)
|
||||
ticks := 0
|
||||
var nextCommand *string
|
||||
restart := func(command string) {
|
||||
reading = true
|
||||
clearCache = util.Once(true)
|
||||
clearSelection = util.Once(true)
|
||||
chunkList.Clear()
|
||||
header = make([]string, 0, opts.HeaderLines)
|
||||
go reader.restart(command)
|
||||
}
|
||||
startTick := 0
|
||||
var nextCommand *commandSpec
|
||||
var nextEnviron []string
|
||||
eventBox.Watch(EvtReadNew)
|
||||
total := 0
|
||||
query := []rune{}
|
||||
determine := func(final bool) {
|
||||
if heightUnknown {
|
||||
items := max(0, total-int(headerLines))
|
||||
if items >= maxFit || final {
|
||||
deferred = false
|
||||
heightUnknown = false
|
||||
terminal.startChan <- fitpad{min(items, maxFit), padHeight}
|
||||
}
|
||||
} else if deferred {
|
||||
deferred = false
|
||||
terminal.startChan <- fitpad{-1, -1}
|
||||
}
|
||||
}
|
||||
|
||||
useSnapshot := false
|
||||
var snapshot []*Chunk
|
||||
var count int
|
||||
restart := func(command commandSpec, environ []string) {
|
||||
if !useSnapshot {
|
||||
clearDenylist()
|
||||
}
|
||||
reading = true
|
||||
headerUpdated = false
|
||||
startTick = ticks
|
||||
chunkList.Clear()
|
||||
itemIndex = 0
|
||||
inputRevision.bumpMajor()
|
||||
readyChan := make(chan bool)
|
||||
go reader.restart(command, environ, readyChan)
|
||||
<-readyChan
|
||||
}
|
||||
|
||||
exitCode := ExitOk
|
||||
stop := false
|
||||
for {
|
||||
delay := true
|
||||
ticks++
|
||||
input := func() []rune {
|
||||
if opts.Phony {
|
||||
return []rune{}
|
||||
paused, input := terminal.Input()
|
||||
if !paused {
|
||||
query = input
|
||||
}
|
||||
return []rune(terminal.Input())
|
||||
return query
|
||||
}
|
||||
eventBox.Wait(func(events *util.Events) {
|
||||
if _, fin := (*events)[EvtReadFin]; fin {
|
||||
@@ -252,41 +430,159 @@ func Run(opts *Options, revision string) {
|
||||
}
|
||||
for evt, value := range *events {
|
||||
switch evt {
|
||||
|
||||
case EvtQuit:
|
||||
if reading {
|
||||
reader.terminate()
|
||||
}
|
||||
quitSignal := value.(quitSignal)
|
||||
exitCode = quitSignal.code
|
||||
err = quitSignal.err
|
||||
stop = true
|
||||
return
|
||||
case EvtReadNew, EvtReadFin:
|
||||
if evt == EvtReadFin && nextCommand != nil {
|
||||
restart(*nextCommand)
|
||||
restart(*nextCommand, nextEnviron)
|
||||
nextCommand = nil
|
||||
nextEnviron = nil
|
||||
break
|
||||
} else {
|
||||
reading = reading && evt == EvtReadNew
|
||||
}
|
||||
snapshot, count := chunkList.Snapshot()
|
||||
terminal.UpdateCount(count, !reading, value.(*string))
|
||||
if opts.Sync {
|
||||
opts.Sync = false
|
||||
terminal.UpdateList(PassMerger(&snapshot, opts.Tac), false)
|
||||
if useSnapshot && evt == EvtReadFin { // reload-sync
|
||||
clearDenylist()
|
||||
useSnapshot = false
|
||||
}
|
||||
if !useSnapshot {
|
||||
if !snapshotRevision.compatible(inputRevision) {
|
||||
query = []rune{}
|
||||
}
|
||||
var changed bool
|
||||
snapshot, count, changed = chunkList.Snapshot(opts.Tail)
|
||||
if changed {
|
||||
inputRevision.bumpMinor()
|
||||
}
|
||||
snapshotRevision = inputRevision
|
||||
}
|
||||
total = count
|
||||
terminal.UpdateCount(max(0, total-int(headerLines)), !reading, value.(*string))
|
||||
if headerLines > 0 && !headerUpdated {
|
||||
terminal.UpdateHeader(GetItems(snapshot, int(headerLines)))
|
||||
headerUpdated = int32(total) >= headerLines
|
||||
}
|
||||
if heightUnknown && !deferred {
|
||||
determine(!reading)
|
||||
}
|
||||
if !useSnapshot || evt == EvtReadFin {
|
||||
matcher.Reset(snapshot, input(), false, !reading, sort, snapshotRevision)
|
||||
}
|
||||
matcher.Reset(snapshot, input(), false, !reading, sort, clearCache())
|
||||
|
||||
case EvtSearchNew:
|
||||
var command *string
|
||||
var command *commandSpec
|
||||
var environ []string
|
||||
var changed bool
|
||||
headerLinesChanged := false
|
||||
withNthChanged := false
|
||||
switch val := value.(type) {
|
||||
case searchRequest:
|
||||
sort = val.sort
|
||||
command = val.command
|
||||
environ = val.environ
|
||||
changed = val.changed
|
||||
bump := false
|
||||
if len(val.denylist) > 0 && val.revision.compatible(inputRevision) {
|
||||
denyMutex.Lock()
|
||||
for _, itemIndex := range val.denylist {
|
||||
denylist[itemIndex] = struct{}{}
|
||||
}
|
||||
denyMutex.Unlock()
|
||||
bump = true
|
||||
}
|
||||
if val.nth != nil {
|
||||
// Change nth and clear caches
|
||||
nth = *val.nth
|
||||
bump = true
|
||||
}
|
||||
if val.headerLines != nil {
|
||||
headerLines = int32(*val.headerLines)
|
||||
headerUpdated = false
|
||||
headerLinesChanged = true
|
||||
bump = true
|
||||
}
|
||||
if val.withNth != nil {
|
||||
newTransformer := val.withNth.fn
|
||||
// Cancel any in-flight scan and block the terminal from reading
|
||||
// items before mutating them in-place. Snapshot shares middle
|
||||
// chunk pointers, so the matcher and terminal can race with us.
|
||||
matcher.CancelScan()
|
||||
terminal.PauseRendering()
|
||||
// Reset cross-line ANSI state before re-processing all items
|
||||
lineAnsiState = nil
|
||||
prevLineAnsiState = nil
|
||||
chunkList.ForEachItem(func(item *Item) {
|
||||
origBytes := *item.origText
|
||||
savedIndex := item.Index()
|
||||
if newTransformer != nil {
|
||||
transformItem(item, origBytes, newTransformer, savedIndex)
|
||||
} else {
|
||||
item.text, item.colors = ansiProcessor(origBytes)
|
||||
}
|
||||
item.text.Index = savedIndex
|
||||
item.transformed = nil
|
||||
}, func() {
|
||||
nthTransformer = newTransformer
|
||||
})
|
||||
terminal.ResumeRendering()
|
||||
matcher.ResumeScan()
|
||||
withNthChanged = true
|
||||
bump = true
|
||||
}
|
||||
if bump {
|
||||
patternCache = make(map[string]*Pattern)
|
||||
cache.Clear()
|
||||
inputRevision.bumpMinor()
|
||||
}
|
||||
if command != nil {
|
||||
useSnapshot = val.sync
|
||||
}
|
||||
}
|
||||
if command != nil {
|
||||
if reading {
|
||||
reader.terminate()
|
||||
nextCommand = command
|
||||
nextEnviron = environ
|
||||
} else {
|
||||
restart(*command)
|
||||
restart(*command, environ)
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
break
|
||||
}
|
||||
snapshot, _ := chunkList.Snapshot()
|
||||
matcher.Reset(snapshot, input(), true, !reading, sort, clearCache())
|
||||
if !useSnapshot {
|
||||
newSnapshot, newCount, changed := chunkList.Snapshot(opts.Tail)
|
||||
if changed {
|
||||
inputRevision.bumpMinor()
|
||||
}
|
||||
// We want to avoid showing empty list when reload is triggered
|
||||
// and the query string is changed at the same time i.e. command != nil && changed
|
||||
if command == nil || newCount > 0 {
|
||||
if snapshotRevision != inputRevision {
|
||||
query = []rune{}
|
||||
}
|
||||
snapshot = newSnapshot
|
||||
snapshotRevision = inputRevision
|
||||
}
|
||||
}
|
||||
if headerLinesChanged {
|
||||
terminal.UpdateCount(max(0, total-int(headerLines)), !reading, nil)
|
||||
if headerLines > 0 {
|
||||
terminal.UpdateHeader(GetItems(snapshot, int(headerLines)))
|
||||
} else {
|
||||
terminal.UpdateHeader(nil)
|
||||
}
|
||||
} else if withNthChanged && headerLines > 0 {
|
||||
terminal.UpdateHeader(GetItems(snapshot, int(headerLines)))
|
||||
}
|
||||
matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision)
|
||||
delay = false
|
||||
|
||||
case EvtSearchProgress:
|
||||
@@ -295,20 +591,15 @@ func Run(opts *Options, revision string) {
|
||||
terminal.UpdateProgress(val)
|
||||
}
|
||||
|
||||
case EvtHeader:
|
||||
headerPadded := make([]string, opts.HeaderLines)
|
||||
copy(headerPadded, value.([]string))
|
||||
terminal.UpdateHeader(headerPadded)
|
||||
|
||||
case EvtSearchFin:
|
||||
switch val := value.(type) {
|
||||
case *Merger:
|
||||
case MatchResult:
|
||||
merger := val.merger
|
||||
if deferred {
|
||||
count := val.Length()
|
||||
count := merger.Length()
|
||||
if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 {
|
||||
deferred = false
|
||||
terminal.startChan <- true
|
||||
} else if val.final {
|
||||
determine(merger.final)
|
||||
} else if merger.final {
|
||||
if opts.Exit0 && count == 0 || opts.Select1 && count == 1 {
|
||||
if opts.PrintQuery {
|
||||
opts.Printer(opts.Query)
|
||||
@@ -316,29 +607,34 @@ func Run(opts *Options, revision string) {
|
||||
if len(opts.Expect) > 0 {
|
||||
opts.Printer("")
|
||||
}
|
||||
for i := 0; i < count; i++ {
|
||||
opts.Printer(val.Get(i).item.AsString(opts.Ansi))
|
||||
transformer := buildItemTransformer(opts)
|
||||
for i := range count {
|
||||
opts.Printer(transformer(merger.Get(i).item))
|
||||
}
|
||||
if count > 0 {
|
||||
os.Exit(exitOk)
|
||||
if count == 0 {
|
||||
exitCode = ExitNoMatch
|
||||
}
|
||||
os.Exit(exitNoMatch)
|
||||
stop = true
|
||||
return
|
||||
}
|
||||
deferred = false
|
||||
terminal.startChan <- true
|
||||
determine(merger.final)
|
||||
}
|
||||
}
|
||||
terminal.UpdateList(val, clearSelection())
|
||||
terminal.UpdateList(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
events.Clear()
|
||||
})
|
||||
if stop {
|
||||
break
|
||||
}
|
||||
if delay && reading {
|
||||
dur := util.DurWithin(
|
||||
time.Duration(ticks)*coordinatorDelayStep,
|
||||
dur := util.Constrain(
|
||||
time.Duration(ticks-startTick)*coordinatorDelayStep,
|
||||
0, coordinatorDelayMax)
|
||||
time.Sleep(dur)
|
||||
}
|
||||
}
|
||||
return exitCode, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func WriteTemporaryFile(data []string, printSep string) string {
|
||||
f, err := os.CreateTemp("", "fzf-temp-*")
|
||||
if err != nil {
|
||||
// Unable to create temporary file
|
||||
// FIXME: Should we terminate the program?
|
||||
return ""
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
f.WriteString(strings.Join(data, printSep))
|
||||
f.WriteString(printSep)
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
func removeFiles(files []string) {
|
||||
for _, filename := range files {
|
||||
os.Remove(filename)
|
||||
}
|
||||
}
|
||||
|
||||
func stringBytes(data string) []byte {
|
||||
return unsafe.Slice(unsafe.StringData(data), len(data))
|
||||
}
|
||||
|
||||
func byteString(data []byte) string {
|
||||
return unsafe.String(unsafe.SliceData(data), len(data))
|
||||
}
|
||||
+4
-5
@@ -2,7 +2,6 @@ package fzf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
@@ -26,12 +25,12 @@ func NewHistory(path string, maxSize int) (*History, error) {
|
||||
}
|
||||
|
||||
// Read history file
|
||||
data, err := ioutil.ReadFile(path)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
// If it doesn't exist, check if we can create a file with the name
|
||||
if os.IsNotExist(err) {
|
||||
data = []byte{}
|
||||
if err := ioutil.WriteFile(path, data, 0600); err != nil {
|
||||
if err := os.WriteFile(path, data, 0600); err != nil {
|
||||
return nil, fmtError(err)
|
||||
}
|
||||
} else {
|
||||
@@ -62,11 +61,11 @@ func (h *History) append(line string) error {
|
||||
lines = lines[len(lines)-h.maxSize:]
|
||||
}
|
||||
h.lines = append(lines, "")
|
||||
return ioutil.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600)
|
||||
return os.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600)
|
||||
}
|
||||
|
||||
func (h *History) override(str string) {
|
||||
// You can update the history but they're not written to the file
|
||||
// You can update the history, but they're not written to the file
|
||||
if h.cursor == len(h.lines)-1 {
|
||||
h.lines[h.cursor] = str
|
||||
} else if h.cursor < len(h.lines)-1 {
|
||||
|
||||
+2
-8
@@ -1,9 +1,7 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
@@ -12,16 +10,12 @@ func TestHistory(t *testing.T) {
|
||||
maxHistory := 50
|
||||
|
||||
// Invalid arguments
|
||||
user, _ := user.Current()
|
||||
var paths []string
|
||||
if runtime.GOOS == "windows" {
|
||||
// GOPATH should exist, so we shouldn't be able to override it
|
||||
paths = []string{os.Getenv("GOPATH")}
|
||||
} else {
|
||||
paths = []string{"/etc", "/proc"}
|
||||
if user.Name != "root" {
|
||||
paths = append(paths, "/etc/sudoers")
|
||||
}
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
@@ -30,7 +24,7 @@ func TestHistory(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
f, _ := ioutil.TempFile("", "fzf-history")
|
||||
f, _ := os.CreateTemp("", "fzf-history")
|
||||
f.Close()
|
||||
|
||||
{ // Append lines
|
||||
@@ -44,7 +38,7 @@ func TestHistory(t *testing.T) {
|
||||
if len(h.lines) != maxHistory+1 {
|
||||
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
|
||||
}
|
||||
for i := 0; i < maxHistory; i++ {
|
||||
for i := range maxHistory {
|
||||
if h.lines[i] != "foobar" {
|
||||
t.Error("Expected: foobar, actual: " + h.lines[i])
|
||||
}
|
||||
|
||||
+17
-2
@@ -1,13 +1,22 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
type transformed struct {
|
||||
// Because nth can be changed dynamically by change-nth action, we need to
|
||||
// keep the revision number at the time of transformation.
|
||||
revision revision
|
||||
tokens []Token
|
||||
}
|
||||
|
||||
// Item represents each input line. 56 bytes.
|
||||
type Item struct {
|
||||
text util.Chars // 32 = 24 + 1 + 1 + 2 + 4
|
||||
transformed *[]Token // 8
|
||||
transformed *transformed // 8
|
||||
origText *[]byte // 8
|
||||
colors *[]ansiOffset // 8
|
||||
}
|
||||
@@ -17,7 +26,7 @@ func (item *Item) Index() int32 {
|
||||
return item.text.Index
|
||||
}
|
||||
|
||||
var minItem = Item{text: util.Chars{Index: -1}}
|
||||
var minItem = Item{text: util.Chars{Index: math.MinInt32}}
|
||||
|
||||
func (item *Item) TrimLength() uint16 {
|
||||
return item.text.TrimLength()
|
||||
@@ -42,3 +51,9 @@ func (item *Item) AsString(stripAnsi bool) string {
|
||||
}
|
||||
return item.text.ToString()
|
||||
}
|
||||
|
||||
func (item *Item) acceptNth(stripAnsi bool, delimiter Delimiter, transformer func([]Token, int32) string) string {
|
||||
tokens := Tokenize(item.AsString(stripAnsi), delimiter)
|
||||
transformed := transformer(tokens, item.Index())
|
||||
return StripLastDelimiter(transformed, delimiter)
|
||||
}
|
||||
|
||||
+126
-90
@@ -3,8 +3,8 @@ package fzf
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
@@ -12,15 +12,30 @@ import (
|
||||
|
||||
// MatchRequest represents a search request
|
||||
type MatchRequest struct {
|
||||
chunks []*Chunk
|
||||
pattern *Pattern
|
||||
final bool
|
||||
sort bool
|
||||
clearCache bool
|
||||
chunks []*Chunk
|
||||
pattern *Pattern
|
||||
final bool
|
||||
sort bool
|
||||
revision revision
|
||||
}
|
||||
|
||||
type MatchResult struct {
|
||||
merger *Merger
|
||||
passMerger *Merger
|
||||
cancelled bool
|
||||
}
|
||||
|
||||
func (mr MatchResult) cacheable() bool {
|
||||
return mr.merger != nil && mr.merger.cacheable()
|
||||
}
|
||||
|
||||
func (mr MatchResult) final() bool {
|
||||
return mr.merger != nil && mr.merger.final
|
||||
}
|
||||
|
||||
// Matcher is responsible for performing search
|
||||
type Matcher struct {
|
||||
cache *ChunkCache
|
||||
patternBuilder func([]rune) *Pattern
|
||||
sort bool
|
||||
tac bool
|
||||
@@ -28,7 +43,11 @@ type Matcher struct {
|
||||
reqBox *util.EventBox
|
||||
partitions int
|
||||
slab []*util.Slab
|
||||
mergerCache map[string]*Merger
|
||||
sortBuf [][]Result
|
||||
mergerCache map[string]MatchResult
|
||||
revision revision
|
||||
scanMutex sync.Mutex
|
||||
cancelScan *util.AtomicBool
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -37,10 +56,14 @@ const (
|
||||
)
|
||||
|
||||
// NewMatcher returns a new Matcher
|
||||
func NewMatcher(patternBuilder func([]rune) *Pattern,
|
||||
sort bool, tac bool, eventBox *util.EventBox) *Matcher {
|
||||
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
|
||||
func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
|
||||
sort bool, tac bool, eventBox *util.EventBox, revision revision, threads int) *Matcher {
|
||||
partitions := runtime.NumCPU()
|
||||
if threads > 0 {
|
||||
partitions = threads
|
||||
}
|
||||
return &Matcher{
|
||||
cache: cache,
|
||||
patternBuilder: patternBuilder,
|
||||
sort: sort,
|
||||
tac: tac,
|
||||
@@ -48,7 +71,10 @@ func NewMatcher(patternBuilder func([]rune) *Pattern,
|
||||
reqBox: util.NewEventBox(),
|
||||
partitions: partitions,
|
||||
slab: make([]*util.Slab, partitions),
|
||||
mergerCache: make(map[string]*Merger)}
|
||||
sortBuf: make([][]Result, partitions),
|
||||
mergerCache: make(map[string]MatchResult),
|
||||
revision: revision,
|
||||
cancelScan: util.NewAtomicBool(false)}
|
||||
}
|
||||
|
||||
// Loop puts Matcher in action
|
||||
@@ -58,8 +84,13 @@ func (m *Matcher) Loop() {
|
||||
for {
|
||||
var request MatchRequest
|
||||
|
||||
stop := false
|
||||
m.reqBox.Wait(func(events *util.Events) {
|
||||
for _, val := range *events {
|
||||
for t, val := range *events {
|
||||
if t == reqQuit {
|
||||
stop = true
|
||||
return
|
||||
}
|
||||
switch val := val.(type) {
|
||||
case MatchRequest:
|
||||
request = val
|
||||
@@ -69,123 +100,109 @@ func (m *Matcher) Loop() {
|
||||
}
|
||||
events.Clear()
|
||||
})
|
||||
if stop {
|
||||
break
|
||||
}
|
||||
|
||||
if request.sort != m.sort || request.clearCache {
|
||||
cacheCleared := false
|
||||
if request.sort != m.sort || request.revision != m.revision {
|
||||
m.sort = request.sort
|
||||
m.mergerCache = make(map[string]*Merger)
|
||||
clearChunkCache()
|
||||
m.mergerCache = make(map[string]MatchResult)
|
||||
if !request.revision.compatible(m.revision) {
|
||||
m.cache.Clear()
|
||||
}
|
||||
m.revision = request.revision
|
||||
cacheCleared = true
|
||||
}
|
||||
|
||||
// Restart search
|
||||
patternString := request.pattern.AsString()
|
||||
var merger *Merger
|
||||
cancelled := false
|
||||
var result MatchResult
|
||||
count := CountItems(request.chunks)
|
||||
|
||||
foundCache := false
|
||||
if count == prevCount {
|
||||
// Look up mergerCache
|
||||
if cached, found := m.mergerCache[patternString]; found {
|
||||
foundCache = true
|
||||
merger = cached
|
||||
if !cacheCleared {
|
||||
if count == prevCount {
|
||||
// Look up mergerCache
|
||||
if cached, found := m.mergerCache[patternString]; found && cached.final() == request.final {
|
||||
result = cached
|
||||
}
|
||||
} else {
|
||||
// Invalidate mergerCache
|
||||
prevCount = count
|
||||
m.mergerCache = make(map[string]MatchResult)
|
||||
}
|
||||
} else {
|
||||
// Invalidate mergerCache
|
||||
prevCount = count
|
||||
m.mergerCache = make(map[string]*Merger)
|
||||
}
|
||||
|
||||
if !foundCache {
|
||||
merger, cancelled = m.scan(request)
|
||||
if result.merger == nil {
|
||||
m.scanMutex.Lock()
|
||||
result = m.scan(request)
|
||||
m.scanMutex.Unlock()
|
||||
}
|
||||
|
||||
if !cancelled {
|
||||
if merger.cacheable() {
|
||||
m.mergerCache[patternString] = merger
|
||||
if !result.cancelled {
|
||||
if result.cacheable() {
|
||||
m.mergerCache[patternString] = result
|
||||
}
|
||||
merger.final = request.final
|
||||
m.eventBox.Set(EvtSearchFin, merger)
|
||||
result.merger.final = request.final
|
||||
m.eventBox.Set(EvtSearchFin, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk {
|
||||
partitions := m.partitions
|
||||
perSlice := len(chunks) / partitions
|
||||
|
||||
if perSlice == 0 {
|
||||
partitions = len(chunks)
|
||||
perSlice = 1
|
||||
}
|
||||
|
||||
slices := make([][]*Chunk, partitions)
|
||||
for i := 0; i < partitions; i++ {
|
||||
start := i * perSlice
|
||||
end := start + perSlice
|
||||
if i == partitions-1 {
|
||||
end = len(chunks)
|
||||
}
|
||||
slices[i] = chunks[start:end]
|
||||
}
|
||||
return slices
|
||||
}
|
||||
|
||||
type partialResult struct {
|
||||
index int
|
||||
matches []Result
|
||||
}
|
||||
|
||||
func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
func (m *Matcher) scan(request MatchRequest) MatchResult {
|
||||
startedAt := time.Now()
|
||||
|
||||
numChunks := len(request.chunks)
|
||||
if numChunks == 0 {
|
||||
return EmptyMerger, false
|
||||
m := EmptyMerger(request.revision)
|
||||
return MatchResult{m, m, false}
|
||||
}
|
||||
pattern := request.pattern
|
||||
passMerger := PassMerger(&request.chunks, m.tac, request.revision, pattern.startIndex)
|
||||
if pattern.IsEmpty() {
|
||||
return PassMerger(&request.chunks, m.tac), false
|
||||
return MatchResult{passMerger, passMerger, false}
|
||||
}
|
||||
|
||||
minIndex := request.chunks[0].items[0].Index()
|
||||
maxIndex := request.chunks[numChunks-1].lastIndex(minIndex)
|
||||
cancelled := util.NewAtomicBool(false)
|
||||
|
||||
slices := m.sliceChunks(request.chunks)
|
||||
numSlices := len(slices)
|
||||
resultChan := make(chan partialResult, numSlices)
|
||||
numWorkers := min(m.partitions, numChunks)
|
||||
var nextChunk atomic.Int32
|
||||
resultChan := make(chan partialResult, numWorkers)
|
||||
countChan := make(chan int, numChunks)
|
||||
waitGroup := sync.WaitGroup{}
|
||||
|
||||
for idx, chunks := range slices {
|
||||
for idx := range numWorkers {
|
||||
waitGroup.Add(1)
|
||||
if m.slab[idx] == nil {
|
||||
m.slab[idx] = util.MakeSlab(slab16Size, slab32Size)
|
||||
}
|
||||
go func(idx int, slab *util.Slab, chunks []*Chunk) {
|
||||
defer func() { waitGroup.Done() }()
|
||||
count := 0
|
||||
allMatches := make([][]Result, len(chunks))
|
||||
for idx, chunk := range chunks {
|
||||
matches := request.pattern.Match(chunk, slab)
|
||||
allMatches[idx] = matches
|
||||
count += len(matches)
|
||||
go func(idx int, slab *util.Slab) {
|
||||
defer waitGroup.Done()
|
||||
var matches []Result
|
||||
for {
|
||||
ci := int(nextChunk.Add(1)) - 1
|
||||
if ci >= numChunks {
|
||||
break
|
||||
}
|
||||
chunkMatches := request.pattern.Match(request.chunks[ci], slab)
|
||||
matches = append(matches, chunkMatches...)
|
||||
if cancelled.Get() {
|
||||
return
|
||||
}
|
||||
countChan <- len(matches)
|
||||
countChan <- len(chunkMatches)
|
||||
}
|
||||
sliceMatches := make([]Result, 0, count)
|
||||
for _, matches := range allMatches {
|
||||
sliceMatches = append(sliceMatches, matches...)
|
||||
if m.sort && request.pattern.sortable {
|
||||
m.sortBuf[idx] = radixSortResults(matches, m.tac, m.sortBuf[idx])
|
||||
}
|
||||
if m.sort {
|
||||
if m.tac {
|
||||
sort.Sort(ByRelevanceTac(sliceMatches))
|
||||
} else {
|
||||
sort.Sort(ByRelevance(sliceMatches))
|
||||
}
|
||||
}
|
||||
resultChan <- partialResult{idx, sliceMatches}
|
||||
}(idx, m.slab[idx], chunks)
|
||||
resultChan <- partialResult{idx, matches}
|
||||
}(idx, m.slab[idx])
|
||||
}
|
||||
|
||||
wait := func() bool {
|
||||
@@ -204,8 +221,8 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
break
|
||||
}
|
||||
|
||||
if m.reqBox.Peek(reqReset) {
|
||||
return nil, wait()
|
||||
if m.cancelScan.Get() || m.reqBox.Peek(reqReset) {
|
||||
return MatchResult{nil, nil, wait()}
|
||||
}
|
||||
|
||||
if time.Since(startedAt) > progressMinDuration {
|
||||
@@ -213,16 +230,17 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
partialResults := make([][]Result, numSlices)
|
||||
for range slices {
|
||||
partialResults := make([][]Result, numWorkers)
|
||||
for range numWorkers {
|
||||
partialResult := <-resultChan
|
||||
partialResults[partialResult.index] = partialResult.matches
|
||||
}
|
||||
return NewMerger(pattern, partialResults, m.sort, m.tac), false
|
||||
merger := NewMerger(pattern, partialResults, m.sort && request.pattern.sortable, m.tac, request.revision, minIndex, maxIndex)
|
||||
return MatchResult{merger, passMerger, false}
|
||||
}
|
||||
|
||||
// Reset is called to interrupt/signal the ongoing search
|
||||
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, clearCache bool) {
|
||||
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, revision revision) {
|
||||
pattern := m.patternBuilder(patternRunes)
|
||||
|
||||
var event util.EventType
|
||||
@@ -231,5 +249,23 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
|
||||
} else {
|
||||
event = reqRetry
|
||||
}
|
||||
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, clearCache})
|
||||
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort, revision})
|
||||
}
|
||||
|
||||
// CancelScan cancels any in-flight scan, waits for it to finish,
|
||||
// and prevents new scans from starting until ResumeScan is called.
|
||||
// This is used to safely mutate shared items (e.g., during with-nth changes).
|
||||
func (m *Matcher) CancelScan() {
|
||||
m.cancelScan.Set(true)
|
||||
m.scanMutex.Lock()
|
||||
m.cancelScan.Set(false)
|
||||
}
|
||||
|
||||
// ResumeScan allows scans to proceed again after CancelScan.
|
||||
func (m *Matcher) ResumeScan() {
|
||||
m.scanMutex.Unlock()
|
||||
}
|
||||
|
||||
func (m *Matcher) Stop() {
|
||||
m.reqBox.Set(reqQuit, nil)
|
||||
}
|
||||
|
||||
+97
-35
@@ -3,49 +3,71 @@ package fzf
|
||||
import "fmt"
|
||||
|
||||
// EmptyMerger is a Merger with no data
|
||||
var EmptyMerger = NewMerger(nil, [][]Result{}, false, false)
|
||||
func EmptyMerger(revision revision) *Merger {
|
||||
return NewMerger(nil, [][]Result{}, false, false, revision, 0, 0)
|
||||
}
|
||||
|
||||
// Merger holds a set of locally sorted lists of items and provides the view of
|
||||
// a single, globally-sorted list
|
||||
type Merger struct {
|
||||
pattern *Pattern
|
||||
lists [][]Result
|
||||
merged []Result
|
||||
chunks *[]*Chunk
|
||||
cursors []int
|
||||
sorted bool
|
||||
tac bool
|
||||
final bool
|
||||
count int
|
||||
pattern *Pattern
|
||||
lists [][]Result
|
||||
merged []Result
|
||||
chunks *[]*Chunk
|
||||
cursors []int
|
||||
sorted bool
|
||||
tac bool
|
||||
final bool
|
||||
count int
|
||||
pass bool
|
||||
startIndex int
|
||||
revision revision
|
||||
minIndex int32
|
||||
maxIndex int32
|
||||
}
|
||||
|
||||
// PassMerger returns a new Merger that simply returns the items in the
|
||||
// original order
|
||||
func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
|
||||
// original order. startIndex items are skipped from the beginning.
|
||||
func PassMerger(chunks *[]*Chunk, tac bool, revision revision, startIndex int32) *Merger {
|
||||
var minIndex, maxIndex int32
|
||||
if len(*chunks) > 0 {
|
||||
minIndex = (*chunks)[0].items[0].Index()
|
||||
maxIndex = (*chunks)[len(*chunks)-1].lastIndex(minIndex)
|
||||
}
|
||||
si := int(startIndex)
|
||||
mg := Merger{
|
||||
pattern: nil,
|
||||
chunks: chunks,
|
||||
tac: tac,
|
||||
count: 0}
|
||||
pattern: nil,
|
||||
chunks: chunks,
|
||||
tac: tac,
|
||||
count: 0,
|
||||
pass: true,
|
||||
startIndex: si,
|
||||
revision: revision,
|
||||
minIndex: minIndex + startIndex,
|
||||
maxIndex: maxIndex}
|
||||
|
||||
for _, chunk := range *mg.chunks {
|
||||
mg.count += chunk.count
|
||||
}
|
||||
mg.count = max(0, mg.count-si)
|
||||
return &mg
|
||||
}
|
||||
|
||||
// NewMerger returns a new Merger
|
||||
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merger {
|
||||
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision revision, minIndex int32, maxIndex int32) *Merger {
|
||||
mg := Merger{
|
||||
pattern: pattern,
|
||||
lists: lists,
|
||||
merged: []Result{},
|
||||
chunks: nil,
|
||||
cursors: make([]int, len(lists)),
|
||||
sorted: sorted,
|
||||
tac: tac,
|
||||
final: false,
|
||||
count: 0}
|
||||
pattern: pattern,
|
||||
lists: lists,
|
||||
merged: []Result{},
|
||||
chunks: nil,
|
||||
cursors: make([]int, len(lists)),
|
||||
sorted: sorted,
|
||||
tac: tac,
|
||||
final: false,
|
||||
count: 0,
|
||||
revision: revision,
|
||||
minIndex: minIndex,
|
||||
maxIndex: maxIndex}
|
||||
|
||||
for _, list := range mg.lists {
|
||||
mg.count += len(list)
|
||||
@@ -53,17 +75,56 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merge
|
||||
return &mg
|
||||
}
|
||||
|
||||
// Revision returns revision number
|
||||
func (mg *Merger) Revision() revision {
|
||||
return mg.revision
|
||||
}
|
||||
|
||||
// Length returns the number of items
|
||||
func (mg *Merger) Length() int {
|
||||
return mg.count
|
||||
}
|
||||
|
||||
func (mg *Merger) First() Result {
|
||||
if mg.tac && !mg.sorted {
|
||||
return mg.Get(mg.count - 1)
|
||||
}
|
||||
return mg.Get(0)
|
||||
}
|
||||
|
||||
// FindIndex returns the index of the item with the given item index
|
||||
func (mg *Merger) FindIndex(itemIndex int32) int {
|
||||
index := -1
|
||||
if mg.pass {
|
||||
index = int(itemIndex - mg.minIndex)
|
||||
if mg.tac {
|
||||
index = mg.count - index - 1
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < mg.count; i++ {
|
||||
if mg.Get(i).item.Index() == itemIndex {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
// Get returns the pointer to the Result object indexed by the given integer
|
||||
func (mg *Merger) Get(idx int) Result {
|
||||
if mg.chunks != nil {
|
||||
if mg.tac {
|
||||
idx = mg.count - idx - 1
|
||||
}
|
||||
idx += mg.startIndex
|
||||
firstChunk := (*mg.chunks)[0]
|
||||
if firstChunk.count < chunkSize && idx >= firstChunk.count {
|
||||
idx -= firstChunk.count
|
||||
|
||||
chunk := (*mg.chunks)[idx/chunkSize+1]
|
||||
return Result{item: &chunk.items[idx%chunkSize]}
|
||||
}
|
||||
chunk := (*mg.chunks)[idx/chunkSize]
|
||||
return Result{item: &chunk.items[idx%chunkSize]}
|
||||
}
|
||||
@@ -75,14 +136,16 @@ func (mg *Merger) Get(idx int) Result {
|
||||
if mg.tac {
|
||||
idx = mg.count - idx - 1
|
||||
}
|
||||
for _, list := range mg.lists {
|
||||
numItems := len(list)
|
||||
if idx < numItems {
|
||||
return list[idx]
|
||||
}
|
||||
idx -= numItems
|
||||
return mg.mergedGet(idx)
|
||||
}
|
||||
|
||||
func (mg *Merger) ToMap() map[int32]Result {
|
||||
ret := make(map[int32]Result, mg.count)
|
||||
for i := 0; i < mg.count; i++ {
|
||||
result := mg.Get(i)
|
||||
ret[result.Index()] = result
|
||||
}
|
||||
panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count))
|
||||
return ret
|
||||
}
|
||||
|
||||
func (mg *Merger) cacheable() bool {
|
||||
@@ -101,12 +164,11 @@ func (mg *Merger) mergedGet(idx int) Result {
|
||||
}
|
||||
if cursor >= 0 {
|
||||
rank := list[cursor]
|
||||
if minIdx < 0 || compareRanks(rank, minRank, mg.tac) {
|
||||
if minIdx < 0 || mg.sorted && compareRanks(rank, minRank, mg.tac) || !mg.sorted && rank.item.Index() < minRank.item.Index() {
|
||||
minRank = rank
|
||||
minIdx = listIdx
|
||||
}
|
||||
}
|
||||
mg.cursors[listIdx] = cursor
|
||||
}
|
||||
|
||||
if minIdx >= 0 {
|
||||
|
||||
+29
-13
@@ -23,21 +23,22 @@ func randResult() Result {
|
||||
}
|
||||
|
||||
func TestEmptyMerger(t *testing.T) {
|
||||
assert(t, EmptyMerger.Length() == 0, "Not empty")
|
||||
assert(t, EmptyMerger.count == 0, "Invalid count")
|
||||
assert(t, len(EmptyMerger.lists) == 0, "Invalid lists")
|
||||
assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list")
|
||||
r := revision{}
|
||||
assert(t, EmptyMerger(r).Length() == 0, "Not empty")
|
||||
assert(t, EmptyMerger(r).count == 0, "Invalid count")
|
||||
assert(t, len(EmptyMerger(r).lists) == 0, "Invalid lists")
|
||||
assert(t, len(EmptyMerger(r).merged) == 0, "Invalid merged list")
|
||||
}
|
||||
|
||||
func buildLists(partiallySorted bool) ([][]Result, []Result) {
|
||||
numLists := 4
|
||||
lists := make([][]Result, numLists)
|
||||
cnt := 0
|
||||
for i := 0; i < numLists; i++ {
|
||||
for i := range numLists {
|
||||
numResults := rand.Int() % 20
|
||||
cnt += numResults
|
||||
lists[i] = make([]Result, numResults)
|
||||
for j := 0; j < numResults; j++ {
|
||||
for j := range numResults {
|
||||
item := randResult()
|
||||
lists[i][j] = item
|
||||
}
|
||||
@@ -53,13 +54,28 @@ func buildLists(partiallySorted bool) ([][]Result, []Result) {
|
||||
}
|
||||
|
||||
func TestMergerUnsorted(t *testing.T) {
|
||||
lists, items := buildLists(false)
|
||||
lists, _ := buildLists(false)
|
||||
|
||||
// Sort each list by index to simulate real worker behavior
|
||||
// (workers process chunks in ascending order via nextChunk.Add(1))
|
||||
for _, list := range lists {
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
return list[i].item.Index() < list[j].item.Index()
|
||||
})
|
||||
}
|
||||
items := []Result{}
|
||||
for _, list := range lists {
|
||||
items = append(items, list...)
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return items[i].item.Index() < items[j].item.Index()
|
||||
})
|
||||
cnt := len(items)
|
||||
|
||||
// Not sorted: same order
|
||||
mg := NewMerger(nil, lists, false, false)
|
||||
// Not sorted: items in ascending index order
|
||||
mg := NewMerger(nil, lists, false, false, revision{}, 0, 0)
|
||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||
for i := 0; i < cnt; i++ {
|
||||
for i := range cnt {
|
||||
assert(t, items[i] == mg.Get(i), "Invalid Get")
|
||||
}
|
||||
}
|
||||
@@ -69,17 +85,17 @@ func TestMergerSorted(t *testing.T) {
|
||||
cnt := len(items)
|
||||
|
||||
// Sorted sorted order
|
||||
mg := NewMerger(nil, lists, true, false)
|
||||
mg := NewMerger(nil, lists, true, false, revision{}, 0, 0)
|
||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||
sort.Sort(ByRelevance(items))
|
||||
for i := 0; i < cnt; i++ {
|
||||
for i := range cnt {
|
||||
if items[i] != mg.Get(i) {
|
||||
t.Error("Not sorted", items[i], mg.Get(i))
|
||||
}
|
||||
}
|
||||
|
||||
// Inverse order
|
||||
mg2 := NewMerger(nil, lists, true, false)
|
||||
mg2 := NewMerger(nil, lists, true, false, revision{}, 0, 0)
|
||||
for i := cnt - 1; i >= 0; i-- {
|
||||
if items[i] != mg2.Get(i) {
|
||||
t.Error("Not sorted", items[i], mg2.Get(i))
|
||||
|
||||
+3359
-757
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
//go:build !pprof
|
||||
// +build !pprof
|
||||
|
||||
package fzf
|
||||
|
||||
import "errors"
|
||||
|
||||
func (o *Options) initProfiling() error {
|
||||
if o.CPUProfile != "" || o.MEMProfile != "" || o.BlockProfile != "" || o.MutexProfile != "" {
|
||||
return errors.New("error: profiling not supported: FZF must be built with '-tags=pprof' to enable profiling")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
//go:build pprof
|
||||
// +build pprof
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func (o *Options) initProfiling() error {
|
||||
if o.CPUProfile != "" {
|
||||
f, err := os.Create(o.CPUProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create CPU profile: %w", err)
|
||||
}
|
||||
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
return fmt.Errorf("could not start CPU profile: %w", err)
|
||||
}
|
||||
|
||||
util.AtExit(func() {
|
||||
pprof.StopCPUProfile()
|
||||
if err := f.Close(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error: closing cpu profile:", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
stopProfile := func(name string, f *os.File) {
|
||||
if err := pprof.Lookup(name).WriteTo(f, 0); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: could not write %s profile: %v\n", name, err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: closing %s profile: %v\n", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
if o.MEMProfile != "" {
|
||||
f, err := os.Create(o.MEMProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create MEM profile: %w", err)
|
||||
}
|
||||
util.AtExit(func() {
|
||||
runtime.GC()
|
||||
stopProfile("allocs", f)
|
||||
})
|
||||
}
|
||||
|
||||
if o.BlockProfile != "" {
|
||||
runtime.SetBlockProfileRate(1)
|
||||
f, err := os.Create(o.BlockProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create BLOCK profile: %w", err)
|
||||
}
|
||||
util.AtExit(func() { stopProfile("block", f) })
|
||||
}
|
||||
|
||||
if o.MutexProfile != "" {
|
||||
runtime.SetMutexProfileFraction(1)
|
||||
f, err := os.Create(o.MutexProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create MUTEX profile: %w", err)
|
||||
}
|
||||
util.AtExit(func() { stopProfile("mutex", f) })
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
//go:build pprof
|
||||
// +build pprof
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
// runInitProfileTests is an internal flag used TestInitProfiling
|
||||
var runInitProfileTests = flag.Bool("test-init-profile", false, "run init profile tests")
|
||||
|
||||
func TestInitProfiling(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("short test")
|
||||
}
|
||||
|
||||
// Run this test in a separate process since it interferes with
|
||||
// profiling and modifies the global atexit state. Without this
|
||||
// running `go test -bench . -cpuprofile cpu.out` will fail.
|
||||
if !*runInitProfileTests {
|
||||
t.Parallel()
|
||||
|
||||
// Make sure we are not the child process.
|
||||
if os.Getenv("_FZF_CHILD_PROC") != "" {
|
||||
t.Fatal("already running as child process!")
|
||||
}
|
||||
|
||||
cmd := exec.Command(os.Args[0],
|
||||
"-test.timeout", "30s",
|
||||
"-test.run", "^"+t.Name()+"$",
|
||||
"-test-init-profile",
|
||||
)
|
||||
cmd.Env = append(os.Environ(), "_FZF_CHILD_PROC=1")
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
out = bytes.TrimSpace(out)
|
||||
if err != nil {
|
||||
t.Fatalf("Child test process failed: %v:\n%s", err, out)
|
||||
}
|
||||
// Make sure the test actually ran
|
||||
if bytes.Contains(out, []byte("no tests to run")) {
|
||||
t.Fatalf("Failed to run test %q:\n%s", t.Name(), out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Child process
|
||||
|
||||
tempdir := t.TempDir()
|
||||
t.Cleanup(util.RunAtExitFuncs)
|
||||
|
||||
o := Options{
|
||||
CPUProfile: filepath.Join(tempdir, "cpu.prof"),
|
||||
MEMProfile: filepath.Join(tempdir, "mem.prof"),
|
||||
BlockProfile: filepath.Join(tempdir, "block.prof"),
|
||||
MutexProfile: filepath.Join(tempdir, "mutex.prof"),
|
||||
}
|
||||
if err := o.initProfiling(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
profiles := []string{
|
||||
o.CPUProfile,
|
||||
o.MEMProfile,
|
||||
o.BlockProfile,
|
||||
o.MutexProfile,
|
||||
}
|
||||
for _, name := range profiles {
|
||||
if _, err := os.Stat(name); err != nil {
|
||||
t.Errorf("Failed to create profile %s: %v", filepath.Base(name), err)
|
||||
}
|
||||
}
|
||||
|
||||
util.RunAtExitFuncs()
|
||||
|
||||
for _, name := range profiles {
|
||||
if _, err := os.Stat(name); err != nil {
|
||||
t.Errorf("Failed to write profile %s: %v", filepath.Base(name), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
+302
-122
@@ -2,16 +2,20 @@ package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
)
|
||||
|
||||
func TestDelimiterRegex(t *testing.T) {
|
||||
// Valid regex
|
||||
// Valid regex, but a single character -> string
|
||||
delim := delimiterRegexp(".")
|
||||
if delim.regex == nil || delim.str != nil {
|
||||
if delim.regex != nil || *delim.str != "." {
|
||||
t.Error(delim)
|
||||
}
|
||||
delim = delimiterRegexp("|")
|
||||
if delim.regex != nil || *delim.str != "|" {
|
||||
t.Error(delim)
|
||||
}
|
||||
// Broken regex -> string
|
||||
@@ -65,9 +69,22 @@ func TestDelimiterRegexRegex(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelimiterRegexRegexCaret(t *testing.T) {
|
||||
delim := delimiterRegexp(`(^\s*|\s+)`)
|
||||
tokens := Tokenize("foo bar baz", delim)
|
||||
if delim.str != nil ||
|
||||
len(tokens) != 4 ||
|
||||
tokens[0].text.ToString() != "" ||
|
||||
tokens[1].text.ToString() != "foo " ||
|
||||
tokens[2].text.ToString() != "bar " ||
|
||||
tokens[3].text.ToString() != "baz" {
|
||||
t.Errorf("%s %d", tokens, len(tokens))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitNth(t *testing.T) {
|
||||
{
|
||||
ranges := splitNth("..")
|
||||
ranges, _ := splitNth("..")
|
||||
if len(ranges) != 1 ||
|
||||
ranges[0].begin != rangeEllipsis ||
|
||||
ranges[0].end != rangeEllipsis {
|
||||
@@ -75,7 +92,7 @@ func TestSplitNth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
{
|
||||
ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1")
|
||||
ranges, _ := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1")
|
||||
if len(ranges) != 10 ||
|
||||
ranges[0].begin != rangeEllipsis || ranges[0].end != 3 ||
|
||||
ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis ||
|
||||
@@ -93,19 +110,20 @@ func TestSplitNth(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIrrelevantNth(t *testing.T) {
|
||||
index := 0
|
||||
{
|
||||
opts := defaultOptions()
|
||||
words := []string{"--nth", "..", "-x"}
|
||||
parseOptions(opts, words)
|
||||
parseOptions(&index, opts, words)
|
||||
postProcessOptions(opts)
|
||||
if len(opts.Nth) != 0 {
|
||||
t.Errorf("nth should be empty: %v", opts.Nth)
|
||||
}
|
||||
}
|
||||
for _, words := range [][]string{[]string{"--nth", "..,3", "+x"}, []string{"--nth", "3,1..", "+x"}, []string{"--nth", "..-1,1", "+x"}} {
|
||||
for _, words := range [][]string{{"--nth", "..,3", "+x"}, {"--nth", "3,1..", "+x"}, {"--nth", "..-1,1", "+x"}} {
|
||||
{
|
||||
opts := defaultOptions()
|
||||
parseOptions(opts, words)
|
||||
parseOptions(&index, opts, words)
|
||||
postProcessOptions(opts)
|
||||
if len(opts.Nth) != 0 {
|
||||
t.Errorf("nth should be empty: %v", opts.Nth)
|
||||
@@ -114,7 +132,7 @@ func TestIrrelevantNth(t *testing.T) {
|
||||
{
|
||||
opts := defaultOptions()
|
||||
words = append(words, "-x")
|
||||
parseOptions(opts, words)
|
||||
parseOptions(&index, opts, words)
|
||||
postProcessOptions(opts)
|
||||
if len(opts.Nth) != 2 {
|
||||
t.Errorf("nth should not be empty: %v", opts.Nth)
|
||||
@@ -124,58 +142,61 @@ func TestIrrelevantNth(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseKeys(t *testing.T) {
|
||||
pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "")
|
||||
check := func(i int, s string) {
|
||||
if pairs[i] != s {
|
||||
t.Errorf("%s != %s", pairs[i], s)
|
||||
pairs, _, _ := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "")
|
||||
checkEvent := func(e tui.Event, s string) {
|
||||
if pairs[e] != s {
|
||||
t.Errorf("%s != %s", pairs[e], s)
|
||||
}
|
||||
}
|
||||
check := func(et tui.EventType, s string) {
|
||||
checkEvent(et.AsEvent(), s)
|
||||
}
|
||||
if len(pairs) != 12 {
|
||||
t.Error(12)
|
||||
}
|
||||
check(tui.CtrlZ, "ctrl-z")
|
||||
check(tui.AltZ, "alt-z")
|
||||
check(tui.F2, "f2")
|
||||
check(tui.AltZ+'@', "@")
|
||||
check(tui.AltA, "Alt-a")
|
||||
check(tui.AltZ+'!', "!")
|
||||
check(tui.CtrlA+'g'-'a', "ctrl-G")
|
||||
check(tui.AltZ+'J', "J")
|
||||
check(tui.AltZ+'g', "g")
|
||||
check(tui.CtrlAltA, "ctrl-alt-a")
|
||||
check(tui.CtrlAltM, "ALT-enter")
|
||||
check(tui.AltSpace, "alt-SPACE")
|
||||
check(tui.CtrlG, "ctrl-G")
|
||||
checkEvent(tui.AltKey('z'), "alt-z")
|
||||
checkEvent(tui.Key('@'), "@")
|
||||
checkEvent(tui.AltKey('a'), "Alt-a")
|
||||
checkEvent(tui.Key('!'), "!")
|
||||
checkEvent(tui.Key('J'), "J")
|
||||
checkEvent(tui.Key('g'), "g")
|
||||
checkEvent(tui.CtrlAltKey('a'), "ctrl-alt-a")
|
||||
checkEvent(tui.CtrlAltKey('m'), "ALT-enter")
|
||||
checkEvent(tui.AltKey(' '), "alt-SPACE")
|
||||
|
||||
// Synonyms
|
||||
pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
|
||||
pairs, _, _ = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
|
||||
if len(pairs) != 9 {
|
||||
t.Error(9)
|
||||
}
|
||||
check(tui.CtrlM, "Return")
|
||||
check(tui.AltZ+' ', "space")
|
||||
check(tui.Enter, "Return")
|
||||
checkEvent(tui.Key(' '), "space")
|
||||
check(tui.Tab, "tab")
|
||||
check(tui.BTab, "btab")
|
||||
check(tui.ESC, "esc")
|
||||
check(tui.ShiftTab, "btab")
|
||||
check(tui.Esc, "esc")
|
||||
check(tui.Up, "up")
|
||||
check(tui.Down, "down")
|
||||
check(tui.Left, "left")
|
||||
check(tui.Right, "right")
|
||||
|
||||
pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
|
||||
pairs, _, _ = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
|
||||
if len(pairs) != 11 {
|
||||
t.Error(11)
|
||||
}
|
||||
check(tui.Tab, "Ctrl-I")
|
||||
check(tui.PgUp, "page-up")
|
||||
check(tui.PgDn, "Page-Down")
|
||||
check(tui.PageUp, "page-up")
|
||||
check(tui.PageDown, "Page-Down")
|
||||
check(tui.Home, "Home")
|
||||
check(tui.End, "End")
|
||||
check(tui.AltBS, "Alt-BSpace")
|
||||
check(tui.SLeft, "shift-left")
|
||||
check(tui.SRight, "shift-right")
|
||||
check(tui.BTab, "shift-tab")
|
||||
check(tui.CtrlM, "Enter")
|
||||
check(tui.BSpace, "bspace")
|
||||
check(tui.AltBackspace, "Alt-BSpace")
|
||||
check(tui.ShiftLeft, "shift-left")
|
||||
check(tui.ShiftRight, "shift-right")
|
||||
check(tui.ShiftTab, "shift-tab")
|
||||
check(tui.Enter, "Enter")
|
||||
check(tui.Backspace, "bspace")
|
||||
}
|
||||
|
||||
func TestParseKeysWithComma(t *testing.T) {
|
||||
@@ -184,98 +205,140 @@ func TestParseKeysWithComma(t *testing.T) {
|
||||
t.Errorf("%d != %d", a, b)
|
||||
}
|
||||
}
|
||||
check := func(pairs map[int]string, i int, s string) {
|
||||
if pairs[i] != s {
|
||||
t.Errorf("%s != %s", pairs[i], s)
|
||||
check := func(pairs map[tui.Event]string, e tui.Event, s string) {
|
||||
if pairs[e] != s {
|
||||
t.Errorf("%s != %s", pairs[e], s)
|
||||
}
|
||||
}
|
||||
|
||||
pairs := parseKeyChords(",", "")
|
||||
pairs, _, _ := parseKeyChords(",", "")
|
||||
checkN(len(pairs), 1)
|
||||
check(pairs, tui.AltZ+',', ",")
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs = parseKeyChords(",,a,b", "")
|
||||
pairs, _, _ = parseKeyChords(",,a,b", "")
|
||||
checkN(len(pairs), 3)
|
||||
check(pairs, tui.AltZ+'a', "a")
|
||||
check(pairs, tui.AltZ+'b', "b")
|
||||
check(pairs, tui.AltZ+',', ",")
|
||||
check(pairs, tui.Key('a'), "a")
|
||||
check(pairs, tui.Key('b'), "b")
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs = parseKeyChords("a,b,,", "")
|
||||
pairs, _, _ = parseKeyChords("a,b,,", "")
|
||||
checkN(len(pairs), 3)
|
||||
check(pairs, tui.AltZ+'a', "a")
|
||||
check(pairs, tui.AltZ+'b', "b")
|
||||
check(pairs, tui.AltZ+',', ",")
|
||||
check(pairs, tui.Key('a'), "a")
|
||||
check(pairs, tui.Key('b'), "b")
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs = parseKeyChords("a,,,b", "")
|
||||
pairs, _, _ = parseKeyChords("a,,,b", "")
|
||||
checkN(len(pairs), 3)
|
||||
check(pairs, tui.AltZ+'a', "a")
|
||||
check(pairs, tui.AltZ+'b', "b")
|
||||
check(pairs, tui.AltZ+',', ",")
|
||||
check(pairs, tui.Key('a'), "a")
|
||||
check(pairs, tui.Key('b'), "b")
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs = parseKeyChords("a,,,b,c", "")
|
||||
pairs, _, _ = parseKeyChords("a,,,b,c", "")
|
||||
checkN(len(pairs), 4)
|
||||
check(pairs, tui.AltZ+'a', "a")
|
||||
check(pairs, tui.AltZ+'b', "b")
|
||||
check(pairs, tui.AltZ+'c', "c")
|
||||
check(pairs, tui.AltZ+',', ",")
|
||||
check(pairs, tui.Key('a'), "a")
|
||||
check(pairs, tui.Key('b'), "b")
|
||||
check(pairs, tui.Key('c'), "c")
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs = parseKeyChords(",,,", "")
|
||||
pairs, _, _ = parseKeyChords(",,,", "")
|
||||
checkN(len(pairs), 1)
|
||||
check(pairs, tui.AltZ+',', ",")
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs, _, _ = parseKeyChords(",ALT-,,", "")
|
||||
checkN(len(pairs), 1)
|
||||
check(pairs, tui.AltKey(','), "ALT-,")
|
||||
}
|
||||
|
||||
func TestBind(t *testing.T) {
|
||||
keymap := defaultKeymap()
|
||||
check := func(keyName int, arg1 string, types ...actionType) {
|
||||
if len(keymap[keyName]) != len(types) {
|
||||
t.Errorf("invalid number of actions (%d != %d)", len(types), len(keymap[keyName]))
|
||||
check := func(event tui.Event, arg1 string, types ...actionType) {
|
||||
if len(keymap[event]) != len(types) {
|
||||
t.Errorf("invalid number of actions for %v (%d != %d)",
|
||||
event, len(types), len(keymap[event]))
|
||||
return
|
||||
}
|
||||
for idx, action := range keymap[keyName] {
|
||||
for idx, action := range keymap[event] {
|
||||
if types[idx] != action.t {
|
||||
t.Errorf("invalid action type (%d != %d)", types[idx], action.t)
|
||||
}
|
||||
}
|
||||
if len(arg1) > 0 && keymap[keyName][0].a != arg1 {
|
||||
t.Errorf("invalid action argument: (%s != %s)", arg1, keymap[keyName][0].a)
|
||||
if len(arg1) > 0 && keymap[event][0].a != arg1 {
|
||||
t.Errorf("invalid action argument: (%s != %s)", arg1, keymap[event][0].a)
|
||||
}
|
||||
}
|
||||
check(tui.CtrlA, "", actBeginningOfLine)
|
||||
check(tui.CtrlA.AsEvent(), "", actBeginningOfLine)
|
||||
parseKeymap(keymap,
|
||||
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
|
||||
"f1:execute(ls {+})+abort+execute(echo {+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
|
||||
"f1:execute(ls {+})+abort+execute(echo \n{+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
|
||||
"alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
|
||||
"x:Execute(foo+bar),X:execute/bar+baz/"+
|
||||
",f1:+top,f1:+top"+
|
||||
",f1:+first,f1:+top"+
|
||||
",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up")
|
||||
check(tui.CtrlA, "", actKillLine)
|
||||
check(tui.CtrlB, "", actToggleSort, actUp, actDown)
|
||||
check(tui.AltZ+'c', "", actPageUp)
|
||||
check(tui.AltZ+',', "", actAbort)
|
||||
check(tui.AltZ+':', "", actAccept)
|
||||
check(tui.AltZ, "", actPageDown)
|
||||
check(tui.F1, "ls {+}", actExecute, actAbort, actExecute, actSelectAll, actTop, actTop)
|
||||
check(tui.F2, "echo {}, {}, {}", actExecute)
|
||||
check(tui.F3, "echo '({})'", actExecute)
|
||||
check(tui.F4, "less {}", actExecute)
|
||||
check(tui.AltZ+'x', "foo+bar", actExecute)
|
||||
check(tui.AltZ+'X', "bar+baz", actExecute)
|
||||
check(tui.AltA, "echo (,),[,],/,:,;,%,{}", actExecuteMulti)
|
||||
check(tui.AltB, "echo (,),[,],/,:,@,%,{}", actExecute)
|
||||
check(tui.AltZ+'+', "++\nfoobar,Y:execute(baz)+up", actExecute)
|
||||
check(tui.CtrlA.AsEvent(), "", actKillLine)
|
||||
check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown)
|
||||
check(tui.Key('c'), "", actPageUp)
|
||||
check(tui.Key(','), "", actAbort)
|
||||
check(tui.Key(':'), "", actAccept)
|
||||
check(tui.AltKey('z'), "", actPageDown)
|
||||
check(tui.F1.AsEvent(), "ls {+}", actExecute, actAbort, actExecute, actSelectAll, actFirst, actFirst)
|
||||
check(tui.F2.AsEvent(), "echo {}, {}, {}", actExecute)
|
||||
check(tui.F3.AsEvent(), "echo '({})'", actExecute)
|
||||
check(tui.F4.AsEvent(), "less {}", actExecute)
|
||||
check(tui.Key('x'), "foo+bar", actExecute)
|
||||
check(tui.Key('X'), "bar+baz", actExecute)
|
||||
check(tui.AltKey('a'), "echo (,),[,],/,:,;,%,{}", actExecuteMulti)
|
||||
check(tui.AltKey('b'), "echo (,),[,],/,:,@,%,{}", actExecute)
|
||||
check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute)
|
||||
|
||||
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
|
||||
parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
|
||||
check(tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute)
|
||||
check(tui.Key([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute)
|
||||
}
|
||||
|
||||
parseKeymap(keymap, "f1:abort")
|
||||
check(tui.F1, "", actAbort)
|
||||
check(tui.F1.AsEvent(), "", actAbort)
|
||||
}
|
||||
|
||||
func TestParseEveryEvent(t *testing.T) {
|
||||
pairs, _, err := parseKeyChords("every(2),every(0.5)", "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(pairs) != 2 {
|
||||
t.Errorf("expected 2 distinct every events, got %d", len(pairs))
|
||||
}
|
||||
if pairs[(tui.Event{Type: tui.Every, Char: 2000})] != "every(2)" {
|
||||
t.Errorf("every(2) not registered")
|
||||
}
|
||||
if pairs[(tui.Event{Type: tui.Every, Char: 500})] != "every(0.5)" {
|
||||
t.Errorf("every(0.5) not registered")
|
||||
}
|
||||
|
||||
// Floor at 0.01s -> 10ms
|
||||
pairs, _, err = parseKeyChords("every(0.001)", "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if pairs[(tui.Event{Type: tui.Every, Char: 10})] != "every(0.001)" {
|
||||
t.Errorf("every(0.001) should floor to 10ms")
|
||||
}
|
||||
|
||||
// Reject zero, negatives, and overflow (>= 2^31 ms = ~24.85 days)
|
||||
for _, bad := range []string{"every(0)", "every(-1)", "every(abc)", "every()", "every(2147484)"} {
|
||||
if _, _, err := parseKeyChords(bad, ""); err == nil {
|
||||
t.Errorf("%s should be rejected", bad)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestColorSpec(t *testing.T) {
|
||||
var base *tui.ColorTheme
|
||||
theme := tui.Dark256
|
||||
dark := parseTheme(theme, "dark")
|
||||
base, dark, _ := parseTheme(theme, "dark")
|
||||
if *dark != *base {
|
||||
t.Errorf("incorrect base theme returned")
|
||||
}
|
||||
if *dark != *theme {
|
||||
t.Errorf("colors should be equivalent")
|
||||
}
|
||||
@@ -283,7 +346,10 @@ func TestColorSpec(t *testing.T) {
|
||||
t.Errorf("point should not be equivalent")
|
||||
}
|
||||
|
||||
light := parseTheme(theme, "dark,light")
|
||||
base, light, _ := parseTheme(theme, "dark,light")
|
||||
if *light != *base {
|
||||
t.Errorf("incorrect base theme returned")
|
||||
}
|
||||
if *light == *theme {
|
||||
t.Errorf("should not be equivalent")
|
||||
}
|
||||
@@ -294,8 +360,8 @@ func TestColorSpec(t *testing.T) {
|
||||
t.Errorf("point should not be equivalent")
|
||||
}
|
||||
|
||||
customized := parseTheme(theme, "fg:231,bg:232")
|
||||
if customized.Fg != 231 || customized.Bg != 232 {
|
||||
_, customized, _ := parseTheme(theme, "fg:231,bg:232")
|
||||
if customized.Fg.Color != 231 || customized.Bg.Color != 232 {
|
||||
t.Errorf("color not customized")
|
||||
}
|
||||
if *tui.Dark256 == *customized {
|
||||
@@ -307,55 +373,46 @@ func TestColorSpec(t *testing.T) {
|
||||
t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized)
|
||||
}
|
||||
|
||||
customized = parseTheme(theme, "fg:231,dark,bg:232")
|
||||
_, customized, _ = parseTheme(theme, "fg:231,dark bg:232")
|
||||
if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
|
||||
t.Errorf("color not customized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNilTheme(t *testing.T) {
|
||||
var theme *tui.ColorTheme
|
||||
newTheme := parseTheme(theme, "prompt:12")
|
||||
if newTheme != nil {
|
||||
t.Errorf("color is disabled. keep it that way.")
|
||||
}
|
||||
newTheme = parseTheme(theme, "prompt:12,dark,prompt:13")
|
||||
if newTheme.Prompt != 13 {
|
||||
t.Errorf("color should now be enabled and customized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultCtrlNP(t *testing.T) {
|
||||
check := func(words []string, key int, expected actionType) {
|
||||
index := 0
|
||||
check := func(words []string, et tui.EventType, expected actionType) {
|
||||
e := et.AsEvent()
|
||||
opts := defaultOptions()
|
||||
parseOptions(opts, words)
|
||||
parseOptions(&index, opts, words)
|
||||
postProcessOptions(opts)
|
||||
if opts.Keymap[key][0].t != expected {
|
||||
if opts.Keymap[e][0].t != expected {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
check([]string{}, tui.CtrlN, actDown)
|
||||
check([]string{}, tui.CtrlP, actUp)
|
||||
check([]string{}, tui.CtrlN, actDownMatch)
|
||||
check([]string{}, tui.CtrlP, actUpMatch)
|
||||
|
||||
check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
|
||||
check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
|
||||
|
||||
f, _ := ioutil.TempFile("", "fzf-history")
|
||||
f, _ := os.CreateTemp("", "fzf-history")
|
||||
f.Close()
|
||||
hist := "--history=" + f.Name()
|
||||
check([]string{hist}, tui.CtrlN, actNextHistory)
|
||||
check([]string{hist}, tui.CtrlP, actPreviousHistory)
|
||||
check([]string{hist}, tui.CtrlP, actPrevHistory)
|
||||
|
||||
check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
|
||||
check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlP, actPreviousHistory)
|
||||
check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlP, actPrevHistory)
|
||||
|
||||
check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlN, actNextHistory)
|
||||
check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
|
||||
}
|
||||
|
||||
func optsFor(words ...string) *Options {
|
||||
index := 0
|
||||
opts := defaultOptions()
|
||||
parseOptions(opts, words)
|
||||
parseOptions(&index, opts, words)
|
||||
postProcessOptions(opts)
|
||||
return opts
|
||||
}
|
||||
@@ -387,23 +444,26 @@ func TestPreviewOpts(t *testing.T) {
|
||||
opts.Preview.size.size == 50) {
|
||||
t.Error()
|
||||
}
|
||||
opts = optsFor("--preview", "cat {}", "--preview-window=left:15:hidden:wrap")
|
||||
opts = optsFor("--preview", "cat {}", "--preview-window=left:15,hidden,wrap:+{1}-/2")
|
||||
if !(opts.Preview.command == "cat {}" &&
|
||||
opts.Preview.hidden == true &&
|
||||
opts.Preview.wrap == true &&
|
||||
opts.Preview.position == posLeft &&
|
||||
opts.Preview.scroll == "+{1}-/2" &&
|
||||
opts.Preview.size.percent == false &&
|
||||
opts.Preview.size.size == 15+2+2) {
|
||||
opts.Preview.size.size == 15) {
|
||||
t.Error(opts.Preview)
|
||||
}
|
||||
opts = optsFor("--preview-window=up:15:wrap:hidden", "--preview-window=down")
|
||||
opts = optsFor("--preview-window=up,15,wrap,hidden,+{1}+3-1-2/2", "--preview-window=down", "--preview-window=cycle")
|
||||
if !(opts.Preview.command == "" &&
|
||||
opts.Preview.hidden == false &&
|
||||
opts.Preview.wrap == false &&
|
||||
opts.Preview.hidden == true &&
|
||||
opts.Preview.wrap == true &&
|
||||
opts.Preview.cycle == true &&
|
||||
opts.Preview.position == posDown &&
|
||||
opts.Preview.size.percent == true &&
|
||||
opts.Preview.size.size == 50) {
|
||||
t.Error(opts.Preview)
|
||||
opts.Preview.scroll == "+{1}+3-1-2/2" &&
|
||||
opts.Preview.size.percent == false &&
|
||||
opts.Preview.size.size == 15) {
|
||||
t.Error(opts.Preview.size.size)
|
||||
}
|
||||
opts = optsFor("--preview-window=up:15:wrap:hidden")
|
||||
if !(opts.Preview.command == "" &&
|
||||
@@ -411,9 +471,74 @@ func TestPreviewOpts(t *testing.T) {
|
||||
opts.Preview.wrap == true &&
|
||||
opts.Preview.position == posUp &&
|
||||
opts.Preview.size.percent == false &&
|
||||
opts.Preview.size.size == 15+2) {
|
||||
opts.Preview.size.size == 15) {
|
||||
t.Error(opts.Preview)
|
||||
}
|
||||
opts = optsFor("--preview=foo", "--preview-window=up", "--preview-window=default:70%")
|
||||
if !(opts.Preview.command == "foo" &&
|
||||
opts.Preview.position == posRight &&
|
||||
opts.Preview.size.percent == true &&
|
||||
opts.Preview.size.size == 70) {
|
||||
t.Error(opts.Preview)
|
||||
}
|
||||
|
||||
// wrap-word tests
|
||||
opts = optsFor("--preview-window=wrap-word")
|
||||
if !(opts.Preview.wrap == true && opts.Preview.wrapWord == true) {
|
||||
t.Errorf("wrap-word: wrap=%v, wrapWord=%v", opts.Preview.wrap, opts.Preview.wrapWord)
|
||||
}
|
||||
opts = optsFor("--preview-window=wrap-word,nowrap")
|
||||
if !(opts.Preview.wrap == false && opts.Preview.wrapWord == false) {
|
||||
t.Errorf("wrap-word,nowrap: wrap=%v, wrapWord=%v", opts.Preview.wrap, opts.Preview.wrapWord)
|
||||
}
|
||||
opts = optsFor("--preview-window=wrap-word,wrap")
|
||||
if !(opts.Preview.wrap == true && opts.Preview.wrapWord == false) {
|
||||
t.Errorf("wrap-word,wrap: wrap=%v, wrapWord=%v", opts.Preview.wrap, opts.Preview.wrapWord)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreviewWrapSign(t *testing.T) {
|
||||
// Default: no preview wrap sign override
|
||||
opts := optsFor()
|
||||
if opts.PreviewWrapSign != nil {
|
||||
t.Errorf("expected nil PreviewWrapSign, got %v", *opts.PreviewWrapSign)
|
||||
}
|
||||
|
||||
// --preview-wrap-sign sets PreviewWrapSign
|
||||
opts = optsFor("--preview-wrap-sign", ">> ")
|
||||
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != ">> " {
|
||||
t.Errorf("expected '>> ', got %v", opts.PreviewWrapSign)
|
||||
}
|
||||
|
||||
// --preview-wrap-sign is independent of --wrap-sign
|
||||
opts = optsFor("--wrap-sign", "| ", "--preview-wrap-sign", ">> ")
|
||||
if opts.WrapSign == nil || *opts.WrapSign != "| " {
|
||||
t.Errorf("expected WrapSign '| ', got %v", opts.WrapSign)
|
||||
}
|
||||
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != ">> " {
|
||||
t.Errorf("expected PreviewWrapSign '>> ', got %v", opts.PreviewWrapSign)
|
||||
}
|
||||
|
||||
// --preview-wrap-sign without --wrap-sign
|
||||
opts = optsFor("--preview-wrap-sign", "→ ")
|
||||
if opts.WrapSign != nil {
|
||||
t.Errorf("expected nil WrapSign, got %v", *opts.WrapSign)
|
||||
}
|
||||
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != "→ " {
|
||||
t.Errorf("expected PreviewWrapSign '→ ', got %v", opts.PreviewWrapSign)
|
||||
}
|
||||
|
||||
// Last --preview-wrap-sign wins
|
||||
opts = optsFor("--preview-wrap-sign", "A ", "--preview-wrap-sign", "B ")
|
||||
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != "B " {
|
||||
t.Errorf("expected PreviewWrapSign 'B ', got %v", opts.PreviewWrapSign)
|
||||
}
|
||||
|
||||
// Empty string is allowed
|
||||
opts = optsFor("--preview-wrap-sign", "")
|
||||
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != "" {
|
||||
t.Errorf("expected empty PreviewWrapSign, got %v", opts.PreviewWrapSign)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdditiveExpect(t *testing.T) {
|
||||
@@ -422,3 +547,58 @@ func TestAdditiveExpect(t *testing.T) {
|
||||
t.Error(opts.Expect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSign(t *testing.T) {
|
||||
testCases := []struct {
|
||||
inputSign string
|
||||
isValid bool
|
||||
}{
|
||||
{"> ", true},
|
||||
{"아", true},
|
||||
{"😀", true},
|
||||
{">>>", false},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
err := validateSign(testCase.inputSign, "", 2)
|
||||
if testCase.isValid && err != nil {
|
||||
t.Errorf("Input sign `%s` caused error", testCase.inputSign)
|
||||
}
|
||||
|
||||
if !testCase.isValid && err == nil {
|
||||
t.Errorf("Input sign `%s` did not cause error", testCase.inputSign)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSingleActionList(t *testing.T) {
|
||||
actions, _ := parseSingleActionList("Execute@foo+bar,baz@+up+up+reload:down+down")
|
||||
if len(actions) != 4 {
|
||||
t.Errorf("Invalid number of actions parsed:%d", len(actions))
|
||||
}
|
||||
if actions[0].t != actExecute || actions[0].a != "foo+bar,baz" {
|
||||
t.Errorf("Invalid action parsed: %v", actions[0])
|
||||
}
|
||||
if actions[1].t != actUp || actions[2].t != actUp {
|
||||
t.Errorf("Invalid action parsed: %v / %v", actions[1], actions[2])
|
||||
}
|
||||
if actions[3].t != actReload || actions[3].a != "down+down" {
|
||||
t.Errorf("Invalid action parsed: %v", actions[3])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSingleActionListError(t *testing.T) {
|
||||
_, err := parseSingleActionList("change-query(foobar)baz")
|
||||
if err == nil {
|
||||
t.Errorf("Failed to detect error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaskActionContents(t *testing.T) {
|
||||
original := ":execute((f)(o)(o)(b)(a)(r))+change-query@qu@ry@+up,x:reload:hello:world"
|
||||
expected := ":execute +change-query +up,x:reload "
|
||||
masked := maskActionContents(original)
|
||||
if masked != expected {
|
||||
t.Errorf("Not masked: %s", masked)
|
||||
}
|
||||
}
|
||||
|
||||
+147
-65
@@ -23,6 +23,7 @@ type termType int
|
||||
const (
|
||||
termFuzzy termType = iota
|
||||
termExact
|
||||
termExactBoundary
|
||||
termPrefix
|
||||
termSuffix
|
||||
termEqual
|
||||
@@ -33,6 +34,7 @@ type term struct {
|
||||
inv bool
|
||||
text []rune
|
||||
caseSensitive bool
|
||||
normalize bool
|
||||
}
|
||||
|
||||
// String returns the string representation of a term.
|
||||
@@ -50,6 +52,7 @@ type Pattern struct {
|
||||
caseSensitive bool
|
||||
normalize bool
|
||||
forward bool
|
||||
withPos bool
|
||||
text []rune
|
||||
termSets []termSet
|
||||
sortable bool
|
||||
@@ -57,34 +60,24 @@ type Pattern struct {
|
||||
cacheKey string
|
||||
delimiter Delimiter
|
||||
nth []Range
|
||||
procFun map[termType]algo.Algo
|
||||
revision revision
|
||||
procFun [6]algo.Algo
|
||||
cache *ChunkCache
|
||||
denylist map[int32]struct{}
|
||||
startIndex int32
|
||||
directAlgo algo.Algo
|
||||
directTerm *term
|
||||
}
|
||||
|
||||
var (
|
||||
_patternCache map[string]*Pattern
|
||||
_splitRegex *regexp.Regexp
|
||||
_cache ChunkCache
|
||||
)
|
||||
var _splitRegex *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
_splitRegex = regexp.MustCompile(" +")
|
||||
clearPatternCache()
|
||||
clearChunkCache()
|
||||
}
|
||||
|
||||
func clearPatternCache() {
|
||||
// We can uniquely identify the pattern for a given string since
|
||||
// search mode and caseMode do not change while the program is running
|
||||
_patternCache = make(map[string]*Pattern)
|
||||
}
|
||||
|
||||
func clearChunkCache() {
|
||||
_cache = NewChunkCache()
|
||||
}
|
||||
|
||||
// BuildPattern builds Pattern object from the given arguments
|
||||
func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
|
||||
cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
|
||||
func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
|
||||
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}, startIndex int32) *Pattern {
|
||||
|
||||
var asString string
|
||||
if extended {
|
||||
@@ -96,7 +89,9 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
|
||||
asString = string(runes)
|
||||
}
|
||||
|
||||
cached, found := _patternCache[asString]
|
||||
// We can uniquely identify the pattern for a given string since
|
||||
// search mode and caseMode do not change while the program is running
|
||||
cached, found := patternCache[asString]
|
||||
if found {
|
||||
return cached
|
||||
}
|
||||
@@ -128,6 +123,8 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
|
||||
}
|
||||
} else {
|
||||
lowerString := strings.ToLower(asString)
|
||||
normalize = normalize &&
|
||||
lowerString == string(algo.NormalizeRunes([]rune(lowerString)))
|
||||
caseSensitive = caseMode == CaseRespect ||
|
||||
caseMode == CaseSmart && lowerString != asString
|
||||
if !caseSensitive {
|
||||
@@ -142,37 +139,46 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
|
||||
caseSensitive: caseSensitive,
|
||||
normalize: normalize,
|
||||
forward: forward,
|
||||
withPos: withPos,
|
||||
text: []rune(asString),
|
||||
termSets: termSets,
|
||||
sortable: sortable,
|
||||
cacheable: cacheable,
|
||||
nth: nth,
|
||||
revision: revision,
|
||||
delimiter: delimiter,
|
||||
procFun: make(map[termType]algo.Algo)}
|
||||
cache: cache,
|
||||
denylist: denylist,
|
||||
startIndex: startIndex,
|
||||
}
|
||||
|
||||
ptr.cacheKey = ptr.buildCacheKey()
|
||||
ptr.directAlgo, ptr.directTerm = ptr.buildDirectAlgo(fuzzyAlgo)
|
||||
ptr.procFun[termFuzzy] = fuzzyAlgo
|
||||
ptr.procFun[termEqual] = algo.EqualMatch
|
||||
ptr.procFun[termExact] = algo.ExactMatchNaive
|
||||
ptr.procFun[termExactBoundary] = algo.ExactMatchBoundary
|
||||
ptr.procFun[termPrefix] = algo.PrefixMatch
|
||||
ptr.procFun[termSuffix] = algo.SuffixMatch
|
||||
|
||||
_patternCache[asString] = ptr
|
||||
patternCache[asString] = ptr
|
||||
return ptr
|
||||
}
|
||||
|
||||
func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet {
|
||||
str = strings.Replace(str, "\\ ", "\t", -1)
|
||||
str = strings.ReplaceAll(str, "\\ ", "\t")
|
||||
tokens := _splitRegex.Split(str, -1)
|
||||
sets := []termSet{}
|
||||
set := termSet{}
|
||||
switchSet := false
|
||||
afterBar := false
|
||||
for _, token := range tokens {
|
||||
typ, inv, text := termFuzzy, false, strings.Replace(token, "\t", " ", -1)
|
||||
typ, inv, text := termFuzzy, false, strings.ReplaceAll(token, "\t", " ")
|
||||
lowerText := strings.ToLower(text)
|
||||
caseSensitive := caseMode == CaseRespect ||
|
||||
caseMode == CaseSmart && text != lowerText
|
||||
normalizeTerm := normalize &&
|
||||
lowerText == string(algo.NormalizeRunes([]rune(lowerText)))
|
||||
if !caseSensitive {
|
||||
text = lowerText
|
||||
}
|
||||
@@ -198,15 +204,17 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
||||
text = text[:len(text)-1]
|
||||
}
|
||||
|
||||
if strings.HasPrefix(text, "'") {
|
||||
if len(text) > 2 && strings.HasPrefix(text, "'") && strings.HasSuffix(text, "'") {
|
||||
typ = termExactBoundary
|
||||
text = text[1 : len(text)-1]
|
||||
} else if strings.HasPrefix(text, "'") {
|
||||
// Flip exactness
|
||||
if fuzzy && !inv {
|
||||
typ = termExact
|
||||
text = text[1:]
|
||||
} else {
|
||||
typ = termFuzzy
|
||||
text = text[1:]
|
||||
}
|
||||
text = text[1:]
|
||||
} else if strings.HasPrefix(text, "^") {
|
||||
if typ == termSuffix {
|
||||
typ = termEqual
|
||||
@@ -222,14 +230,15 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
||||
set = termSet{}
|
||||
}
|
||||
textRunes := []rune(text)
|
||||
if normalize {
|
||||
if normalizeTerm {
|
||||
textRunes = algo.NormalizeRunes(textRunes)
|
||||
}
|
||||
set = append(set, term{
|
||||
typ: typ,
|
||||
inv: inv,
|
||||
text: textRunes,
|
||||
caseSensitive: caseSensitive})
|
||||
caseSensitive: caseSensitive,
|
||||
normalize: normalizeTerm})
|
||||
switchSet = true
|
||||
}
|
||||
}
|
||||
@@ -241,6 +250,9 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
||||
|
||||
// IsEmpty returns true if the pattern is effectively empty
|
||||
func (p *Pattern) IsEmpty() bool {
|
||||
if len(p.denylist) > 0 {
|
||||
return false
|
||||
}
|
||||
if !p.extended {
|
||||
return len(p.text) == 0
|
||||
}
|
||||
@@ -265,6 +277,22 @@ func (p *Pattern) buildCacheKey() string {
|
||||
return strings.Join(cacheableTerms, "\t")
|
||||
}
|
||||
|
||||
// buildDirectAlgo returns the algo function and term for the direct fast path
|
||||
// in matchChunk. Returns (nil, nil) if the pattern is not suitable.
|
||||
// Requirements: extended mode, single term set with single non-inverse fuzzy term, no nth.
|
||||
func (p *Pattern) buildDirectAlgo(fuzzyAlgo algo.Algo) (algo.Algo, *term) {
|
||||
if !p.extended || len(p.nth) > 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if len(p.termSets) == 1 && len(p.termSets[0]) == 1 {
|
||||
t := &p.termSets[0][0]
|
||||
if !t.inv && t.typ == termFuzzy {
|
||||
return fuzzyAlgo, t
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// CacheKey is used to build string to be used as the key of result cache
|
||||
func (p *Pattern) CacheKey() string {
|
||||
return p.cacheKey
|
||||
@@ -272,66 +300,110 @@ func (p *Pattern) CacheKey() string {
|
||||
|
||||
// Match returns the list of matches Items in the given Chunk
|
||||
func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
|
||||
// ChunkCache: Exact match
|
||||
cacheKey := p.CacheKey()
|
||||
|
||||
// Bitmap cache: exact match or prefix/suffix
|
||||
var cachedBitmap *ChunkBitmap
|
||||
if p.cacheable {
|
||||
if cached := _cache.Lookup(chunk, cacheKey); cached != nil {
|
||||
return cached
|
||||
}
|
||||
cachedBitmap = p.cache.Lookup(chunk, cacheKey)
|
||||
}
|
||||
if cachedBitmap == nil {
|
||||
cachedBitmap = p.cache.Search(chunk, cacheKey)
|
||||
}
|
||||
|
||||
// Prefix/suffix cache
|
||||
space := _cache.Search(chunk, cacheKey)
|
||||
|
||||
matches := p.matchChunk(chunk, space, slab)
|
||||
matches, bitmap := p.matchChunk(chunk, cachedBitmap, slab)
|
||||
|
||||
if p.cacheable {
|
||||
_cache.Add(chunk, cacheKey, matches)
|
||||
p.cache.Add(chunk, cacheKey, bitmap, len(matches))
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
|
||||
func (p *Pattern) matchChunk(chunk *Chunk, cachedBitmap *ChunkBitmap, slab *util.Slab) ([]Result, ChunkBitmap) {
|
||||
matches := []Result{}
|
||||
var bitmap ChunkBitmap
|
||||
|
||||
if space == nil {
|
||||
for idx := 0; idx < chunk.count; idx++ {
|
||||
if match, _, _ := p.MatchItem(&chunk.items[idx], false, slab); match != nil {
|
||||
matches = append(matches, *match)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, result := range space {
|
||||
if match, _, _ := p.MatchItem(result.item, false, slab); match != nil {
|
||||
matches = append(matches, *match)
|
||||
}
|
||||
// Skip header items in chunks that contain them
|
||||
startIdx := 0
|
||||
if p.startIndex > 0 && chunk.count > 0 && chunk.items[0].Index() < p.startIndex {
|
||||
startIdx = int(p.startIndex - chunk.items[0].Index())
|
||||
if startIdx >= chunk.count {
|
||||
return matches, bitmap
|
||||
}
|
||||
}
|
||||
return matches
|
||||
|
||||
hasCachedBitmap := cachedBitmap != nil
|
||||
|
||||
// Fast path: single fuzzy term, no nth, no denylist.
|
||||
// Calls the algo function directly, bypassing MatchItem/extendedMatch/iter
|
||||
// and avoiding per-match []Offset heap allocation.
|
||||
if p.directAlgo != nil && len(p.denylist) == 0 {
|
||||
t := p.directTerm
|
||||
for idx := startIdx; idx < chunk.count; idx++ {
|
||||
if hasCachedBitmap && cachedBitmap[idx/64]&(uint64(1)<<(idx%64)) == 0 {
|
||||
continue
|
||||
}
|
||||
res, _ := p.directAlgo(t.caseSensitive, t.normalize, p.forward,
|
||||
&chunk.items[idx].text, t.text, p.withPos, slab)
|
||||
if res.Start >= 0 {
|
||||
bitmap[idx/64] |= uint64(1) << (idx % 64)
|
||||
matches = append(matches, buildResultFromBounds(
|
||||
&chunk.items[idx], res.Score,
|
||||
int(res.Start), int(res.End), int(res.End), true))
|
||||
}
|
||||
}
|
||||
return matches, bitmap
|
||||
}
|
||||
|
||||
if len(p.denylist) == 0 {
|
||||
for idx := startIdx; idx < chunk.count; idx++ {
|
||||
if hasCachedBitmap && cachedBitmap[idx/64]&(uint64(1)<<(idx%64)) == 0 {
|
||||
continue
|
||||
}
|
||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match.item != nil {
|
||||
bitmap[idx/64] |= uint64(1) << (idx % 64)
|
||||
matches = append(matches, match)
|
||||
}
|
||||
}
|
||||
return matches, bitmap
|
||||
}
|
||||
|
||||
for idx := startIdx; idx < chunk.count; idx++ {
|
||||
if hasCachedBitmap && cachedBitmap[idx/64]&(uint64(1)<<(idx%64)) == 0 {
|
||||
continue
|
||||
}
|
||||
if _, prs := p.denylist[chunk.items[idx].Index()]; prs {
|
||||
continue
|
||||
}
|
||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match.item != nil {
|
||||
bitmap[idx/64] |= uint64(1) << (idx % 64)
|
||||
matches = append(matches, match)
|
||||
}
|
||||
}
|
||||
return matches, bitmap
|
||||
}
|
||||
|
||||
// MatchItem returns true if the Item is a match
|
||||
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) {
|
||||
// MatchItem returns the match result if the Item is a match.
|
||||
// A zero-value Result (with item == nil) indicates no match.
|
||||
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (Result, []Offset, *[]int) {
|
||||
if p.extended {
|
||||
if offsets, bonus, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) {
|
||||
result := buildResult(item, offsets, bonus)
|
||||
return &result, offsets, pos
|
||||
return buildResult(item, offsets, bonus), offsets, pos
|
||||
}
|
||||
return nil, nil, nil
|
||||
return Result{}, nil, nil
|
||||
}
|
||||
offset, bonus, pos := p.basicMatch(item, withPos, slab)
|
||||
if sidx := offset[0]; sidx >= 0 {
|
||||
offsets := []Offset{offset}
|
||||
result := buildResult(item, offsets, bonus)
|
||||
return &result, offsets, pos
|
||||
return buildResult(item, offsets, bonus), offsets, pos
|
||||
}
|
||||
return nil, nil, nil
|
||||
return Result{}, nil, nil
|
||||
}
|
||||
|
||||
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
|
||||
var input []Token
|
||||
if len(p.nth) == 0 {
|
||||
input = []Token{Token{text: &item.text, prefixLength: 0}}
|
||||
input = []Token{{text: &item.text, prefixLength: 0}}
|
||||
} else {
|
||||
input = p.transformInput(item)
|
||||
}
|
||||
@@ -344,7 +416,7 @@ func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset,
|
||||
func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) {
|
||||
var input []Token
|
||||
if len(p.nth) == 0 {
|
||||
input = []Token{Token{text: &item.text, prefixLength: 0}}
|
||||
input = []Token{{text: &item.text, prefixLength: 0}}
|
||||
} else {
|
||||
input = p.transformInput(item)
|
||||
}
|
||||
@@ -360,7 +432,7 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of
|
||||
matched := false
|
||||
for _, term := range termSet {
|
||||
pfun := p.procFun[term.typ]
|
||||
off, score, pos := p.iter(pfun, input, term.caseSensitive, p.normalize, p.forward, term.text, withPos, slab)
|
||||
off, score, pos := p.iter(pfun, input, term.caseSensitive, term.normalize, p.forward, term.text, withPos, slab)
|
||||
if sidx := off[0]; sidx >= 0 {
|
||||
if term.inv {
|
||||
continue
|
||||
@@ -393,12 +465,22 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of
|
||||
|
||||
func (p *Pattern) transformInput(item *Item) []Token {
|
||||
if item.transformed != nil {
|
||||
return *item.transformed
|
||||
transformed := *item.transformed
|
||||
if transformed.revision == p.revision {
|
||||
return transformed.tokens
|
||||
}
|
||||
}
|
||||
|
||||
tokens := Tokenize(item.text.ToString(), p.delimiter)
|
||||
ret := Transform(tokens, p.nth)
|
||||
item.transformed = &ret
|
||||
// Strip the last delimiter to allow suffix match
|
||||
if len(ret) > 0 && !p.delimiter.IsAwk() {
|
||||
chars := ret[len(ret)-1].text
|
||||
stripped := StripLastDelimiter(chars.ToString(), p.delimiter)
|
||||
newChars := util.ToChars(stringBytes(stripped))
|
||||
ret[len(ret)-1].text = &newChars
|
||||
}
|
||||
item.transformed = &transformed{p.revision, ret}
|
||||
return ret
|
||||
}
|
||||
|
||||
|
||||
+143
-31
@@ -2,6 +2,7 @@ package fzf
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/algo"
|
||||
@@ -64,10 +65,15 @@ func TestParseTermsEmpty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func buildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
|
||||
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
|
||||
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
|
||||
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
|
||||
withPos, cacheable, nth, delimiter, revision{}, runes, nil, 0)
|
||||
}
|
||||
|
||||
func TestExact(t *testing.T) {
|
||||
defer clearPatternCache()
|
||||
clearPatternCache()
|
||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true,
|
||||
pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true,
|
||||
[]Range{}, Delimiter{}, []rune("'abc"))
|
||||
chars := util.ToChars([]byte("aabbcc abc"))
|
||||
res, pos := algo.ExactMatchNaive(
|
||||
@@ -81,9 +87,7 @@ func TestExact(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEqual(t *testing.T) {
|
||||
defer clearPatternCache()
|
||||
clearPatternCache()
|
||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("^AbC$"))
|
||||
pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("^AbC$"))
|
||||
|
||||
match := func(str string, sidxExpected int, eidxExpected int) {
|
||||
chars := util.ToChars([]byte(str))
|
||||
@@ -98,22 +102,18 @@ func TestEqual(t *testing.T) {
|
||||
}
|
||||
match("ABC", -1, -1)
|
||||
match("AbC", 0, 3)
|
||||
match("AbC ", 0, 3)
|
||||
match(" AbC ", 1, 4)
|
||||
match(" AbC", 2, 5)
|
||||
}
|
||||
|
||||
func TestCaseSensitivity(t *testing.T) {
|
||||
defer clearPatternCache()
|
||||
clearPatternCache()
|
||||
pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
clearPatternCache()
|
||||
pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
clearPatternCache()
|
||||
pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
clearPatternCache()
|
||||
pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
clearPatternCache()
|
||||
pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
clearPatternCache()
|
||||
pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
pat1 := buildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
pat2 := buildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
pat3 := buildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
pat4 := buildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
pat5 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
pat6 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
|
||||
if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
|
||||
string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
|
||||
@@ -126,9 +126,9 @@ func TestCaseSensitivity(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOrigTextAndTransformed(t *testing.T) {
|
||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg"))
|
||||
pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("jg"))
|
||||
tokens := Tokenize("junegunn", Delimiter{})
|
||||
trans := Transform(tokens, []Range{Range{1, 1}})
|
||||
trans := Transform(tokens, []Range{{1, 1}})
|
||||
|
||||
origBytes := []byte("junegunn.choi")
|
||||
for _, extended := range []bool{false, true} {
|
||||
@@ -136,12 +136,12 @@ func TestOrigTextAndTransformed(t *testing.T) {
|
||||
chunk.items[0] = Item{
|
||||
text: util.ToChars([]byte("junegunn")),
|
||||
origText: &origBytes,
|
||||
transformed: &trans}
|
||||
transformed: &transformed{pattern.revision, trans}}
|
||||
pattern.extended = extended
|
||||
matches := pattern.matchChunk(&chunk, nil, slab) // No cache
|
||||
matches, _ := pattern.matchChunk(&chunk, nil, slab) // No cache
|
||||
if !(matches[0].item.text.ToString() == "junegunn" &&
|
||||
string(*matches[0].item.origText) == "junegunn.choi" &&
|
||||
reflect.DeepEqual(*matches[0].item.transformed, trans)) {
|
||||
reflect.DeepEqual((*matches[0].item.transformed).tokens, trans)) {
|
||||
t.Error("Invalid match result", matches)
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ func TestOrigTextAndTransformed(t *testing.T) {
|
||||
if !(match.item.text.ToString() == "junegunn" &&
|
||||
string(*match.item.origText) == "junegunn.choi" &&
|
||||
offsets[0][0] == 0 && offsets[0][1] == 5 &&
|
||||
reflect.DeepEqual(*match.item.transformed, trans)) {
|
||||
reflect.DeepEqual((*match.item.transformed).tokens, trans)) {
|
||||
t.Error("Invalid match result", match, offsets, extended)
|
||||
}
|
||||
if !((*pos)[0] == 4 && (*pos)[1] == 0) {
|
||||
@@ -160,15 +160,13 @@ func TestOrigTextAndTransformed(t *testing.T) {
|
||||
|
||||
func TestCacheKey(t *testing.T) {
|
||||
test := func(extended bool, patStr string, expected string, cacheable bool) {
|
||||
clearPatternCache()
|
||||
pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune(patStr))
|
||||
pat := buildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune(patStr))
|
||||
if pat.CacheKey() != expected {
|
||||
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
|
||||
}
|
||||
if pat.cacheable != cacheable {
|
||||
t.Errorf("Expected: %t, actual: %t (%s)", cacheable, pat.cacheable, patStr)
|
||||
}
|
||||
clearPatternCache()
|
||||
}
|
||||
test(false, "foo !bar", "foo !bar", true)
|
||||
test(false, "foo | bar !baz", "foo | bar !baz", true)
|
||||
@@ -184,15 +182,13 @@ func TestCacheKey(t *testing.T) {
|
||||
|
||||
func TestCacheable(t *testing.T) {
|
||||
test := func(fuzzy bool, str string, expected string, cacheable bool) {
|
||||
clearPatternCache()
|
||||
pat := BuildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, true, []Range{}, Delimiter{}, []rune(str))
|
||||
pat := buildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, false, true, []Range{}, Delimiter{}, []rune(str))
|
||||
if pat.CacheKey() != expected {
|
||||
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
|
||||
}
|
||||
if cacheable != pat.cacheable {
|
||||
t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable)
|
||||
}
|
||||
clearPatternCache()
|
||||
}
|
||||
test(true, "foo bar", "foo\tbar", true)
|
||||
test(true, "foo 'bar", "foo\tbar", false)
|
||||
@@ -204,3 +200,119 @@ func TestCacheable(t *testing.T) {
|
||||
test(false, "foo 'bar", "foo", false)
|
||||
test(false, "foo !bar", "foo", false)
|
||||
}
|
||||
|
||||
func buildChunks(numChunks int) []*Chunk {
|
||||
chunks := make([]*Chunk, numChunks)
|
||||
words := []string{
|
||||
"src/main/java/com/example/service/UserService.java",
|
||||
"src/test/java/com/example/service/UserServiceTest.java",
|
||||
"docs/api/reference/endpoints.md",
|
||||
"lib/internal/utils/string_helper.go",
|
||||
"pkg/server/http/handler/auth.go",
|
||||
"build/output/release/app.exe",
|
||||
"config/production/database.yml",
|
||||
"scripts/deploy/kubernetes/setup.sh",
|
||||
"vendor/github.com/junegunn/fzf/src/core.go",
|
||||
"node_modules/.cache/babel/transform.js",
|
||||
}
|
||||
for ci := range numChunks {
|
||||
chunks[ci] = &Chunk{count: chunkSize}
|
||||
for i := range chunkSize {
|
||||
text := words[(ci*chunkSize+i)%len(words)]
|
||||
chunks[ci].items[i] = Item{text: util.ToChars([]byte(text))}
|
||||
chunks[ci].items[i].text.Index = int32(ci*chunkSize + i)
|
||||
}
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
func buildPatternWith(cache *ChunkCache, runes []rune) *Pattern {
|
||||
return BuildPattern(cache, make(map[string]*Pattern),
|
||||
true, algo.FuzzyMatchV2, true, CaseSmart, false, true,
|
||||
false, true, []Range{}, Delimiter{}, revision{}, runes, nil, 0)
|
||||
}
|
||||
|
||||
func TestBitmapCacheBenefit(t *testing.T) {
|
||||
numChunks := 100
|
||||
chunks := buildChunks(numChunks)
|
||||
queries := []string{"s", "se", "ser", "serv", "servi"}
|
||||
|
||||
// 1. Run all queries with shared cache (simulates incremental typing)
|
||||
cache := NewChunkCache()
|
||||
for _, q := range queries {
|
||||
pat := buildPatternWith(cache, []rune(q))
|
||||
for _, chunk := range chunks {
|
||||
pat.Match(chunk, slab)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. GC and measure memory with cache populated
|
||||
runtime.GC()
|
||||
runtime.GC()
|
||||
var memWith runtime.MemStats
|
||||
runtime.ReadMemStats(&memWith)
|
||||
|
||||
// 3. Clear cache, GC, measure again
|
||||
cache.Clear()
|
||||
runtime.GC()
|
||||
runtime.GC()
|
||||
var memWithout runtime.MemStats
|
||||
runtime.ReadMemStats(&memWithout)
|
||||
|
||||
cacheMem := int64(memWith.Alloc) - int64(memWithout.Alloc)
|
||||
t.Logf("Chunks: %d, Queries: %d", numChunks, len(queries))
|
||||
t.Logf("Cache memory: %d bytes (%.1f KB)", cacheMem, float64(cacheMem)/1024)
|
||||
t.Logf("Per-chunk-per-query: %.0f bytes", float64(cacheMem)/float64(numChunks*len(queries)))
|
||||
|
||||
// 4. Verify correctness: cached vs uncached produce same results
|
||||
cache2 := NewChunkCache()
|
||||
for _, q := range queries {
|
||||
pat := buildPatternWith(cache2, []rune(q))
|
||||
for _, chunk := range chunks {
|
||||
pat.Match(chunk, slab)
|
||||
}
|
||||
}
|
||||
for _, q := range queries {
|
||||
patCached := buildPatternWith(cache2, []rune(q))
|
||||
patFresh := buildPatternWith(NewChunkCache(), []rune(q))
|
||||
var countCached, countFresh int
|
||||
for _, chunk := range chunks {
|
||||
countCached += len(patCached.Match(chunk, slab))
|
||||
countFresh += len(patFresh.Match(chunk, slab))
|
||||
}
|
||||
if countCached != countFresh {
|
||||
t.Errorf("query=%q: cached=%d, fresh=%d", q, countCached, countFresh)
|
||||
}
|
||||
t.Logf("query=%q: matches=%d", q, countCached)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWithCache(b *testing.B) {
|
||||
numChunks := 100
|
||||
chunks := buildChunks(numChunks)
|
||||
queries := []string{"s", "se", "ser", "serv", "servi"}
|
||||
|
||||
b.Run("cached", func(b *testing.B) {
|
||||
for range b.N {
|
||||
cache := NewChunkCache()
|
||||
for _, q := range queries {
|
||||
pat := buildPatternWith(cache, []rune(q))
|
||||
for _, chunk := range chunks {
|
||||
pat.Match(chunk, slab)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("uncached", func(b *testing.B) {
|
||||
for range b.N {
|
||||
for _, q := range queries {
|
||||
cache := NewChunkCache()
|
||||
pat := buildPatternWith(cache, []rune(q))
|
||||
for _, chunk := range chunks {
|
||||
pat.Match(chunk, slab)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
//go:build !openbsd
|
||||
|
||||
package protector
|
||||
|
||||
// Protect calls OS specific protections like pledge on OpenBSD
|
||||
func Protect() {}
|
||||
@@ -0,0 +1,10 @@
|
||||
//go:build openbsd
|
||||
|
||||
package protector
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
// Protect calls OS specific protections like pledge on OpenBSD
|
||||
func Protect() {
|
||||
unix.PledgePromises("stdio cpath dpath wpath rpath inet fattr unix tty proc exec")
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
const becomeSuffix = ".become"
|
||||
|
||||
func escapeSingleQuote(str string) string {
|
||||
return "'" + strings.ReplaceAll(str, "'", "'\\''") + "'"
|
||||
}
|
||||
|
||||
func popupArgStr(args []string, opts *Options) (string, string) {
|
||||
fzf, rest := args[0], args[1:]
|
||||
args = []string{"--bind=ctrl-z:ignore"}
|
||||
if !opts.Tmux.border && (opts.BorderShape == tui.BorderUndefined || opts.BorderShape == tui.BorderLine) {
|
||||
if tui.DefaultBorderShape == tui.BorderRounded {
|
||||
rest = append(rest, "--border=rounded")
|
||||
} else {
|
||||
rest = append(rest, "--border=sharp")
|
||||
}
|
||||
}
|
||||
if opts.Tmux.border && opts.Margin == defaultMargin() {
|
||||
args = append(args, "--margin=0,1")
|
||||
}
|
||||
argStr := escapeSingleQuote(fzf)
|
||||
for _, arg := range append(args, rest...) {
|
||||
argStr += " " + escapeSingleQuote(arg)
|
||||
}
|
||||
argStr += ` --no-popup --no-height`
|
||||
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
dir = "."
|
||||
}
|
||||
return argStr, dir
|
||||
}
|
||||
|
||||
func fifo(name string) (string, error) {
|
||||
ns := time.Now().UnixNano()
|
||||
output := filepath.Join(os.TempDir(), fmt.Sprintf("fzf-%s-%d", name, ns))
|
||||
output, err := mkfifo(output, 0600)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool) (*exec.Cmd, error), opts *Options, withExports bool) (int, error) {
|
||||
output, err := fifo("proxy-output")
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
defer os.Remove(output)
|
||||
|
||||
// Take the output
|
||||
go func() {
|
||||
withOutputPipe(output, func(outputFile io.ReadCloser) {
|
||||
if opts.Output == nil {
|
||||
io.Copy(os.Stdout, outputFile)
|
||||
} else {
|
||||
reader := bufio.NewReader(outputFile)
|
||||
sep := opts.PrintSep[0]
|
||||
for {
|
||||
item, err := reader.ReadString(sep)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
opts.Output <- item
|
||||
}
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
var command, input string
|
||||
commandPrefix += ` --no-force-tty-in --proxy-script "$0"`
|
||||
if opts.Input == nil && (opts.ForceTtyIn || util.IsTty(os.Stdin)) {
|
||||
command = fmt.Sprintf(`%s > %q`, commandPrefix, output)
|
||||
} else {
|
||||
input, err = fifo("proxy-input")
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
defer os.Remove(input)
|
||||
|
||||
go func() {
|
||||
withInputPipe(input, func(inputFile io.WriteCloser) {
|
||||
if opts.Input == nil {
|
||||
io.Copy(inputFile, os.Stdin)
|
||||
} else {
|
||||
for item := range opts.Input {
|
||||
fmt.Fprint(inputFile, item+opts.PrintSep)
|
||||
}
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
if withExports {
|
||||
command = fmt.Sprintf(`%s < %q > %q`, commandPrefix, input, output)
|
||||
} else {
|
||||
// For mintty: cannot directly read named pipe from Go code
|
||||
command = fmt.Sprintf(`command cat %q | %s > %q`, input, commandPrefix, output)
|
||||
}
|
||||
}
|
||||
|
||||
// Write the command to a temporary file and run it with sh to ensure POSIX compliance.
|
||||
var exports []string
|
||||
needBash := false
|
||||
if withExports {
|
||||
// Nullify FZF_DEFAULT_* variables as tmux popup may inject them even when undefined.
|
||||
exports = []string{"FZF_DEFAULT_COMMAND=", "FZF_DEFAULT_OPTS=", "FZF_DEFAULT_OPTS_FILE="}
|
||||
validIdentifier := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
||||
for _, pairStr := range os.Environ() {
|
||||
pair := strings.SplitN(pairStr, "=", 2)
|
||||
if validIdentifier.MatchString(pair[0]) {
|
||||
exports = append(exports, fmt.Sprintf("export %s=%s", pair[0], escapeSingleQuote(pair[1])))
|
||||
} else if strings.HasPrefix(pair[0], "BASH_FUNC_") && strings.HasSuffix(pair[0], "%%") {
|
||||
name := pair[0][10 : len(pair[0])-2]
|
||||
exports = append(exports, name+pair[1])
|
||||
exports = append(exports, "export -f "+name)
|
||||
needBash = true
|
||||
}
|
||||
}
|
||||
}
|
||||
temp := WriteTemporaryFile(append(exports, command), "\n")
|
||||
defer os.Remove(temp)
|
||||
|
||||
cmd, err := cmdBuilder(temp, needBash)
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
cmd.Stderr = os.Stderr
|
||||
intChan := make(chan os.Signal, 1)
|
||||
defer close(intChan)
|
||||
go func() {
|
||||
if sig, valid := <-intChan; valid {
|
||||
cmd.Process.Signal(sig)
|
||||
}
|
||||
}()
|
||||
signal.Notify(intChan, os.Interrupt)
|
||||
if err := cmd.Run(); err != nil {
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
code := exitError.ExitCode()
|
||||
if code == ExitBecome {
|
||||
becomeFile := temp + becomeSuffix
|
||||
data, err := os.ReadFile(becomeFile)
|
||||
os.Remove(becomeFile)
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
elems := strings.Split(string(data), "\x00")
|
||||
if len(elems) < 1 {
|
||||
return ExitError, errors.New("invalid become command")
|
||||
}
|
||||
command := elems[0]
|
||||
env := []string{}
|
||||
if len(elems) > 1 {
|
||||
env = elems[1:]
|
||||
}
|
||||
executor := util.NewExecutor(opts.WithShell)
|
||||
ttyin, err := tui.TtyIn(opts.TtyDefault)
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
os.Remove(temp)
|
||||
os.Remove(input)
|
||||
os.Remove(output)
|
||||
executor.Become(ttyin, env, command)
|
||||
}
|
||||
return code, err
|
||||
}
|
||||
}
|
||||
|
||||
return ExitOk, nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//go:build !windows
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func sh(bash bool) (string, error) {
|
||||
if bash {
|
||||
return "bash", nil
|
||||
}
|
||||
return "sh", nil
|
||||
}
|
||||
|
||||
func mkfifo(path string, mode uint32) (string, error) {
|
||||
return path, unix.Mkfifo(path, mode)
|
||||
}
|
||||
|
||||
func withOutputPipe(output string, task func(io.ReadCloser)) error {
|
||||
outputFile, err := os.OpenFile(output, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
task(outputFile)
|
||||
outputFile.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func withInputPipe(input string, task func(io.WriteCloser)) error {
|
||||
inputFile, err := os.OpenFile(input, os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
task(inputFile)
|
||||
inputFile.Close()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//go:build windows
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
var shPath atomic.Value
|
||||
|
||||
func sh(bash bool) (string, error) {
|
||||
if cached := shPath.Load(); cached != nil {
|
||||
return cached.(string), nil
|
||||
}
|
||||
|
||||
name := "sh"
|
||||
if bash {
|
||||
name = "bash"
|
||||
}
|
||||
cmd := exec.Command("cygpath", "-w", "/usr/bin/"+name)
|
||||
bytes, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sh := strings.TrimSpace(string(bytes))
|
||||
shPath.Store(sh)
|
||||
return sh, nil
|
||||
}
|
||||
|
||||
func mkfifo(path string, mode uint32) (string, error) {
|
||||
m := strconv.FormatUint(uint64(mode), 8)
|
||||
sh, err := sh(false)
|
||||
if err != nil {
|
||||
return path, err
|
||||
}
|
||||
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command mkfifo -m %s %q`, m, path))
|
||||
if err := cmd.Run(); err != nil {
|
||||
return path, err
|
||||
}
|
||||
return path + ".lnk", nil
|
||||
}
|
||||
|
||||
func withOutputPipe(output string, task func(io.ReadCloser)) error {
|
||||
sh, err := sh(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command cat %q`, output))
|
||||
outputFile, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
task(outputFile)
|
||||
cmd.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func withInputPipe(input string, task func(io.WriteCloser)) error {
|
||||
sh, err := sh(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command cat - > %q`, input))
|
||||
inputFile, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
task(inputFile)
|
||||
inputFile.Close()
|
||||
cmd.Wait()
|
||||
return nil
|
||||
}
|
||||
+298
-53
@@ -1,34 +1,51 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/charlievieth/fastwalk"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
// Reader reads from command or standard input
|
||||
type Reader struct {
|
||||
pusher func([]byte) bool
|
||||
executor *util.Executor
|
||||
eventBox *util.EventBox
|
||||
delimNil bool
|
||||
event int32
|
||||
finChan chan bool
|
||||
mutex sync.Mutex
|
||||
exec *exec.Cmd
|
||||
command *string
|
||||
killed bool
|
||||
termFunc func()
|
||||
command *string
|
||||
wait bool
|
||||
}
|
||||
|
||||
// NewReader returns new Reader object
|
||||
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool, wait bool) *Reader {
|
||||
return &Reader{pusher, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait}
|
||||
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, executor *util.Executor, delimNil bool, wait bool) *Reader {
|
||||
return &Reader{
|
||||
pusher,
|
||||
executor,
|
||||
eventBox,
|
||||
delimNil,
|
||||
int32(EvtReady),
|
||||
make(chan bool, 1),
|
||||
sync.Mutex{},
|
||||
false,
|
||||
func() { os.Stdin.Close() },
|
||||
nil,
|
||||
wait}
|
||||
}
|
||||
|
||||
func (r *Reader) startEventPoller() {
|
||||
@@ -73,69 +90,151 @@ func (r *Reader) fin(success bool) {
|
||||
|
||||
func (r *Reader) terminate() {
|
||||
r.mutex.Lock()
|
||||
defer func() { r.mutex.Unlock() }()
|
||||
|
||||
r.killed = true
|
||||
if r.exec != nil && r.exec.Process != nil {
|
||||
util.KillCommand(r.exec)
|
||||
} else {
|
||||
os.Stdin.Close()
|
||||
if r.termFunc != nil {
|
||||
r.termFunc()
|
||||
r.termFunc = nil
|
||||
}
|
||||
r.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (r *Reader) restart(command string) {
|
||||
func (r *Reader) restart(command commandSpec, environ []string, readyChan chan bool) {
|
||||
r.event = int32(EvtReady)
|
||||
r.startEventPoller()
|
||||
success := r.readFromCommand(nil, command)
|
||||
success := r.readFromCommand(command.command, environ, func() {
|
||||
readyChan <- true
|
||||
})
|
||||
r.fin(success)
|
||||
removeFiles(command.tempFiles)
|
||||
}
|
||||
|
||||
func (r *Reader) readChannel(inputChan chan string) bool {
|
||||
for {
|
||||
item, more := <-inputChan
|
||||
if !more {
|
||||
break
|
||||
}
|
||||
if r.pusher([]byte(item)) {
|
||||
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ReadSource reads data from the default command or from standard input
|
||||
func (r *Reader) ReadSource() {
|
||||
func (r *Reader) ReadSource(inputChan chan string, roots []string, opts walkerOpts, ignores []string, initCmd string, initEnv []string, readyChan chan bool) {
|
||||
r.startEventPoller()
|
||||
var success bool
|
||||
if util.IsTty() {
|
||||
// The default command for *nix requires bash
|
||||
shell := "bash"
|
||||
signalReady := func() {
|
||||
if readyChan != nil {
|
||||
readyChan <- true
|
||||
}
|
||||
}
|
||||
if inputChan != nil {
|
||||
signalReady()
|
||||
success = r.readChannel(inputChan)
|
||||
} else if len(initCmd) > 0 {
|
||||
success = r.readFromCommand(initCmd, initEnv, signalReady)
|
||||
} else if util.IsTty(os.Stdin) {
|
||||
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
|
||||
if len(cmd) == 0 {
|
||||
success = r.readFromCommand(&shell, defaultCommand)
|
||||
signalReady()
|
||||
success = r.readFiles(roots, opts, ignores)
|
||||
} else {
|
||||
success = r.readFromCommand(nil, cmd)
|
||||
success = r.readFromCommand(cmd, initEnv, signalReady)
|
||||
}
|
||||
} else {
|
||||
signalReady()
|
||||
success = r.readFromStdin()
|
||||
}
|
||||
r.fin(success)
|
||||
}
|
||||
|
||||
func (r *Reader) feed(src io.Reader) {
|
||||
/*
|
||||
readerSlabSize, ae := strconv.Atoi(os.Getenv("SLAB_KB"))
|
||||
if ae != nil {
|
||||
readerSlabSize = 128 * 1024
|
||||
} else {
|
||||
readerSlabSize *= 1024
|
||||
}
|
||||
readerBufferSize, be := strconv.Atoi(os.Getenv("BUF_KB"))
|
||||
if be != nil {
|
||||
readerBufferSize = 64 * 1024
|
||||
} else {
|
||||
readerBufferSize *= 1024
|
||||
}
|
||||
*/
|
||||
|
||||
delim := byte('\n')
|
||||
trimCR := util.IsWindows()
|
||||
if r.delimNil {
|
||||
delim = '\000'
|
||||
trimCR = false
|
||||
}
|
||||
reader := bufio.NewReaderSize(src, readerBufferSize)
|
||||
|
||||
slab := make([]byte, readerSlabSize)
|
||||
leftover := []byte{}
|
||||
var err error
|
||||
for {
|
||||
// ReadBytes returns err != nil if and only if the returned data does not
|
||||
// end in delim.
|
||||
bytea, err := reader.ReadBytes(delim)
|
||||
byteaLen := len(bytea)
|
||||
if byteaLen > 0 {
|
||||
if err == nil {
|
||||
// get rid of carriage return if under Windows:
|
||||
if util.IsWindows() && byteaLen >= 2 && bytea[byteaLen-2] == byte('\r') {
|
||||
bytea = bytea[:byteaLen-2]
|
||||
} else {
|
||||
bytea = bytea[:byteaLen-1]
|
||||
}
|
||||
}
|
||||
if r.pusher(bytea) {
|
||||
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||
n := 0
|
||||
scope := slab[:min(len(slab), readerBufferSize)]
|
||||
for range 100 {
|
||||
n, err = src.Read(scope)
|
||||
if n > 0 || err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
// We're not making any progress after 100 tries. Stop.
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
buf := slab[:n]
|
||||
slab = slab[n:]
|
||||
|
||||
for len(buf) > 0 {
|
||||
if i := bytes.IndexByte(buf, delim); i >= 0 {
|
||||
// Found the delimiter
|
||||
slice := buf[:i+1]
|
||||
buf = buf[i+1:]
|
||||
if trimCR && len(slice) >= 2 && slice[len(slice)-2] == byte('\r') {
|
||||
slice = slice[:len(slice)-2]
|
||||
} else {
|
||||
slice = slice[:len(slice)-1]
|
||||
}
|
||||
if len(leftover) > 0 {
|
||||
slice = append(leftover, slice...)
|
||||
leftover = []byte{}
|
||||
}
|
||||
if (err == nil || len(slice) > 0) && r.pusher(slice) {
|
||||
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||
}
|
||||
} else {
|
||||
// Could not find the delimiter in the buffer
|
||||
// NOTE: We can further optimize this by keeping track of the cursor
|
||||
// position in the slab so that a straddling item that doesn't go
|
||||
// beyond the boundary of a slab doesn't need to be copied to
|
||||
// another buffer. However, the performance gain is negligible in
|
||||
// practice (< 0.1%) and is not
|
||||
// worth the added complexity.
|
||||
leftover = append(leftover, buf...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
leftover = append(leftover, buf...)
|
||||
break
|
||||
}
|
||||
|
||||
if len(slab) == 0 {
|
||||
slab = make([]byte, readerSlabSize)
|
||||
}
|
||||
}
|
||||
if len(leftover) > 0 && r.pusher(leftover) {
|
||||
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,25 +243,171 @@ func (r *Reader) readFromStdin() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Reader) readFromCommand(shell *string, command string) bool {
|
||||
r.mutex.Lock()
|
||||
r.killed = false
|
||||
r.command = &command
|
||||
if shell != nil {
|
||||
r.exec = util.ExecCommandWith(*shell, command, true)
|
||||
} else {
|
||||
r.exec = util.ExecCommand(command, true)
|
||||
func isSymlinkToDir(path string, de os.DirEntry) bool {
|
||||
if de.Type()&fs.ModeSymlink == 0 {
|
||||
return false
|
||||
}
|
||||
out, err := r.exec.StdoutPipe()
|
||||
if err != nil {
|
||||
if s, err := os.Stat(path); err == nil {
|
||||
return s.IsDir()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func trimPath(path string) string {
|
||||
bytes := stringBytes(path)
|
||||
|
||||
for len(bytes) > 1 && bytes[0] == '.' && (bytes[1] == '/' || bytes[1] == '\\') {
|
||||
bytes = bytes[2:]
|
||||
}
|
||||
|
||||
if len(bytes) == 0 {
|
||||
return "."
|
||||
}
|
||||
|
||||
return byteString(bytes)
|
||||
}
|
||||
|
||||
func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bool {
|
||||
conf := fastwalk.Config{
|
||||
Follow: opts.follow,
|
||||
// Use forward slashes when running a Windows binary under WSL or MSYS
|
||||
ToSlash: fastwalk.DefaultToSlash(),
|
||||
Sort: fastwalk.SortFilesFirst,
|
||||
}
|
||||
|
||||
// When following symlinks, precompute the absolute real paths of walker
|
||||
// roots so we can skip symlinks that point to an ancestor. fastwalk's
|
||||
// built-in loop detection (shouldTraverse) catches loops on the second
|
||||
// pass, but a single pass through a symlink like z: -> / already
|
||||
// traverses the entire root filesystem, causing severe resource
|
||||
// exhaustion. Skipping ancestor symlinks prevents this entirely.
|
||||
var absRoots []string
|
||||
if opts.follow {
|
||||
for _, root := range roots {
|
||||
if real, err := filepath.EvalSymlinks(root); err == nil {
|
||||
if abs, err := filepath.Abs(real); err == nil {
|
||||
absRoots = append(absRoots, filepath.Clean(abs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ignoresBase := []string{}
|
||||
ignoresFull := []string{}
|
||||
ignoresSuffix := []string{}
|
||||
sep := string(os.PathSeparator)
|
||||
if _, ok := os.LookupEnv("MSYSTEM"); ok {
|
||||
sep = "/"
|
||||
}
|
||||
for _, ignore := range ignores {
|
||||
if strings.ContainsRune(ignore, os.PathSeparator) {
|
||||
if strings.HasPrefix(ignore, sep) {
|
||||
ignoresSuffix = append(ignoresSuffix, ignore)
|
||||
} else {
|
||||
// 'foo/bar' should match
|
||||
// * 'foo/bar'
|
||||
// * 'baz/foo/bar'
|
||||
// * but NOT 'bazfoo/bar'
|
||||
ignoresFull = append(ignoresFull, ignore)
|
||||
ignoresSuffix = append(ignoresSuffix, sep+ignore)
|
||||
}
|
||||
} else {
|
||||
ignoresBase = append(ignoresBase, ignore)
|
||||
}
|
||||
}
|
||||
fn := func(path string, de os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
path = trimPath(path)
|
||||
if path != "." {
|
||||
isDirSymlink := isSymlinkToDir(path, de)
|
||||
if isDirSymlink && !opts.follow {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
// Skip symlinks whose target is an ancestor of (or equal to)
|
||||
// any walker root. Following such symlinks would traverse a
|
||||
// superset of the tree we're already walking.
|
||||
if isDirSymlink && len(absRoots) > 0 {
|
||||
if target, err := filepath.EvalSymlinks(path); err == nil {
|
||||
if abs, err := filepath.Abs(target); err == nil {
|
||||
abs = filepath.Clean(abs)
|
||||
if abs == string(os.PathSeparator) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
for _, absRoot := range absRoots {
|
||||
if absRoot == abs || strings.HasPrefix(absRoot, abs+string(os.PathSeparator)) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
isDir := de.IsDir() || isDirSymlink
|
||||
if isDir {
|
||||
base := filepath.Base(path)
|
||||
if !opts.hidden && base[0] == '.' && base != ".." {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if slices.Contains(ignoresBase, base) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if slices.Contains(ignoresFull, path) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
for _, ignore := range ignoresSuffix {
|
||||
if strings.HasSuffix(path, ignore) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
if path != sep {
|
||||
path += sep
|
||||
}
|
||||
}
|
||||
if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher(stringBytes(path)) {
|
||||
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||
}
|
||||
}
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
if r.killed {
|
||||
return context.Canceled
|
||||
}
|
||||
return nil
|
||||
}
|
||||
noerr := true
|
||||
for _, root := range roots {
|
||||
noerr = noerr && (fastwalk.Walk(&conf, root, fn) == nil)
|
||||
}
|
||||
return noerr
|
||||
}
|
||||
|
||||
func (r *Reader) readFromCommand(command string, environ []string, signalReady func()) bool {
|
||||
r.mutex.Lock()
|
||||
|
||||
r.killed = false
|
||||
r.termFunc = nil
|
||||
r.command = &command
|
||||
exec := r.executor.ExecCommand(command, true)
|
||||
if environ != nil {
|
||||
exec.Env = environ
|
||||
}
|
||||
execOut, err := exec.StdoutPipe()
|
||||
if err != nil || exec.Start() != nil {
|
||||
signalReady()
|
||||
r.mutex.Unlock()
|
||||
return false
|
||||
}
|
||||
err = r.exec.Start()
|
||||
r.mutex.Unlock()
|
||||
if err != nil {
|
||||
return false
|
||||
|
||||
// Function to call to terminate the running command
|
||||
r.termFunc = func() {
|
||||
execOut.Close()
|
||||
util.KillCommand(exec)
|
||||
}
|
||||
r.feed(out)
|
||||
return r.exec.Wait() == nil
|
||||
|
||||
signalReady()
|
||||
r.mutex.Unlock()
|
||||
|
||||
r.feed(execOut)
|
||||
return exec.Wait() == nil
|
||||
}
|
||||
|
||||
+10
-5
@@ -10,9 +10,10 @@ import (
|
||||
func TestReadFromCommand(t *testing.T) {
|
||||
strs := []string{}
|
||||
eb := util.NewEventBox()
|
||||
exec := util.NewExecutor("")
|
||||
reader := NewReader(
|
||||
func(s []byte) bool { strs = append(strs, string(s)); return true },
|
||||
eb, false, true)
|
||||
eb, exec, false, true)
|
||||
|
||||
reader.startEventPoller()
|
||||
|
||||
@@ -22,8 +23,12 @@ func TestReadFromCommand(t *testing.T) {
|
||||
}
|
||||
|
||||
// Normal command
|
||||
reader.fin(reader.readFromCommand(nil, `echo abc && echo def`))
|
||||
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
|
||||
counter := 0
|
||||
ready := func() {
|
||||
counter++
|
||||
}
|
||||
reader.fin(reader.readFromCommand(`echo abc&&echo def`, nil, ready))
|
||||
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" || counter != 1 {
|
||||
t.Errorf("%s", strs)
|
||||
}
|
||||
|
||||
@@ -47,9 +52,9 @@ func TestReadFromCommand(t *testing.T) {
|
||||
reader.startEventPoller()
|
||||
|
||||
// Failing command
|
||||
reader.fin(reader.readFromCommand(nil, `no-such-command`))
|
||||
reader.fin(reader.readFromCommand(`no-such-command`, nil, ready))
|
||||
strs = []string{}
|
||||
if len(strs) > 0 {
|
||||
if len(strs) > 0 || counter != 2 {
|
||||
t.Errorf("%s", strs)
|
||||
}
|
||||
|
||||
|
||||
+256
-60
@@ -2,6 +2,7 @@ package fzf
|
||||
|
||||
import (
|
||||
"math"
|
||||
"slices"
|
||||
"sort"
|
||||
"unicode"
|
||||
|
||||
@@ -15,7 +16,12 @@ type Offset [2]int32
|
||||
type colorOffset struct {
|
||||
offset [2]int32
|
||||
color tui.ColorPair
|
||||
attr tui.Attr
|
||||
match bool
|
||||
url *url
|
||||
}
|
||||
|
||||
func (co colorOffset) IsFullBgMarker(at int32) bool {
|
||||
return at == co.offset[0] && at == co.offset[1] && co.color.Attr()&tui.FullBg > 0
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
@@ -25,11 +31,9 @@ type Result struct {
|
||||
|
||||
func buildResult(item *Item, offsets []Offset, score int) Result {
|
||||
if len(offsets) > 1 {
|
||||
sort.Sort(ByOrder(offsets))
|
||||
slices.SortFunc(offsets, compareOffsets)
|
||||
}
|
||||
|
||||
result := Result{item: item}
|
||||
numChars := item.text.Length()
|
||||
minBegin := math.MaxUint16
|
||||
minEnd := math.MaxUint16
|
||||
maxEnd := 0
|
||||
@@ -37,25 +41,63 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
||||
for _, offset := range offsets {
|
||||
b, e := int(offset[0]), int(offset[1])
|
||||
if b < e {
|
||||
minBegin = util.Min(b, minBegin)
|
||||
minEnd = util.Min(e, minEnd)
|
||||
maxEnd = util.Max(e, maxEnd)
|
||||
minBegin = min(b, minBegin)
|
||||
minEnd = min(e, minEnd)
|
||||
maxEnd = max(e, maxEnd)
|
||||
validOffsetFound = true
|
||||
}
|
||||
}
|
||||
|
||||
return buildResultFromBounds(item, score, minBegin, minEnd, maxEnd, validOffsetFound)
|
||||
}
|
||||
|
||||
// buildResultFromBounds builds a Result from pre-computed offset bounds.
|
||||
func buildResultFromBounds(item *Item, score int, minBegin, minEnd, maxEnd int, validOffsetFound bool) Result {
|
||||
result := Result{item: item}
|
||||
numChars := item.text.Length()
|
||||
|
||||
for idx, criterion := range sortCriteria {
|
||||
val := uint16(math.MaxUint16)
|
||||
switch criterion {
|
||||
case byScore:
|
||||
// Higher is better
|
||||
val = math.MaxUint16 - util.AsUint16(score)
|
||||
case byChunk:
|
||||
if validOffsetFound {
|
||||
b := minBegin
|
||||
e := maxEnd
|
||||
for ; b >= 1; b-- {
|
||||
if unicode.IsSpace(item.text.Get(b - 1)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for ; e < numChars; e++ {
|
||||
if unicode.IsSpace(item.text.Get(e)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
val = util.AsUint16(e - b)
|
||||
}
|
||||
case byLength:
|
||||
val = item.TrimLength()
|
||||
case byPathname:
|
||||
if validOffsetFound {
|
||||
lastDelim := -1
|
||||
s := item.text.ToString()
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
if s[i] == '/' || s[i] == '\\' {
|
||||
lastDelim = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if lastDelim <= minBegin {
|
||||
val = util.AsUint16(minBegin - lastDelim)
|
||||
}
|
||||
}
|
||||
case byBegin, byEnd:
|
||||
if validOffsetFound {
|
||||
whitePrefixLen := 0
|
||||
for idx := 0; idx < numChars; idx++ {
|
||||
for idx := range numChars {
|
||||
r := item.text.Get(idx)
|
||||
whitePrefixLen = idx
|
||||
if idx == minBegin || !unicode.IsSpace(r) {
|
||||
@@ -65,7 +107,7 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
||||
if criterion == byBegin {
|
||||
val = util.AsUint16(minEnd - whitePrefixLen)
|
||||
} else {
|
||||
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength()))
|
||||
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/(int(item.TrimLength())+1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,21 +129,21 @@ func minRank() Result {
|
||||
return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
|
||||
}
|
||||
|
||||
func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, color tui.ColorPair, attr tui.Attr, current bool) []colorOffset {
|
||||
func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, attrNth tui.Attr, nthOverlay tui.Attr, hidden bool) []colorOffset {
|
||||
itemColors := result.item.Colors()
|
||||
|
||||
// No ANSI code, or --color=no
|
||||
if len(itemColors) == 0 {
|
||||
var offsets []colorOffset
|
||||
for _, off := range matchOffsets {
|
||||
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, attr: attr})
|
||||
// No ANSI codes
|
||||
if len(itemColors) == 0 && len(nthOffsets) == 0 {
|
||||
offsets := make([]colorOffset, len(matchOffsets))
|
||||
for i, off := range matchOffsets {
|
||||
offsets[i] = colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch, match: true}
|
||||
}
|
||||
return offsets
|
||||
}
|
||||
|
||||
// Find max column
|
||||
var maxCol int32
|
||||
for _, off := range matchOffsets {
|
||||
for _, off := range append(matchOffsets, nthOffsets...) {
|
||||
if off[1] > maxCol {
|
||||
maxCol = off[1]
|
||||
}
|
||||
@@ -111,58 +153,137 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
||||
maxCol = ansi.offset[1]
|
||||
}
|
||||
}
|
||||
cols := make([]int, maxCol)
|
||||
|
||||
type cellInfo struct {
|
||||
index int
|
||||
color bool
|
||||
match bool
|
||||
nth bool
|
||||
fbg tui.Color
|
||||
}
|
||||
|
||||
cols := make([]cellInfo, maxCol+1)
|
||||
for idx := range cols {
|
||||
cols[idx].fbg = -1
|
||||
}
|
||||
for colorIndex, ansi := range itemColors {
|
||||
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
|
||||
cols[i] = colorIndex + 1 // XXX
|
||||
if ansi.offset[0] == ansi.offset[1] && ansi.color.attr&tui.FullBg > 0 {
|
||||
cols[ansi.offset[0]].fbg = ansi.color.lbg
|
||||
} else {
|
||||
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
|
||||
cols[i] = cellInfo{colorIndex, true, false, false, cols[i].fbg}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, off := range matchOffsets {
|
||||
for i := off[0]; i < off[1]; i++ {
|
||||
cols[i] = -1
|
||||
cols[i].match = true
|
||||
}
|
||||
}
|
||||
|
||||
// sort.Sort(ByOrder(offsets))
|
||||
for _, off := range nthOffsets {
|
||||
for i := off[0]; i < off[1]; i++ {
|
||||
cols[i].nth = true
|
||||
}
|
||||
}
|
||||
|
||||
// slices.SortFunc(offsets, compareOffsets)
|
||||
|
||||
// Merge offsets
|
||||
// ------------ ---- -- ----
|
||||
// ++++++++ ++++++++++
|
||||
// --++++++++-- --++++++++++---
|
||||
curr := 0
|
||||
curr := cellInfo{0, false, false, false, -1}
|
||||
start := 0
|
||||
ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair {
|
||||
if !theme.Colored {
|
||||
return tui.NewColorPair(-1, -1, ansi.color.attr).MergeAttr(base)
|
||||
}
|
||||
// fd --color always | fzf --ansi --delimiter / --nth -1 --color fg:dim:strip,nth:regular
|
||||
if base.ShouldStripColors() {
|
||||
return base
|
||||
}
|
||||
fg := ansi.color.fg
|
||||
bg := ansi.color.bg
|
||||
if fg == -1 {
|
||||
fg = colBase.Fg()
|
||||
}
|
||||
if bg == -1 {
|
||||
bg = colBase.Bg()
|
||||
}
|
||||
return tui.NewColorPair(fg, bg, ansi.color.attr).WithUl(ansi.color.ul).MergeAttr(base)
|
||||
}
|
||||
fgAttr := tui.ColNormal.Attr()
|
||||
nthAttrFinal := fgAttr.Merge(attrNth).Merge(nthOverlay)
|
||||
nthBase := colBase.WithNewAttr(nthAttrFinal)
|
||||
|
||||
var colors []colorOffset
|
||||
add := func(idx int) {
|
||||
if curr != 0 && idx > start {
|
||||
if curr == -1 {
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)}, color: color, attr: attr})
|
||||
} else {
|
||||
ansi := itemColors[curr-1]
|
||||
fg := ansi.color.fg
|
||||
bg := ansi.color.bg
|
||||
if theme != nil {
|
||||
if fg == -1 {
|
||||
if current {
|
||||
fg = theme.Current
|
||||
} else {
|
||||
fg = theme.Fg
|
||||
}
|
||||
}
|
||||
if bg == -1 {
|
||||
if current {
|
||||
bg = theme.DarkBg
|
||||
} else {
|
||||
bg = theme.Bg
|
||||
if curr.fbg >= 0 {
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(start)},
|
||||
color: tui.NewColorPair(-1, curr.fbg, tui.FullBg),
|
||||
match: false,
|
||||
url: nil})
|
||||
}
|
||||
if (curr.color || curr.nth || curr.match) && idx > start {
|
||||
if curr.match {
|
||||
var color tui.ColorPair
|
||||
if curr.nth {
|
||||
color = nthBase.Merge(colMatch)
|
||||
} else {
|
||||
color = colBase.Merge(colMatch)
|
||||
}
|
||||
var url *url
|
||||
if curr.color {
|
||||
ansi := itemColors[curr.index]
|
||||
url = ansi.color.url
|
||||
origColor := ansiToColorPair(ansi, colMatch)
|
||||
// hl or hl+ only sets the foreground color, so colMatch is the
|
||||
// combination of either [hl and bg] or [hl+ and bg+].
|
||||
//
|
||||
// If the original text already has background color, and the
|
||||
// foreground color of colMatch is -1, we shouldn't only apply the
|
||||
// background color of colMatch.
|
||||
// e.g. echo -e "\x1b[32;7mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline
|
||||
// echo -e "\x1b[42mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline
|
||||
if color.Fg().IsDefault() && origColor.HasBg() {
|
||||
color = origColor
|
||||
if curr.nth {
|
||||
color = color.WithAttr((attrNth &^ tui.AttrRegular).Merge(nthOverlay))
|
||||
}
|
||||
} else {
|
||||
color = origColor.MergeNonDefault(color)
|
||||
}
|
||||
}
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)}, color: color, match: true, url: url})
|
||||
} else if curr.color {
|
||||
ansi := itemColors[curr.index]
|
||||
base := colBase
|
||||
if curr.nth {
|
||||
base = nthBase
|
||||
}
|
||||
if hidden {
|
||||
base = base.WithFg(theme.Nomatch)
|
||||
}
|
||||
color := ansiToColorPair(ansi, base)
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)},
|
||||
color: tui.NewColorPair(fg, bg),
|
||||
attr: ansi.color.attr.Merge(attr)})
|
||||
color: color,
|
||||
match: false,
|
||||
url: ansi.color.url})
|
||||
} else {
|
||||
color := nthBase
|
||||
if hidden {
|
||||
color = color.WithFg(theme.Nomatch)
|
||||
}
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)},
|
||||
color: color,
|
||||
match: false,
|
||||
url: nil})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,21 +298,20 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
||||
return colors
|
||||
}
|
||||
|
||||
// ByOrder is for sorting substring offsets
|
||||
type ByOrder []Offset
|
||||
|
||||
func (a ByOrder) Len() int {
|
||||
return len(a)
|
||||
}
|
||||
|
||||
func (a ByOrder) Swap(i, j int) {
|
||||
a[i], a[j] = a[j], a[i]
|
||||
}
|
||||
|
||||
func (a ByOrder) Less(i, j int) bool {
|
||||
ioff := a[i]
|
||||
joff := a[j]
|
||||
return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1])
|
||||
func compareOffsets(a, b Offset) int {
|
||||
if a[0] < b[0] {
|
||||
return -1
|
||||
}
|
||||
if a[0] > b[0] {
|
||||
return 1
|
||||
}
|
||||
if a[1] < b[1] {
|
||||
return -1
|
||||
}
|
||||
if a[1] > b[1] {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ByRelevance is for sorting Items
|
||||
@@ -223,3 +343,79 @@ func (a ByRelevanceTac) Swap(i, j int) {
|
||||
func (a ByRelevanceTac) Less(i, j int) bool {
|
||||
return compareRanks(a[i], a[j], true)
|
||||
}
|
||||
|
||||
// radixSortResults sorts Results by their points key using LSD radix sort.
|
||||
// O(n) time complexity vs O(n log n) for comparison sort.
|
||||
// The sort is stable, so equal-key items maintain original (item-index) order.
|
||||
// For tac mode, runs of equal keys are reversed after sorting.
|
||||
func radixSortResults(a []Result, tac bool, scratch []Result) []Result {
|
||||
n := len(a)
|
||||
if n < 128 {
|
||||
if tac {
|
||||
sort.Sort(ByRelevanceTac(a))
|
||||
} else {
|
||||
sort.Sort(ByRelevance(a))
|
||||
}
|
||||
return scratch[:0]
|
||||
}
|
||||
|
||||
if cap(scratch) < n {
|
||||
scratch = make([]Result, n)
|
||||
}
|
||||
buf := scratch[:n]
|
||||
src, dst := a, buf
|
||||
scattered := 0
|
||||
|
||||
for pass := range 8 {
|
||||
shift := uint(pass) * 8
|
||||
|
||||
var count [256]int
|
||||
for i := range src {
|
||||
count[byte(sortKey(&src[i])>>shift)]++
|
||||
}
|
||||
|
||||
// Skip if all items have the same byte value at this position
|
||||
if count[byte(sortKey(&src[0])>>shift)] == n {
|
||||
continue
|
||||
}
|
||||
|
||||
var offset [256]int
|
||||
for i := 1; i < 256; i++ {
|
||||
offset[i] = offset[i-1] + count[i-1]
|
||||
}
|
||||
|
||||
for i := range src {
|
||||
b := byte(sortKey(&src[i]) >> shift)
|
||||
dst[offset[b]] = src[i]
|
||||
offset[b]++
|
||||
}
|
||||
|
||||
src, dst = dst, src
|
||||
scattered++
|
||||
}
|
||||
|
||||
// If odd number of scatters, data is in buf, copy back to a
|
||||
if scattered%2 == 1 {
|
||||
copy(a, src)
|
||||
}
|
||||
|
||||
// Handle tac: reverse runs of equal keys so equal-key items
|
||||
// are in reverse item-index order
|
||||
if tac {
|
||||
i := 0
|
||||
for i < n {
|
||||
ki := sortKey(&a[i])
|
||||
j := i + 1
|
||||
for j < n && sortKey(&a[j]) == ki {
|
||||
j++
|
||||
}
|
||||
if j-i > 1 {
|
||||
for l, r := i, j-1; l < r; l, r = l+1, r-1 {
|
||||
a[l], a[r] = a[r], a[l]
|
||||
}
|
||||
}
|
||||
i = j
|
||||
}
|
||||
}
|
||||
return scratch
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// +build !386,!amd64
|
||||
//go:build !386 && !amd64 && !arm64
|
||||
|
||||
package fzf
|
||||
|
||||
@@ -14,3 +14,7 @@ func compareRanks(irank Result, jrank Result, tac bool) bool {
|
||||
}
|
||||
return (irank.item.Index() <= jrank.item.Index()) != tac
|
||||
}
|
||||
|
||||
func sortKey(r *Result) uint64 {
|
||||
return uint64(r.points[0]) | uint64(r.points[1])<<16 | uint64(r.points[2])<<32 | uint64(r.points[3])<<48
|
||||
}
|
||||
|
||||
+171
-33
@@ -1,9 +1,9 @@
|
||||
// +build !tcell
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/rand"
|
||||
"slices"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
@@ -18,9 +18,9 @@ func withIndex(i *Item, index int) *Item {
|
||||
|
||||
func TestOffsetSort(t *testing.T) {
|
||||
offsets := []Offset{
|
||||
Offset{3, 5}, Offset{2, 7},
|
||||
Offset{1, 3}, Offset{2, 9}}
|
||||
sort.Sort(ByOrder(offsets))
|
||||
{3, 5}, {2, 7},
|
||||
{1, 3}, {2, 9}}
|
||||
slices.SortFunc(offsets, compareOffsets)
|
||||
|
||||
if offsets[0][0] != 1 || offsets[0][1] != 3 ||
|
||||
offsets[1][0] != 2 || offsets[1][1] != 7 ||
|
||||
@@ -56,9 +56,9 @@ func TestResultRank(t *testing.T) {
|
||||
// FIXME global
|
||||
sortCriteria = []criterion{byScore, byLength}
|
||||
|
||||
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
|
||||
str := []rune("foo")
|
||||
item1 := buildResult(
|
||||
withIndex(&Item{text: util.RunesToChars(strs[0])}, 1), []Offset{}, 2)
|
||||
withIndex(&Item{text: util.RunesToChars(str)}, 1), []Offset{}, 2)
|
||||
if item1.points[3] != math.MaxUint16-2 || // Bonus
|
||||
item1.points[2] != 3 || // Length
|
||||
item1.points[1] != 0 || // Unused
|
||||
@@ -67,7 +67,7 @@ func TestResultRank(t *testing.T) {
|
||||
t.Error(item1)
|
||||
}
|
||||
// Only differ in index
|
||||
item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2)
|
||||
item2 := buildResult(&Item{text: util.RunesToChars(str)}, []Offset{}, 2)
|
||||
|
||||
items := []Result{item1, item2}
|
||||
sort.Sort(ByRelevance(items))
|
||||
@@ -84,13 +84,13 @@ func TestResultRank(t *testing.T) {
|
||||
|
||||
// Sort by relevance
|
||||
item3 := buildResult(
|
||||
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 3)
|
||||
withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 3)
|
||||
item4 := buildResult(
|
||||
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 4)
|
||||
withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 4)
|
||||
item5 := buildResult(
|
||||
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 5)
|
||||
withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 5)
|
||||
item6 := buildResult(
|
||||
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 6)
|
||||
withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 6)
|
||||
items = []Result{item1, item2, item3, item4, item5, item6}
|
||||
sort.Sort(ByRelevance(items))
|
||||
if !(items[0] == item6 && items[1] == item5 &&
|
||||
@@ -100,37 +100,175 @@ func TestResultRank(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkTiebreak(t *testing.T) {
|
||||
// FIXME global
|
||||
sortCriteria = []criterion{byScore, byChunk}
|
||||
|
||||
score := 100
|
||||
test := func(input string, offset Offset, chunk string) {
|
||||
item := buildResult(withIndex(&Item{text: util.RunesToChars([]rune(input))}, 1), []Offset{offset}, score)
|
||||
if !(item.points[3] == math.MaxUint16-uint16(score) && item.points[2] == uint16(len(chunk))) {
|
||||
t.Error(item.points)
|
||||
}
|
||||
}
|
||||
test("hello foobar goodbye", Offset{8, 9}, "foobar")
|
||||
test("hello foobar goodbye", Offset{7, 18}, "foobar goodbye")
|
||||
test("hello foobar goodbye", Offset{0, 1}, "hello")
|
||||
test("hello foobar goodbye", Offset{5, 7}, "hello foobar") // TBD
|
||||
}
|
||||
|
||||
func TestColorOffset(t *testing.T) {
|
||||
// ------------ 20 ---- -- ----
|
||||
// ++++++++ ++++++++++
|
||||
// --++++++++-- --++++++++++---
|
||||
|
||||
offsets := []Offset{Offset{5, 15}, Offset{25, 35}}
|
||||
offsets := []Offset{{5, 15}, {10, 12}, {25, 35}}
|
||||
item := Result{
|
||||
item: &Item{
|
||||
colors: &[]ansiOffset{
|
||||
ansiOffset{[2]int32{0, 20}, ansiState{1, 5, 0}},
|
||||
ansiOffset{[2]int32{22, 27}, ansiState{2, 6, tui.Bold}},
|
||||
ansiOffset{[2]int32{30, 32}, ansiState{3, 7, 0}},
|
||||
ansiOffset{[2]int32{33, 40}, ansiState{4, 8, tui.Bold}}}}}
|
||||
// [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}]
|
||||
{[2]int32{0, 20}, ansiState{1, 5, -1, 0, -1, nil}},
|
||||
{[2]int32{22, 27}, ansiState{2, 6, -1, tui.Bold, -1, nil}},
|
||||
{[2]int32{30, 32}, ansiState{3, 7, -1, 0, -1, nil}},
|
||||
{[2]int32{33, 40}, ansiState{4, 8, -1, tui.Bold, -1, nil}}}}}
|
||||
|
||||
pair := tui.NewColorPair(99, 199)
|
||||
colors := item.colorOffsets(offsets, tui.Dark256, pair, tui.AttrRegular, true)
|
||||
assert := func(idx int, b int32, e int32, c tui.ColorPair, bold bool) {
|
||||
var attr tui.Attr
|
||||
if bold {
|
||||
attr = tui.Bold
|
||||
}
|
||||
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
|
||||
colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
|
||||
colors := item.colorOffsets(offsets, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined, 0, false)
|
||||
assert := func(idx int, b int32, e int32, c tui.ColorPair) {
|
||||
o := colors[idx]
|
||||
if o.offset[0] != b || o.offset[1] != e || o.color != c || o.attr != attr {
|
||||
t.Error(o)
|
||||
if o.offset[0] != b || o.offset[1] != e || o.color != c {
|
||||
t.Error(o, b, e, c)
|
||||
}
|
||||
}
|
||||
// [{[0 5] {1 5 0}} {[5 15] {99 199 0}} {[15 20] {1 5 0}}
|
||||
// {[22 25] {2 6 1}} {[25 27] {99 199 1}} {[27 30] {99 199 0}}
|
||||
// {[30 32] {99 199 0}} {[32 33] {99 199 0}} {[33 35] {99 199 1}}
|
||||
// {[35 40] {4 8 1}}]
|
||||
assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
||||
assert(1, 5, 15, colMatch)
|
||||
assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
||||
assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold))
|
||||
assert(4, 25, 27, colMatch.WithAttr(tui.Bold))
|
||||
assert(5, 27, 30, colMatch)
|
||||
assert(6, 30, 32, colMatch)
|
||||
assert(7, 32, 33, colMatch) // TODO: Should we merge consecutive blocks?
|
||||
assert(8, 33, 35, colMatch.WithAttr(tui.Bold))
|
||||
assert(9, 35, 40, tui.NewColorPair(4, 8, tui.Bold))
|
||||
|
||||
colRegular := tui.NewColorPair(-1, -1, tui.AttrUndefined)
|
||||
colUnderline := tui.NewColorPair(-1, -1, tui.Underline)
|
||||
|
||||
nthOffsets := []Offset{{37, 39}, {42, 45}}
|
||||
for _, attr := range []tui.Attr{tui.AttrRegular, tui.StrikeThrough} {
|
||||
colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, attr, 0, false)
|
||||
|
||||
// [{[0 5] {1 5 0}} {[5 15] {1 5 8}} {[15 20] {1 5 0}}
|
||||
// {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}}
|
||||
// {[30 32] {3 7 8}} {[32 33] {-1 -1 8}} {[33 35] {4 8 9}}
|
||||
// {[35 37] {4 8 1}} {[37 39] {4 8 x|1}} {[39 40] {4 8 x|1}}]
|
||||
assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
||||
assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline))
|
||||
assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
||||
assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold))
|
||||
assert(4, 25, 27, tui.NewColorPair(2, 6, tui.Bold|tui.Underline))
|
||||
assert(5, 27, 30, colUnderline)
|
||||
assert(6, 30, 32, tui.NewColorPair(3, 7, tui.Underline))
|
||||
assert(7, 32, 33, colUnderline)
|
||||
assert(8, 33, 35, tui.NewColorPair(4, 8, tui.Bold|tui.Underline))
|
||||
assert(9, 35, 37, tui.NewColorPair(4, 8, tui.Bold))
|
||||
expected := tui.Bold | attr
|
||||
if attr == tui.AttrRegular {
|
||||
expected = tui.Bold
|
||||
}
|
||||
assert(10, 37, 39, tui.NewColorPair(4, 8, expected))
|
||||
assert(11, 39, 40, tui.NewColorPair(4, 8, tui.Bold))
|
||||
}
|
||||
|
||||
// Test nthOverlay: simulates nth:regular with current-fg:underline
|
||||
// The overlay (underline) should survive even though nth:regular clears attrs.
|
||||
// Precedence: fg < nth < current-fg
|
||||
colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, tui.AttrRegular, tui.Underline, false)
|
||||
|
||||
// nth regions should have Underline (from overlay), not cleared by AttrRegular
|
||||
// Non-nth regions keep colBase attrs (AttrUndefined)
|
||||
assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
||||
assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline))
|
||||
assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
||||
assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold))
|
||||
assert(4, 25, 27, tui.NewColorPair(2, 6, tui.Bold|tui.Underline))
|
||||
assert(5, 27, 30, colUnderline)
|
||||
assert(6, 30, 32, tui.NewColorPair(3, 7, tui.Underline))
|
||||
assert(7, 32, 33, colUnderline)
|
||||
assert(8, 33, 35, tui.NewColorPair(4, 8, tui.Bold|tui.Underline))
|
||||
assert(9, 35, 37, tui.NewColorPair(4, 8, tui.Bold))
|
||||
// nth region within ANSI bold: AttrRegular clears, overlay adds Underline back
|
||||
assert(10, 37, 39, tui.NewColorPair(4, 8, tui.Bold|tui.Underline))
|
||||
assert(11, 39, 40, tui.NewColorPair(4, 8, tui.Bold))
|
||||
|
||||
// Test nthOverlay with additive attrs: nth:strikethrough with selected-fg:bold
|
||||
colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, tui.StrikeThrough, tui.Bold, false)
|
||||
|
||||
// Non-nth entries unchanged from overlay=0 case
|
||||
assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
||||
assert(5, 27, 30, colUnderline) // match only, no nth
|
||||
assert(7, 32, 33, colUnderline) // match only, no nth
|
||||
// nth region within ANSI bold: StrikeThrough|Bold merged with ANSI Bold
|
||||
assert(10, 37, 39, tui.NewColorPair(4, 8, tui.Bold|tui.StrikeThrough))
|
||||
}
|
||||
|
||||
func TestRadixSortResults(t *testing.T) {
|
||||
sortCriteria = []criterion{byScore, byLength}
|
||||
|
||||
rng := rand.New(rand.NewSource(42))
|
||||
|
||||
for _, n := range []int{128, 256, 500, 1000} {
|
||||
for _, tac := range []bool{false, true} {
|
||||
// Build items with random points and indices
|
||||
items := make([]*Item, n)
|
||||
for i := range items {
|
||||
items[i] = &Item{text: util.Chars{Index: int32(i)}}
|
||||
}
|
||||
|
||||
results := make([]Result, n)
|
||||
for i := range results {
|
||||
results[i] = Result{
|
||||
item: items[i],
|
||||
points: [4]uint16{
|
||||
uint16(rng.Intn(256)),
|
||||
uint16(rng.Intn(256)),
|
||||
uint16(rng.Intn(256)),
|
||||
uint16(rng.Intn(256)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Make some duplicates to test stability
|
||||
for i := 0; i < n/4; i++ {
|
||||
j := rng.Intn(n)
|
||||
k := rng.Intn(n)
|
||||
results[j].points = results[k].points
|
||||
}
|
||||
|
||||
// Copy for reference sort
|
||||
expected := make([]Result, n)
|
||||
copy(expected, results)
|
||||
if tac {
|
||||
sort.Sort(ByRelevanceTac(expected))
|
||||
} else {
|
||||
sort.Sort(ByRelevance(expected))
|
||||
}
|
||||
|
||||
// Radix sort
|
||||
var scratch []Result
|
||||
scratch = radixSortResults(results, tac, scratch)
|
||||
|
||||
for i := range results {
|
||||
if results[i] != expected[i] {
|
||||
t.Errorf("n=%d tac=%v: mismatch at index %d: got item %d, want item %d",
|
||||
n, tac, i, results[i].item.Index(), expected[i].item.Index())
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert(0, 0, 5, tui.NewColorPair(1, 5), false)
|
||||
assert(1, 5, 15, pair, false)
|
||||
assert(2, 15, 20, tui.NewColorPair(1, 5), false)
|
||||
assert(3, 22, 25, tui.NewColorPair(2, 6), true)
|
||||
assert(4, 25, 35, pair, false)
|
||||
assert(5, 35, 40, tui.NewColorPair(4, 8), true)
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,4 +1,4 @@
|
||||
// +build 386 amd64
|
||||
//go:build 386 || amd64 || arm64
|
||||
|
||||
package fzf
|
||||
|
||||
@@ -14,3 +14,7 @@ func compareRanks(irank Result, jrank Result, tac bool) bool {
|
||||
}
|
||||
return (irank.item.Index() <= jrank.item.Index()) != tac
|
||||
}
|
||||
|
||||
func sortKey(r *Result) uint64 {
|
||||
return *(*uint64)(unsafe.Pointer(&r.points[0]))
|
||||
}
|
||||
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var getRegex *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
getRegex = regexp.MustCompile(`^GET /(?:\?([a-z0-9=&]+))? HTTP`)
|
||||
}
|
||||
|
||||
type getParams struct {
|
||||
limit int
|
||||
offset int
|
||||
}
|
||||
|
||||
const (
|
||||
crlf = "\r\n"
|
||||
httpOk = "HTTP/1.1 200 OK" + crlf
|
||||
httpBadRequest = "HTTP/1.1 400 Bad Request" + crlf
|
||||
httpUnauthorized = "HTTP/1.1 401 Unauthorized" + crlf
|
||||
httpUnavailable = "HTTP/1.1 503 Service Unavailable" + crlf
|
||||
httpReadTimeout = 10 * time.Second
|
||||
channelTimeout = 2 * time.Second
|
||||
jsonContentType = "Content-Type: application/json" + crlf
|
||||
maxContentLength = 1024 * 1024
|
||||
)
|
||||
|
||||
type httpServer struct {
|
||||
apiKey []byte
|
||||
actionChannel chan []*action
|
||||
getHandler func(getParams) string
|
||||
}
|
||||
|
||||
type listenAddress struct {
|
||||
host string
|
||||
port int
|
||||
sock string
|
||||
}
|
||||
|
||||
func (addr listenAddress) IsLocal() bool {
|
||||
return addr.host == "localhost" || addr.host == "127.0.0.1" || len(addr.sock) > 0
|
||||
}
|
||||
|
||||
var defaultListenAddr = listenAddress{"localhost", 0, ""}
|
||||
|
||||
func parseListenAddress(address string) (listenAddress, error) {
|
||||
if strings.HasSuffix(address, ".sock") {
|
||||
return listenAddress{"", 0, address}, nil
|
||||
}
|
||||
|
||||
parts := strings.SplitN(address, ":", 3)
|
||||
if len(parts) == 1 {
|
||||
parts = []string{"localhost", parts[0]}
|
||||
}
|
||||
if len(parts) != 2 {
|
||||
return defaultListenAddr, fmt.Errorf("invalid listen address: %s", address)
|
||||
}
|
||||
portStr := parts[len(parts)-1]
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil || port < 0 || port > 65535 {
|
||||
return defaultListenAddr, fmt.Errorf("invalid listen port: %s", portStr)
|
||||
}
|
||||
if len(parts[0]) == 0 {
|
||||
parts[0] = "localhost"
|
||||
}
|
||||
return listenAddress{parts[0], port, ""}, nil
|
||||
}
|
||||
|
||||
func startHttpServer(address listenAddress, actionChannel chan []*action, getHandler func(getParams) string) (net.Listener, int, error) {
|
||||
host := address.host
|
||||
port := address.port
|
||||
apiKey := os.Getenv("FZF_API_KEY")
|
||||
if !address.IsLocal() && len(apiKey) == 0 {
|
||||
return nil, port, errors.New("FZF_API_KEY is required to allow remote access")
|
||||
}
|
||||
|
||||
var listener net.Listener
|
||||
var err error
|
||||
if len(address.sock) > 0 {
|
||||
if _, err := os.Stat(address.sock); err == nil {
|
||||
// Check if the socket is already in use
|
||||
if conn, err := net.Dial("unix", address.sock); err == nil {
|
||||
conn.Close()
|
||||
return nil, 0, fmt.Errorf("socket already in use: %s", address.sock)
|
||||
}
|
||||
os.Remove(address.sock)
|
||||
}
|
||||
listener, err = net.Listen("unix", address.sock)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to listen on %s", address.sock)
|
||||
}
|
||||
os.Chmod(address.sock, 0600)
|
||||
} else {
|
||||
addrStr := fmt.Sprintf("%s:%d", host, port)
|
||||
listener, err = net.Listen("tcp", addrStr)
|
||||
if err != nil {
|
||||
return nil, port, fmt.Errorf("failed to listen on %s", addrStr)
|
||||
}
|
||||
if port == 0 {
|
||||
addr := listener.Addr().String()
|
||||
parts := strings.Split(addr, ":")
|
||||
if len(parts) < 2 {
|
||||
return nil, port, fmt.Errorf("cannot extract port: %s", addr)
|
||||
}
|
||||
var err error
|
||||
port, err = strconv.Atoi(parts[len(parts)-1])
|
||||
if err != nil {
|
||||
return nil, port, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
server := httpServer{
|
||||
apiKey: []byte(apiKey),
|
||||
actionChannel: actionChannel,
|
||||
getHandler: getHandler,
|
||||
}
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
conn.Write([]byte(server.handleHttpRequest(conn)))
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
return listener, port, nil
|
||||
}
|
||||
|
||||
// Here we are writing a simplistic HTTP server without using net/http
|
||||
// package to reduce the size of the binary.
|
||||
//
|
||||
// * No --listen: 2.8MB
|
||||
// * --listen with net/http: 5.7MB
|
||||
// * --listen w/o net/http: 3.3MB
|
||||
func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
||||
contentLength := 0
|
||||
apiKey := ""
|
||||
body := ""
|
||||
answer := func(code string, message string) string {
|
||||
message += "\n"
|
||||
return code + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message)
|
||||
}
|
||||
unauthorized := func(message string) string {
|
||||
return answer(httpUnauthorized, message)
|
||||
}
|
||||
bad := func(message string) string {
|
||||
return answer(httpBadRequest, message)
|
||||
}
|
||||
good := func(message string) string {
|
||||
return answer(httpOk+jsonContentType, message)
|
||||
}
|
||||
conn.SetReadDeadline(time.Now().Add(httpReadTimeout))
|
||||
scanner := bufio.NewScanner(conn)
|
||||
scanner.Split(func(data []byte, atEOF bool) (int, []byte, error) {
|
||||
found := bytes.Index(data, []byte(crlf))
|
||||
if found >= 0 {
|
||||
token := data[:found+len(crlf)]
|
||||
return len(token), token, nil
|
||||
}
|
||||
if atEOF || len(body)+len(data) >= contentLength {
|
||||
return 0, data, bufio.ErrFinalToken
|
||||
}
|
||||
return 0, nil, nil
|
||||
})
|
||||
|
||||
section := 0
|
||||
var getMatch []string
|
||||
Loop:
|
||||
for scanner.Scan() {
|
||||
text := scanner.Text()
|
||||
switch section {
|
||||
case 0: // Request line
|
||||
getMatch = getRegex.FindStringSubmatch(text)
|
||||
if len(getMatch) == 0 && !strings.HasPrefix(text, "POST / HTTP") {
|
||||
return bad("invalid request method")
|
||||
}
|
||||
section++
|
||||
case 1: // Request headers
|
||||
if text == crlf { // End of headers
|
||||
if len(getMatch) > 0 {
|
||||
break Loop
|
||||
}
|
||||
if contentLength == 0 {
|
||||
return bad("content-length header missing")
|
||||
}
|
||||
section++
|
||||
continue
|
||||
}
|
||||
pair := strings.SplitN(text, ":", 2)
|
||||
if len(pair) == 2 {
|
||||
switch strings.ToLower(pair[0]) {
|
||||
case "content-length":
|
||||
length, err := strconv.Atoi(strings.TrimSpace(pair[1]))
|
||||
if err != nil || length <= 0 || length > maxContentLength {
|
||||
return bad("invalid content length")
|
||||
}
|
||||
contentLength = length
|
||||
case "x-api-key":
|
||||
apiKey = strings.TrimSpace(pair[1])
|
||||
}
|
||||
}
|
||||
case 2: // Request body
|
||||
body += text
|
||||
}
|
||||
}
|
||||
|
||||
if len(server.apiKey) != 0 && subtle.ConstantTimeCompare([]byte(apiKey), server.apiKey) != 1 {
|
||||
return unauthorized("invalid api key")
|
||||
}
|
||||
|
||||
if len(getMatch) > 0 {
|
||||
response := server.getHandler(parseGetParams(getMatch[1]))
|
||||
if len(response) > 0 {
|
||||
return good(response)
|
||||
}
|
||||
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
|
||||
}
|
||||
|
||||
if len(body) < contentLength {
|
||||
return bad("incomplete request")
|
||||
}
|
||||
body = body[:contentLength]
|
||||
|
||||
actions, err := parseSingleActionList(strings.Trim(string(body), "\r\n"))
|
||||
if err != nil {
|
||||
return bad(err.Error())
|
||||
}
|
||||
if len(actions) == 0 {
|
||||
return bad("no action specified")
|
||||
}
|
||||
|
||||
select {
|
||||
case server.actionChannel <- actions:
|
||||
case <-time.After(channelTimeout):
|
||||
return httpUnavailable + crlf
|
||||
}
|
||||
return httpOk + crlf
|
||||
}
|
||||
|
||||
func parseGetParams(query string) getParams {
|
||||
params := getParams{limit: 100, offset: 0}
|
||||
for _, pair := range strings.Split(query, "&") {
|
||||
parts := strings.SplitN(pair, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
switch parts[0] {
|
||||
case "limit", "offset":
|
||||
if val, err := strconv.Atoi(parts[1]); err == nil {
|
||||
if parts[0] == "limit" {
|
||||
params.limit = val
|
||||
} else {
|
||||
params.offset = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
+7266
-1086
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user